Skip Navigation
46 comments
  • The left side (linear) looks like the code I write while I'm trying to figure out whether I understand the problem or I'm not quite sure what all I need to do prove that I can (or cannot!) solve the problem.

    The code on the right, with all the "abstractions" looks like the code I end up with after I've got everything sorted out and want to make things easier to read, find, and maintain.

    I actually find the code on the right easier to read. I treat it like a well written technical manual. For me, understanding starts with time spent on the table of contents just to get a sense of the domain. That is followed by reading the opening and closing sections of each chapter, and finally digging into the details. I stop at any level that meets my needs, including just picking and choosing based on the table of contents if there is no clear need to understand the entire domain in order to accomplish my goal.

    I've preferred code written in that "abstracted" style since discovering the joy of GOSUB and JSR on my VIC-20.

    • Exactly, the code on the right let us read only what matters when we're trying to solve a problem or understand it. The left one makes us read the whole thing and treats code like prose instead of reference material.

      • And this is actually important when doing your job. I was reading code just yesterday written like the "left side" and it slowed me down because I was forced to understand everything that was happening in a big paragraph instead of just glossing over a function with an understandable name. These "inline functions" will often introduce temporary variables and stuff that forces the reader to understand the scope of things that really don't matter at the current level of abstraction.

  • To me, the one of the right is FAR more readable. At least if your intention is to understand what it does.

    For the left-hand one, you have to read all of it to understand its flow. For the right-hand one, just the first function is enough. You now know what it's doing, and how (roughly) it's doing it. Each subsequent function then explains a step, in detail. This allows you to focus, say, on cooking, without any understanding of the preparation phase, or the boxing phase. This vastly reduces the cognitive load, and lets me focus in on the small details.

    This same set of properties is also what makes the right hand one easier to maintain. It lets you treat parts as black boxes, you don't need to know, or care, what goes on inside. You can work on 1 with minimal references to other parts. E.g. the oven could be a simple warm up, cook, cool down cycle. However it could also be a complex, machine learning driven system, governing 100s of concurrent ovens and 10,000s of pizza for optimal cooking speed, energy use and maintenance requirements. Outside of the function, I don't need to care. It's just "Give me an oven, wait, here's your cooked pizza".

  • I get the point the author is coming from. When I was teaching first year engineering students programming, the one on the left is how everyone would write, it's simply how human intuitively think about a process.

    However, the one on the right feels more robust to me. For non trivial processes with multiple branches, it can ugly real quick if you haven't isolated functionalities into smaller functions. The issue is never when you are first writing the function, but when you're debugging or coming back to make changes.

    What if you're told the new Italian chef wants to have 15 different toppings, not just 2. He also got 3 new steps that must be done to prepare the dough before you can bake the pizza, and the heat of the oven will now depend on the different dough used. My first instinct if my code was the one on the left, would be to refactor it to make room for the new functionality. With the one on the right, the framework is already set and you can easily add new functions for preparing the dough and make a few changes to addToppings() and bake()

    If I feel too lazy to write "proper" code and just make one big function for a process, I often end up regretting it and refactoring it into smaller, more manageable functions once I get back to the project the next day. It's simply easier to wrap your head around

     heatOven() 
        
    bakePizza() 
    box()
    than reading the entire function and look for comments to mark each important step. The pizza got burned? Better take a look at `bakePizza()` then.
      
  • The author’s primary gripe, IMHO, has legs: the question about the oven’s relationship to baking is buried as part of bake() and is weird. But the solution here is not the left-hand code, but rather to port some good, old-fashioned OOP patterns: dependency injection. Pass a reference to an Oven in createPizza() and the problem is solved.

    Doing so also addresses the other concern about whether an Oven should be a singleton (yes, that’s good for a reality-oriented contrived code sample) or manufactured new for each pizza (yes, that’s good for a cloud runtime where each shard/node/core will need to factory its own Oven). The logic for cloud-oven (maybe like ghost kitchens?) or singleton-oven is settled outside of the narrative frame of createPizza(). Again, the joy of dependency injection.

    To their other point, shouldn’t the internals of preheating be enclosed in the oven’s logic…why yes that’s probably the case as well. And so, for a second time, this code seems to recommend OOP. In Sandi Metz style OOP in Ruby (or pretty much any other OOP language) this would be beautiful and rational. Heck, if the question of to preheat or no is sufficiently complex, then that logic can itself be made a class.

    As I write, I thought: “How is golang so bad at abstraction?” I’m not sure that that is the case, but as a writer of engineering education, I think the examples chosen by the Google Testing Blog don’t serve well. Real-world examples work really well with OOP languages, fast execution and “systems thinking” examples work great with golang or C. Perhaps the real problem here is that the contrived example caters to showing off the strengths of OOP, but uses a procedural/imperative-style-loving language. Perhaps the Testing Blog folks assumed that everyone was on-board with the “small factored methods approach is best” as an article of faith and could accept the modeled domain as a hand wave to whatever idea it was they were presenting.

  • Left side doesn't look too bad until you realize that most people (including your future self) reading this file later will just be interested in the first 5 lines of the right side. They won't care about all the details of how the pizza was made, and the left side has too big of a scope to quickly glance it and go to the part that matters.

    Not to mention reuse of functions and maintainability. Right side every time.

    • I'd argue you're right until you need to track down a bug in the code. Then, to the author's point, you have to jump back and forth in the code to figure out all the interdependecies between the methods, and whether a method got overridden somewhere? What else calls this method that I might break by fixing the bug? (Keep in mind this example fits on one screen - which is not usually the case.)

      • all these debugging problems are still better to solve than if all the code was in the same scope like on the left side. It's not worth exchanging possible overriding or data interdependency for a larger scope.

  • I've worked in a company that used linear code most of the time. And at first it felt really easy to read and work with. If you wanted to know what happened, just jump to the entry point, then read over the next 200 lines of code, done. No events, no jumping around between 10 different interfaces, it worked at first.

    But over time it became a total mess. A new feature gets added, now those 200 lines get another if/else at several spots, turns into 250 lines. Then a new option is added that needs to be used for several spots, 300 lines. 400 lines. 500 lines.. things just escalate.

    You can't test that function and bugs sneak in far too easily. If you want to split it up later into new functions it's going to be a major hassle. There also was no dependency injection or using interfaces, other classes were often directly called or instantiated on the spot. Code reuse was spotty and the reused functions often got 5+ parameters for different behavior.

    It was horror after a while.

    The company I work for now uses interfaces, dependency injection, unit tests, but all the way down a function might still have 50 lines tops or so. It's slightly tougher to find out where things happen, but then much easier to work with. You need a certain balance either way.

  • I sit somewhere tangential on this - I think Bret Victor's thoughts are valid here, or my interpretation of them - that we need to start revisiting our tooling. Our IDEs should be doing a lot more heavy lifting to suit our needs and reduce the amount of cognitive load that's better suited for the computer anyways. I get it's not as valid here as other use cases, but there's some room for improvements.

    Having it in separate functions is more testable and maintainable and more readable when we're thinking about control flow. Sometimes we want to look at a function and understand the nuts and bolts and sometimes we just want to know the overall flow. Why can't we swap between views and inline the functions in our IDE when we want to see the full flow? In fact, why can't we see the function inline but with the parameter variables replaced by passed values to get a feel for how the function will flow and compute what can be easily computed (assuming no global state)?

    I could be completely off base, but more and more recently - especially after years of teaching introductory programming - I'm leaning more toward the idea that our IDEs should be doubling down on taking advantage of language features, live computation, and co-operating with our coding style... and not just OOP. I'd love to hear some places that I might be overlooking. Maybe this is all a moot point, but I think code design and tooling should go hand in hand.

  • The code on the right is better (though perhaps taken to an extreme) and what it comes down to is the code on the right makes you think in terms of the layers of the function when you make a change.

    Linear functions provide very little friction to changes that (when you see the interaction through all the layers) are actually quite bad. The code on the left will -- without extreme discipline -- become spaghetti as the years pass. Even with extreme discipline as the lines of code in the function grow, and other comments are added, the actual flow of the function will be harder to see and harder to grok because it can't all be put up on the screen at the same time.

    It's ultimately the same idea as minimizing side state/side effects. You want a bunch of small pieces that can be independently verified as doing their job, rather than one large thing that you can't understand at a glance.

  • I generally prefer a mix of the two, you have a chain of linear logic that pulls out clean chunks into methods when they get two complex or need repetition/recursion etc. I would rarely have a method that is just a list of function calls.

    As with everything, there isn't a one size fits all ideal solution, it depends on the exact code.

46 comments