7 ms·
I’m glad people find value in patterns like these, but I’m so sick of them. Plain Ruby code is so much easier to debug, which is all that matters in the long ru
by corytheboyd 3y ago
I’m glad people find value in patterns like these, but I’m so sick of them. Plain Ruby code is so much easier to debug, which is all that matters in the long run. The senior engineer who introduced the thing like this inevitably leaves and nobody cares to learn some bespoke abstraction enough to keep using it.
I’m sure this is great and solves real problems for OP and friends. I just ask that you think twice about it before forcing it on coworkers. Please, this poor soul can’t take it anymore.
- ismaelct 3y agoThanks for your feedback. Out of curiosity, when you say "plain Ruby code" what do you mean, exactly? Presumably you're still making use of _some_ patterns that you think are Ok.
- graypegg 3y agoIf I had to guess, “plain Ruby” is ruby that fails with a sensible stack trace without internal things showing up. That’s always been my trouble with complex abstractions, it can make it very difficult to use with a debugger especially, which is how I always work with Ruby. New flow control models like this are the main culprits to weird debugger issues. I haven’t gotten a chance to mess around with this specifically, how much of its internals are mixed into execution?
- ismaelct 3y agoGood observation about stack traces and abstractions. Re. your question, the pattern itself is no different than, say, Rack middleware, so you'd see similar cost and benefits. In essence you're running one callable object after the other. A pipeline is essentially this steps = [ ->(r) { r }, ->(r) { r }, ->(r) { r }, ] Wrap initial data in a common Value object initial = Result.new(some_data) Run the Result through the steps, in order result = steps.reduce(initial) { |r, step| step.call(r) } That's the pattern, really. A reduce operation. Re. stack trace, it can add noise because you're iterating over steps instead of calling them procedurally one by one, and you may want to decorate steps (put steps inside steps) for encapsulation, caching, etc. but again no different than Rack.
- codesnik 3y agoyes, it is similar to rack. But what rack has this requirement of being pluggable from multiple decoupled codebases (rails or other framework, your app, some gems you're adding to your app). And that also warranted things like "insert_before" etc. Even with rack, decoupling of code definition (where you've added your middleware) and code execution could be pretty hard to debug, but at least with enough exposure you know where to look, and you usually have only one rack stack in your app. If all those steps in your pipeline are defined in the same codebase, rack approach becomes much less useful.
- ismaelct 3y ago> If all those steps in your pipeline are defined in the same codebase I find that rule a bit arbitrary. For example, I have data-import pipelines where some steps are unique to each task, but some others are shared across tasks. Why does it matter whether the steps are defined in the same codebase or not?
- corytheboyd 3y agoPretty much this. I want traces to go to actual code as much as possible. I also want my IDE to work. One of the most annoying things in my life right now is aasm. It’s a good idea, but I hate having to know this new dsl, I hate the dynamic methods it defines, I hate having to constantly explain to the confused folks that this random bang method is actually triggering this `event :event_name` over here. I would just so much rather have none of the DSL and meta-programming stuff, even if it’s twice as much “boilerplate”, if that means people who understand normal code can actually trace what the program is doing using the tools they already know how to use. Certainly don’t mind abstraction like a standardized Result class, it’s more the chaining and custom halt with error patterns that I can tell are going to complicate debugging.
- ismaelct 3y agoWhat do you think of the Rack interface? Or ActiveRecord's query chaining? ex. User.where(admin: true).joins(:account).order(id: :asc) It's all versions of the same approach. And yes all of the above _can_ complicate debugging, for sure. But so can most abstractions. Only use them if the specific problem they solve is bigger than the drawbacks.
- graypegg 3y agoTo me, Rack and AR are scoped to a specific domain. HTTP request handling and database-backed models are naturally going to involve some internal noise since I don’t want to write a web server or a database interface layer. I want the things I don’t want to write to be called. Here, this is closer to general purpose flow control. There isn’t really a library I wouldn’t want to write myself here. You show a lot of examples of things that could be accomplished by objects and messaging passing (“plain Ruby”) which might be preferable for some. All in all though, it’s a really interesting idea! I still really like the Ruby community’s love of bending ruby to their will. (Even if I do think it can cause issues sometimes)
- ismaelct 3y agoI tried to keep this as idiomatic to Ruby as possible. No meta-programming, no FP machinery like monads, etc. Little more to it than an array of objects and a reduce function. All plain Ruby in my book. I wonder if just using "throw :halt" (like in AR callbacks, to halt callback chains) or an Enumerator with "raise StopIteration" (which would work) would throw people off a bit less, just on account of those things being more familiar.
- Lio 3y agoIt's always worth reading Against Railway-Oriented Programming[1] before you commit to using it. Personally I've dealt with enough Trailblazer code now that for business logic I think I'd much rather work with a simple ruby class with a procedural interface. I like the use of a Result object but not at the expense of exception handling or bloated stacktraces full of library code. Being lightweight, easy to debug and upgrade is underrated IMHO. Having said all that, I still think this is a really interesting article and great food for thought depending the usecase. 1. https://fsharpforfunandprofit.com/posts/against-railway-oriented-programming/ https://fsharpforfunandprofit.com/posts/against-railway-orie...
- ismaelct 3y agoVery good article, thank you. I would add that the tagline says "when used thoughtlessly". For example, I agree that using Result objects to reinvent exceptions is a bad idea. There's a reason they're called "exceptions". Error results should only be used for domain-level errors. ie. things that _your expect_ to go wrong in the domain. Also note that in my article I use continue/halt, not Ok/Error. At the library level it's just a way to compose functions together with a mechanism to halt processing. Whether something halted execution because it was an error, or any other reason (ex. caching), is up to your app's semantics.
- codesnik 3y ago> The senior engineer who introduced the thing like this ... I actually consider the urge to introduce overengineered patterns like this a sure sign of a "mid-level developer". Senior knows better.
- ismaelct 3y agoI would have thought that "senior" means assessing things in context instead of falling back to truisms. "over engineering" only makes sense relative to a concrete problem you want to solve.
- codesnik 3y agoyes, of course. I usually am all about context. It's just this particular example is over-engineered in most context I've seen (and can imagine) in my 18 years of using ruby. Not much overengineered, I've seen much, much worse, but it is still annoying enough.
- corytheboyd 3y agoI’d say mid-levels will follow along, but newer seniors will confidently throw ideas out for all to observe how gloriously smart and perfect they are.
- ismaelct 3y agoAmazing.
- whstl 3y agoYou both are probably right. I see a lot of difference in how different companies or different dev cultures see seniors. A lot of what people used to call senior has become "staff" in larger companies.
- corytheboyd 3y agoTrue true, my org doesn’t even have a staff level, it goes from senior to principal. Not meant as a rebuttal, meant as a reinforcement of role names meaning less.
- jtms 3y agoI'd like to think this is kind of just the cost of discovering that 1 pattern out of many thousands that is actually useful. I am going to guess the Gang of Four didn't just sit down and bang out the entire catalog of design patterns in one extremely productive programming session. They likely battle tested hundreds of different patterns, most of them being thrown out as not useful or a bad abstraction before arriving at the set they published in the book.
- Draiken 3y agoI wrote a whole blog post on this because of the amount of harm caused by these types of systems. Interactors, interactions, pipelines or whatever other name, can very easily ruin a codebase in my experience. Already happened multiple times in my career. Developers don't like to think about abstractions and naming is a known hard problem. People pick this up and now everything is easy. Every name becomes: DoThis, DoThat, DoThisOtherThing and only answers to `call`. No need to think! Now you have deeply nested procedures that oversimplify their interfaces to match the library and suddenly you no longer have meaningful error types, you have strings. You no longer have abstractions, you only have procedural code with some objects in the middle. The list goes on. Like most things in development, used in the right places and with careful consideration, this can be very valuable. But this is a very insidious change that turns your code into a functional style with several drawbacks that are rarely considered.
- ismaelct 3y agoI agree with the drawbacks listed here. I would add that "careful consideration" of any pattern we use is the job description. I would further add that we should extend that thoughtfulness to the opinionated frameworks many of us rely on. They usually come with hundreds of complicated patterns baked in, and we "oversimplify our interfaces" to match the framework as a matter of course. All I'm saying is that committing to any mental model provided by a design pattern or framework has similar drawbacks. In this case, the mental model is "a big operation is a list of smaller operations in a chain". Use with caution. > No need to think! Not sure I agree with fully. If anything, I've found that I need to think harder about what constitutes a step in a workflow, what are the names of each stage, what concept they encapsulate. I can't just chuck everything into a god object or a deeply nested hierarchy tree somewhere. But again I agree. Using this pattern I've definitely over-complicated or gone down the wrong path at times. I would say though that I found it easier to roll back and change direction when compared deeply nested object graphs, for example.
- lamontcg 3y ago> Developers don't like to think about abstractions and naming is a known hard problem. People pick this up and now everything is easy. Every name becomes: DoThis, DoThat, DoThisOtherThing and only answers to `call`. No need to think! This 'framework' doesn't require that though: def double_number(r) = r.continue(r.value * 2) def add_one(r) = r.continue(r.value + 1) def square_number(r) = r.continue(r.value ** 2) pipeline = Pipeline.new do |pl| pl.step method(:double_number) pl.step method(:add_one) pl.step method(:square_number) end Just methods, with no need to implement call() because that is what a method/proc/lambda in ruby implements. In other languages like C# you could have an API that can take a Func<Result, Result> so that you can just pass delgates/lambdas/methods to it. And adding a bit more DSL could make the construction of the pipeline even less verbose. You can use this without going full-Java and creating a dozen (or a hundred) 5-line classes in different files.
- allknowingfrog 3y agoHave you ever noticed that everyone who drives faster than you is a maniac, and everyone who drives slower than you is a moron? I think code abstraction fits a similar pattern.
- corytheboyd 3y agoSortaaaa, it’s definitely harder to read existing code than it is to write new code, which is what you’re getting at. To bend your analogy a little further, we can all identify the person on the road who was clearly wrong, putting us all in danger. It’s the difference between someone slowing down traffic by camping in the fast late and someone else barreling down a residential street 50 mph over the limit. The former is annoying, the latter struck and killed someone. Not that code is this serious at all. Idk.
- ismaelct 3y agoI'm so going to steal this
- quesera 3y agoIt's stolen from George Carlin. Attribution would be appropriate!
- allknowingfrog 3y agoI rather doubt that George is concerned with attribution at this point, but I freely admit that I was probably misquoting somebody.
- quesera 3y agoAttribution is not only for living people. You got the quote close enough for a plagiarism complaint. :) https://www.youtube.com/watch?v=XWPCE2tTLZQ https://www.youtube.com/watch?v=XWPCE2tTLZQ
- whstl 3y ago