Linear code is more readable
blog.separateconcerns.com
blog.separateconcerns.com
In this case I hope nobody is proposing a single 1000-line god function. Nor is a maximum of 5 lines per function going to read well. So where do we split things?
This requires judgment, and yes, good taste. Also iteration. Just because the first place you tried to carve an abstraction didn’t work well, doesn’t mean you give up on abstractions; after refactoring a few times you’ll get an API that makes sense, hopefully with classes that match the business domain clearly.
But at the same time, don’t be over-eager to abstract, or mortally offended by a few lines of duplication. Premature abstraction often ends up coupling code that should not have to evolve together.
As a stylistic device, extracting a function which will only be called in one place to abstract away a unit of work can really clean up an algorithm; especially if you can hide boilerplate or prevent mixing of infra and domain concerns like business logic and DB connection handling. But again I’d recommend using this judiciously, and avoiding breaking up steps that should really be at the same level of abstraction.
Also: cooking recipes are also very abstracted. When they say you need to lightly fry onions they assume you know a way to cut onions and a lightly frying algorithm already. If they would inline everything it would become unreadable.
Code is very similar. If you want it strictly without abstractions it will be as low level as your language allows you, and that is definitely not readable code.
If you e.g. instead of using pythons "decode" method tries to do unicode decoding yourself it would become very hard to understand what your program is actually about. Now there are probably zero people who would do that, because the language provides a simple and well tested abstraction — but what makes that different from you creating your own simple and well tested abstraction and using that throughout the actual business logic of your code?
The hard part is creating abstractions that are so well chosen that nobody will have to ever touch them again.
- Linear code: The meat frying (state-producing) and deglazing (state-requiring) steps are below each other in the same recipe, so to verify that it works you can just linearly go through line by line. However if the recipe becomes long and a lot of stuff happens in between, it's no longer obvious. You'll have to use good comments ("// leave residue in the pan, we'll need it for the fond") because otherwise you might accidentally refactor in a way that violates the precondition (swaps/scrubs the pan).
- Modular code: You need to clearly describe the precondition on the fond preparation subroutine to have any chance to keep using it correctly. On one hand this forces documentation, on the other hand it's probably still easier to forget since the subroutine call ("Prepare the fond.") doesn't directly make the precondition obvious.
Either way has its advantages and drawbacks, and the right choice depends on the circumstances. This is assuming you only want to cook this specific meal and aren't writing a cookbook - otherwise you should definitely modularize to remove repetition.
This is the key. Novice devs tend to write giant functions. Zealot devs who read books like Clean Code for the first time tend to split things to a million functions, each one a few lines long (pretty sure the book itself says no more than 5 lines for each function). I worked with a guy who extracted each and every boolean condition to a function because "it's easier to read", while never writing any comments because "comments are bad" (according to the book). I hate that book, it creates these zealots that mindlessly follow its bad advices.
I think both 100 and 20 are a bit low, but much better than 5. As I mentioned in a comment a few days ago when I also corrected someone that misremembered a detail from the book, I am not a huge fan. But I also think it is mostly correct about most things, and not as terribly bad as some say. Listening to fans of the book is more annoying than to actually read the book.
(And that other comment when I corrected someone was about bad comments. Clean Code definitely does not say that you shall never comment anything.)
I haven't read the book, and I can see how people can go overboard and can turn good advice into a caricature of it, but short, well-named functions that focus on a single thing are generally better than long ones that do dozens of different things. Separate those concerns.
I follow wildly different rules of thumb about everything from line of code count to how many methods per class to whether or not that functionality belongs in a class in the first place, depending on whether I'm writing Java or Python or F# or Racket.
"Every function in this program was just two, or three, or four lines long. Each was transparently obvious. Each told a story. And each led you to the next in a compelling order. That’s how short your functions should be!"
So I think it's fair to say the book advocates for functions 2-4 lines long.
And about comments, from the book:
"So when you find yourself in a position where you need to write a comment, think it through and see whether there isn’t some way to turn the tables and express yourself in code. Every time you express yourself in code, you should pat yourself on the back. Every time you write a comment, you should grimace and feel the failure of your ability of expression"
"Some comments are necessary or beneficial. We’ll look at a few that I consider worthy of the bits they consume. Keep in mind, however, that the only truly good comment is the comment you found a way not to write."
With opinionated sentences like these, it's not hard to see how one would read the book and adopt a "no comment" mindset.
"Store user.age in age variable", is a useless comment which is indeed better expressed with clear code.
"Store user age in struct because when xyz() iterates over this it has no way to access the user object" is useful because it tells us why something is done, where it is used, and why the obvious solution isn't right.
The tech lead insisted they use XML comments (Visual Studio) for everything.
///<Summary> ///Represents the User ///</Summary> public class User { ///<Summary> ///Users Age //</Summary> public int Age {get;set;}
///<Summary>
///Users First Name
//</Summary>
public string FirstName {get;set;}
}ad nauseum.
Here's the thing. Swagger (.net) can pick up the XML file generated from these comments and it gives developers the ability to add more information to the Open API Spec file (swagger generates a UI off of it).
So it has a legitimate use, but if you don't have anything than to repeat what the damned code already says, it's harmful to the readability of the codebase.
I jokingly refer to our customers/users as "humans" at work.
This tends to wear off with exposure to the real world. Not only will you find undeniably good code that's written in flagrant disregard to the holiest of doctrines, you'll also find garbage written The Proper Way, more damning still sometimes you'll discover it was written by your own hands.
I don't think it's a coincidence many of the ideas that have the most fervent and zealous followers have names that sound righteous, if it isn't clean code it's pure functions or more recently memory safety. Clearly nobody who is on the "side" of dirty, impure and unsafe code can be right?
Since every situation is unique, even if similar to others, I think it's always best that programmers rely on their judgement and experience when writing code instead of a set of axioms.
And besides, blindly embracing a set of rules is acting more like a robot, instead of a human being.
Ahhhhh...yes, but...
There are two kinds of people when it comes to dogma: The faithful, and the preachers. The inexperienced developers are the faithful. They cling to the scripture without proof that it is actually necessary.
Insofar, I agree with your post.
But the faithful need someone to preach the faith, and those are usually not the inexperienced ones. Those are usually experienced developers. Their personal reasons to cling to the dogma are varied: Some may have started as faithful themselves, for some it's stubbornness, an unwillingness to change, maintaining a feeling of superiority, the fear of becoming obsolete, ...
So here we are in disagreement. The preacher is the product of experience and development over time. And in my book, the preachers of dogmas are more of a problem than the faithful who follow them. Because it's the preachers who write the scripture, the preachers who make up arguments why alternatives to the ideology are bad, and the preachers who seek to isolate their flock from the "evil" preditions of alternatives.
> I don't think it's a coincidence many of the ideas that have the most fervent and zealous followers have names that sound righteous, if it isn't clean code it's pure functions or more recently memory safety.
Closing my answer on another point of agreement, it is absolutely not a coincidence, that the wording of dogmas in programming sound eerily similar to that in religious teachings ;-)
Take the pizza example. Which is better, linear code or small functions? It's a series of tradeoffs. Once you get above a screen of code, or 10 ifs, functions become hard to read. Once the same logic has been written 3x, abstracting it is usually a win. Even if it is small. And there is a fuzzy area where it isn't obvious which is better, and debating it is probably a loss over writing it and moving on. Doubly so if you're defaulting to the same kind of decisions every time and so the style is consistent.
In a world full of pragmatic tradeoffs, dogmatism is rarely the right choice. (Unless you haven't learned the tradeoffs.)
dogma invites people to stop using their critical thinking skills.
One of my favorite examples of this:
Everyone would agree that having a newborn in a car means safety is paramount. Everyone also agrees that left turns are less safe than right turns. No one would agree that this implies you should never make left turns in a vehicle with a newborn.
^But the above is how both security people and TDD proponents tend to act, as if there can be no risk assessment and critical thinking involved. We've all made right hand turns when we really wanted to go left because there was just too much traffic, even without a newborn in the vehicle.
Because ideas are highly unlikely to be universally correct, no matter how good they are. Even if an idea is supported by all available evidence, it MUST subject itself to scrutiny and possible falsification, all the time.
A dogma flies in the face of that. It is, by definition "any belief held unquestioningly and with undefended certainty" (quote from wikipedia). Once people follow an idea dogmatically, they are very likely to apply that idea no matter if it makes sense or not. They stop following logic and start following scripture.
It is bad enough if this happens in science, where we really do have systems so far supported by all available evidence. But it becomes a lot worse in areas like software development, where we know there isn't "the one" true way of doing things.
And to head off one likely reply to this: Yes, I apply this logic also to the assumption that "dogmas are always bad". If someone could present proof showcasing a dogma that only has good outcomes, with no negatives attached, then I am willing to change my mind on this.
Obviously, readability is important, but I've also seen things like this so often in my career where it's used as an excuse for anything. Most recently, trying to stop a teammate turning nearly every class into a singleton for the sake of "simplicity" and "readability", which I thought was a real stretch.
I don't regret the learning, but I do regret being dogmatic. It was interesting that no one around me knew better either way, or felt they could provide reasonable mentorship, so we went too far with it. These days I write the pizza function on the left, and use comments sparingly where they add context and reasoning.
I always ask people to think about printed pages and they look at me as if I'm crazy... But it's like, if you have to pick up a reference book or something, carefully find the right section addressing your problem, you want to read it, how many pages do you want to read at a sitting? For most the answer is ideally 1 but you can read 2-3 and still not get annoyed, right? If it gets longer than 10 then that's doable but not what you signed up for. Well if I print code onto those pages, and just assume that most of English lines are kind of filler which programming languages don't have, so no compression due to the sparseness of code, then you get ~40 LOC on a textbook page, ideally you would solve a problem in 40 LOC but if it took 120 LOC that is still perfectly readable, it's when it gets to 400+ish that something has really started to get confusing about the structure.
Same with diffs, 400-line diff is still reviewable, but barely.
The printed page isn't the point, the point is that these are kind of objective numbers, if I describe them in printed pages everybody seems to agree on these numbers... But then a book like Clean Code comes around, people want to have these tiny little scraps of an eighth of a page, bound together in a little flip book of half-index-card strips each pointing at other strips, “bake the pizza (see strip 37)”, and nobody thinks about whether this is actually an informational presentation mode that anybody really wants to use. “It works better for review time because you encourage only reviewing one or two pages of flip book at a time.” Yeah Bob I see what you're saying but, like, is this my “crazy uncle” now who insists that the usual book is going away because with the advent of Wikipedia and infinite content feeds all knowledge and story will forever be stored in such flipbooks? Just because editors who don't care about the overall story anymore because their attention span are shot to hell find it easier to review half an index card at a time? This is a good thing? Something feels off!
You get this same argument from people who believe in the layered server architecture. “Business logic needs to go in the business layer, database logic in the data access layer, presentation logic in the view layer, routing logic in the controller layer.” but you would never voluntarily read a book that was structured this way! “Matt saw Alice sitting there, a young girl of maybe 16, gorgeous in her melancholy and disaffected way, an old schoolmate of his. He waved. She beckoned. He said “Hi, how are you?” and she replied...” Right, the author gave you a data structure of adjectives to associate with Alice, you didn't have to flip to the Characters section looking for “ALICE_INTRODUCTION” and wade through all of the different ways she appears in the book to find “when Matt first sees her she is melancholy and gorgeous” and then flip all the way back to the story that you were reading, then flip to the Dialogue section looking for MATT_ALICE_INTRO_DIALOGUE, hope you left a bookmark back in the Main Story section back there! “Oh, but it is so easy to read the whole book if you can skim through the Main Plot part of the book without ever knowing anything about the characters or settings or repercussions or dialogue, “Matt saw Alice (ALICE_INTRO), she beckoned, they talked (MATT_ALICE_INTRO_DIALOGUE), he walked to the diner...”. And if you complain about the big all caps stuff someone says “well in a modern hypertext reader, those just become links and you never need to see them directly!” except you do because you have to maintain it... And it's like, I get it! You can probably compress most modern novels considerably if you remove all their descriptions and dialogue to appendices, it's not wrong! But writing is so much slower in that format, debugging is surprisingly so much slower in that format, the things that are faster are queries like “Did Alice ever mention her father to Matt in the recorded dialogue?” and then you make a refactoring change on the basis that Matt should not know anything about Alice's father and then it turns out that it generated a plot hole because somewhere in the Exposition layer the two were connected more obliquely, Alice wrote about it in a post-it on the fridge or something, that she was going to see her father who was ill.
For example, the DAL shouldn't know that a missing record is going to return a 404, instead it needs to be able to express record not found in it's API. The business layer should also not know that a record not found is going to return a 404. Instead the business layer needs to be able to express that a record was not found. The web layer needs to know that when a record not found is expressed, we return a 404.
This is why I'm not a fan of ORM's without a DAL. Too many people will sprinkle the ORM code directly into a controller and call it a day, and then ORM's will come up with all of these unmaintainable ways to "re-use" queries and all the nasty performance knobs that come with that.
And I'm not saying the gap between the layers needs to be that thick. If a DAL wants to hand back ORM models directly, more power to them, just disconnect them from the DB before you do. If the web layer wants to use those same models as the api contract, more power to them, that can always be fixed when and if they diverge.
And it's not as if these layers themselves must be proper layers, that's what I meant when I said responsibilities. The web layer is responsible for web concerns (security, api contracts, etc). If you want to treat the controller as an orchestration mechanism that you get from 15 different Dependency Injected services, great. It doesn't need to be a physical layer, but it should be a logical layer and the layers below being able to express everything the layer(s) above need to know is an important part of that sort of design.
The truth is that what makes code readable is not really (directly!) about function size in the first place. It's about human perceptual processing and human working memory. Readable code is easily skimmable, and should strive to break the code up into well-defined contexts that allow the programmer to only have to carry a handful of pieces of information in their head at any given moment. Sometimes the best way to do that is a long, linear function. Sometimes it's a mess of small functions. Sometimes it's classes. Which option you choose ultimately needs to be responsive to the natural structure of the domain logic you're implementing.
And, frankly, I think that both versions do a pretty poor job of that, because, forget the style, the substance is a mess. They're both haphzardly newing up objects and mutating shit all over the place. This code reads to me like the end product of about four sprints' worth of rushing the code so you can get the ticket closed just in time for sprint review.
I mean, let's just think about this as if we were describing how things work in a real kitchen, since I think that's pretty much what the example is asking us to do, anyway: on what planet does a pizzeria create a new, disposable oven for every single pizza? What the heck does
pizza.Ready = box.Close()
even mean? Now we've got a box containing a pizza that's storing information about the state of the object that contains it, for some reason? Demeter is off in a corner crying somewhere. What on earth is going on with that 'if order.kind == "Veg"' business, why aren't we just listing the ingredients on the order and then iterating over that list adding the items to the pizza? The logic for figuring out which ingredients go on the pizza never belonged in this routine in the first place; it's ready aim fire not ready fire aim. etc.>I wasn’t sure if I wanted to mention this or not, but I ended up editing the post because there is something that bothers me with this function, and it is that business with the oven.
>[...]
>this code makes no sense: why would you create a whole new oven to make a pizza? In real life, you get an oven once, and then you bake a whole lot of pizzas with it, without going through the whole heating cycle.
The book was written by a Java dev who was dipping his toe into Ruby.
Go code, covered everywhere in an obnoxious rash of error handling, will be bigger.
I’ll take well-structured 1000-lines function over bad spaghetti of hundreds small functions any day.
I'm sure they exist - maybe some sort of exceedingly complicated data transform or something. But in almost every situation I've seen, a 1000 line function has countless side effects, probably sets a few globals, takes loads of poorly named arguments, each of which is a nested data structure which it reaches deeply into and often has the same for loop copied and pasted 10 times with one character changed.
Often a 1000 line function is actually 5 or 6 20 line functions. I'm sure there are legitimate exceptions, but I've never seen them.
Thats my personal opinion, and nothing more:
Something like complex one time financial/workflow/maintenance operation that includes calling dozens of different smaller functions, but very well structured.
It does not make sense to separate it more into different functions, because execution is generally linear and having to deal with tree of calls where some calls is depends on state of the previous is become cumbersome and makes reading and making changes more complex.
Again, thats my personal feeling, and nothing more.
This function is 1830 lines long. It's reasonably well structured I think. Although the #if 0 are maybe not so good.
If you changed a bunch of those to separate, pure (i.e. side-effect-free) functions it would if nothing else make unit testing a breeze, and then you'd be free to fix bugs in the logic without fear. As it is, if I had a bug in that huge function I'd be really worried about breaking some edge-condition or implied-state 500 lines up etc.
if (c == '\033')
term->print_state = 1;
else if (c == (unsigned char)'\233')
term->print_state = 2;
else if (c == '[' && term->print_state == 1)
term->print_state = 2;
else if (c == '4' && term->print_state == 2)
term->print_state = 3;
else if (c == 'i' && term->print_state == 3)
term->print_state = 4;
else
term->print_state = 0;
This could be turned into a pure function that takes c and print_state as input, and returns a new print_state which the outer function assigns. That's 12 lines turned into 1. term->print_state = newState(c, term->print_state);
(I am not a C developer so the sytax could be wrong).
Just because the outer function is impure doesn't mean it can't in turn call pure functions.Why some folks will absolutely insist that a series of manually-written case statements in a single thousand line function is the epitome of style, when there's a pure state machine model inside wishing you would free it from the shackles of if else if else if else....
There's a reason stacks are ubiquitous, they're much easier to fit in your head.
Depending on use and language, this may be "expensive" (e.g. you could be allocating for and then copying some huge data structure only to pass it back where is is simply copied over the top of the input), but this is where discretion comes in and decisions are made on what is appropriate (i.e. is performance critical, or is correctness and maintainability more important?)
Breaking out functions makes sense when you get either reuse or decoupling, but in this case you don't get any of these.
You would be able to see what it does at a high level at a glance and then drill down into the functions and sub-functions to focus on a particular part of it.
With this version, to get the high level overview you have to scroll through multiple pages of code and find the comments that say what each section does.
What happens if someone wants to use a different editor that doesn't have that feature or doesn't display it quite right anymore?
Lol.
edit: another one at 3826 with a preprocessor define interleaved.
Bonus: Uncle Bob teaches us not to use comments.
C. Muratori calls this method "semantic compression" . https://caseymuratori.com/blog_0015
Why not? Who said it's worse? What study settles the issue?
Some times a "1000-line god function" is just what the domain needs, and can be way more readable, with the logic and operations consolidated, than 20 50 line functions, that you still have to read to understand the whole thing (and which then someone will be tempted to reuse a few, adjust them for 2-3 different needs not had by your original operation, and tie parts of the functions implementing your specific logic to irrelevant to it use cases).
And if it's a pure 1000-line function, it could even be 10,000 lines for all I care, and it would still be fine.
Do you have other examples of 50+ lines functions where you thought it was the best to not separate issues?
Also, constructors with many validation steps that are compiler constrained to their local scope. That seems common.
OK fir complex computation, I left the world of mathematics 7 years ago, and I wasn't at the edge on that, I trust you, but to be clear, all your examples scream 'FSM' to me. If you have a pattern matching routine of 50+ lines that isn't a finite state machine, you're doing something wrong imho, and should consider changing abstraction (I'm not a big OO guy, but maybe use dynamic dispatch?)
Remember an import script I wrote in ExpressJs. Was like 50 lines. Did things like copy databases, clean up config etc. There were hardly any layered ifs, just steps, I didn't see much use in breaking it up, was easy to read.
Another developer, who was smart but liked abstract concepts, overenginered the hell out of it, moving it to 20 places, a bunch of provider, and I could never find & make sense out of it after that, was very hard to read was going on. Was such a pain always to update it.
Doing module mocking for unit tests instead of dependency injection in runtime code is almost always a better idea in my opinion. Dependency injection was invented for languages that can't do module mocking.
A relatively common piece of feedback from me to the team at work is usually to take a half step back and look at the larger problem domain and consider whether these things are necessarily the same, or coincidentally the same.
Just because the lines of code look similar right now doesn't mean they need to be that way or need to stay that way. Trying to mash together two disparate use cases because "the code's basically repeated" is often how you get abstractions that, especially over time, end up not actually abstracting anything.
As the various use cases get too divergent, the implementations either move much of the logic up to the caller (shallow abstractions, little value), or expose the differences via flags and end up with two very different implementations under the hood side-by-side (less clear than two independent implementations).
this made me feel a certain type of way. (dont ever look at video game source code, by the way; 1000-lines is quite short by some standards)
if a 1000-line long main is what makes sense then you should do that.
I find 1000-line long methods which are linear far easier to read than code which has every method call broken out into its own method. it's so bad I literally can't read JavaScript that is written in the contemporary style anymore. absolutely impenetrable for me.
it's true that I am not a "real" developer in that I don't work on code full-time, but I've written probably millions of lines of code in my 30-year career. I am not a novice.
if the solution calls for a 1000-line main method, then that's what I'm writing, "best practices" can go in the corner and cry. I'm writing what I need to solve the problem and nothing more.
Cyclomatic complexity: https://en.wikipedia.org/wiki/Cyclomatic_complexity
Overhead: https://en.wikipedia.org/wiki/Overhead_(computing)
Some programming language implementations and operating systems have more overhead for function calls, green threads, threads, and processes.
If each function call creates a new scope, and it's not a stackless language implementation, there's probably a hashmap/dict/object for each function call unless TCO Tail-Call Optimization has occurred.
Though, function call overhead may be less important than Readability and Maintainability
The compiler or interpreter can in some cases minimize e.g. function call overhead with a second pass or "peephole optimization".
Peephole optimization: https://em.wikipedia.org/wiki/Peephole_optimization
Code linting tools measure [McCabe,] Cyclomatic Complexity but not Algorithmic Complexity (or the overhead of O(1) lookup after data structure initialization).
For example, I have been at this for over three decades now, and there are some things that almost never fails. From the article, the kind of person who advocates for the more "testable" code with a few more lines and more abstractions, is never the same person who can maintain that codebase a handful years later.
That should tell us something. For what it's worth, I agree with the article that simpler is better, which often coincides with fewer lines of code. I personally wouldn't have chosen objects that look like "pizza.Sliced = box.SlicePizza()" but most of the time the structure is already in place and it is best to go along with it.
As to that 1000 line function, if it is in an imperative style it might well be the easiest form to read. Have you seen the Python source code? That language success owes to a simple interpreter with ginormous functions that anyone and their brother can read from top to bottom and dare modify without having the brain the size of a planet.
I think you have to consider things like reusability and unit-test-ability as well, and having all your code in a single function can make reasoning about it more difficult due to all the local variables in scope that you need to consider as possibly (maybe or maybe not) relevant to the block of code you’re reading.
That being said, when I look back on my younger, less experienced days, I often fell into the trap of over-refactoring perfectly fine linear code into something more modular, yet less maintainable due to all the jumping around. There is something to be said for leaving the code as you initially wrote it, because it is closer to how your mind was thinking at the time, and how a readers mind will also probably be interpreting the code as well. When you over-refactor, that can be lost.
So I guess in summary, this is one of those “programming is a craft” things, where experience helps you determine what is right in a situation.
Suddenly, you realize that the dozens of functions __need to be called in specific orders__, and they are each only ever used once. So really what you're doing is forcing someone to know the magic order these functions are composed in order for them to be of any use.
Oh my God.
Unfortunately organizing your code along the right lines of abstraction is something that just takes skill and can't easily be summarized in the form of "just always do this and your code will be better"
If you organize your code into units that are easy to recompose and remix, well you get huge benefits when you want recompose and remix things.
If you organize your code into units that can't be easily recomposed, then yes you've added complexity for no benefit. But why make units that can't be treated individually?
"As the complexity of the overall code grows, so would something that gets chopped into dozens of functions to the point of being unreadable."
So the answer to this is, "don't chop it into functions in a way that leaves it unreadable, instead chop it into functions in a way that leaves it more readable."
That may be unsatisfying, but it gets to the point that blindly applying rules is not always going to lead to better code. But it doesn't mean that an approach has no value.
Simply don't chop up your function until you need a slice of it somewhere else. Then refactor out the bit you need. You'll find out exactly which bits need to be replaced with variables and exactly where the slice needs to happen.
Same thing for copy pasta funcs -- the first copy is fine, the second one may be too, but after that consider extracting to a parameterized func (a permutation of the Go Proverb "A little copying is better than a little dependency.")
Really the question should always come up when there are more than say two ways to do things. If I can make a pizza from scratch, reheat a chilled pizza, create a pizza and chill it, reheat a half dozen pizzas, or make three pizzas of the same kind and chill them suddenly the useful abstractions are probably something you can figure out between those helper methods.
Honestly that is the real fear of the left way of thinking. If you add a quantity, whether to cook and whether to chill parameters you end up with a hard API where certain combinations of parameters don't make sense.
Have a clean API and make the implementation as simple as is feasible. Reuse via functions when it makes sense but don't add them willy nilly.
Aka "it is a craft and you figure things out" as someone said in the comments here
That's where nested functions show their true utility. You get short linear logic because everything is in functions, but the functions are all local scope so you get to modify local scope with them, and because the functions are all named, it is easy to determine what is going on.
What you're doing by breaking things into functions is trying to prevent it's eventual growth into a bug infested behemoth. In my experience, nearly every case where an area of a code base has become unmaintainable - it generally originates in a large, stateful piece of code that started in this fashion.
Every one who works in said area then usually has the option of either a) making it worse by adding another block to tweak it's behaviour, or b) start splitting it up and hope they don't break stuff.
I don't want to see the "how" every time I need to understand the "what". In fact, that is going to force me to parse extraneous detail, possibly for hundreds of lines, until I find the bit that actually needs to be changed.
Not every piece of code grows into a bug-infested behemoth. A lot of code doesn't grow for years. We're biased to think that every piece of code needs to "scale", but the reality is that most of it doesn't.
Instead of trying to fix issues in advance you should build a culture where issues are identified and fixed as they come up.
This piece of code will be a pain to maintain when the team gets bigger? So fix it when it actually gets bigger. Create space for engineers to talk about their pains and give them time to address those. Don't assume you know all their future pains and fix them in advance.
> In my experience, nearly every case where an area of a code base has become unmaintainable - it generally originates in a large, stateful piece of code that started in this fashion
In my experience it gets even worse with tons of prematurely-abstracted functions. Identifying and fixing large blocks of code that are hard to maintain is way easier that identifying and fixing premature abstractions. If you have to choose between the two (and you typically do), you should always choose large blocks of code.
The great thing about big blocks of code is that their flaws are so obvious. Which means they are easy to fix when the time comes. The skill every team desperately needs is identifying when the time comes, not writing code that scales from scratch (which is simply impossible).
One of the best reviewed functions I wrote at work is a 2000 line monster with 9 separate variable scopes (stages) written in a linear style. It had one purpose and one purpose only. It was supposed to convert from some individual html pages used in one corner of our app on one platform into a carousell that faked the native feel of another platform. We only needed that in one place and the whole process was incredibly specific to that platform and that corner of the app.
You could argue that every one of those 9 scopes could be a separate function, but then devs would be tempted to reuse them. Yet, each step had subtle assumptions about what happened before. The moment we would have spent effort to make them distinct functions we would have had to recheck our assumptions, generalize, verify that methods work on their own... For code that's barely ever needed elsewhere. We even had some code that was similar to some of the middle parta of the process... But just slightly didn't fit here. Changing that code caused other aspects of our software to fail.
The method was not any less debuggable, it still had end to end tests, none of the intermediate steps leaked state outside of the function. In fact 2 other devs contributed fixes over time. It worked really well. Not to mention that it was fast to write.
Linear code scales well and solves problems. You don't always want that but it sure as hell makes life easier in more contexts than you'd expect.
Note. Initial reactions to the 2000 line monster were not positive. But, spend 5 minutes with the function, and yeah... You couldn't really find practical flaws, just fears that didn't really manifest once you had a couple tests for it.
However, if you screw up linear code, you screw up locally. If you write poor small functions, the rest of the team screws up because they barely ever read the contents of your functions that call other functions that call other functions. I've had way more problems with stuff being called slightly out of order, than with large functions.
I think I’ve worked with some Python that looked and worked this way. I can’t place the details but probably in a processor pipeline running over a particularly hairy data format. Consider ancient specifications written by engineers talking on the phone encapsulated in relatively “modern” but still vintage specifications, sometimes involving screen-scraping a green screen mainframe terminal, wrapped in XML and sent over the internet. Anyway, point is I couldn’t agree more about stories.
It's like saying you need a reason not to add 4000 random jumps in your assembly code just to make it more difficult to read...
Source code tends to be very far removed from how computers execute anything, so I wouldn't use that as a justification for any sort of code style.
It is pronounced "documentation".
what we don't know is if it would have been possible to abstract those assumptions away so that functions could have been defined without them.
My longest code is always image processing. It's usually too hard to break up for the sake of breaking up. There's nothing to reuse between the calls to filters/whatever.
The fact that it works well and reads well (when it does, as in the parent's case), is.
Aside from those factors what else would be against it? Dogma?
To this end, I'd say it is important to be working in a language that avoids messing up the logic with boiler plate, or building some kind of mechanism (as dpkg did) to ease error handling and shove it out of the main flow; this is where the happy path shines: when it reads like a specification.
Isn’t that the fucking point? Having a 2000 line function is a code smell so bad, I don’t care how well the function works. It’s an automatic review fail in my book. Abstractions, closures, scope, and most importantly - docs to make sure others use your functions the way you intended them. Jesus.
Just don’t do that. Your code doesn’t have to have abstractions out the wazzo, but if your class (or method) is getting bigger than 1000 lines that’s a great sign that it’s doing too much and abstractions can be teased out. Your future self will thank you, as well as your team.
> You can't create the right abstraction until you fully understand the code, but the existence of the wrong abstraction may prevent you from ever doing so. This suggests that you should not reach for abstractions, but instead, you should resist them until they absolutely insist upon being created.
I am not saying you need to create complex abstract hiarchies right off the bat. But usually, it’s pretty easy to tease out a couple significant abstractions that are very obvious, and break down your classes by a factor of two or three. Just getting such low hanging fruit will prevent you from ever having a 2000 line long method.
And for the folks who are saying that they make sure to not add abstractions too early - are you disciplined enough to go back and add them later? I feel like if you’re the kind of engineer that busts out 2000 line methods, you’re also not going to refactor it as this method grows to 2500 or 3000 lines or beyond.
Probably most robust software you depend on is full of solid, quality abstractions. Learning to write code like this takes practice. The wrong abstraction might be wrong, but it’s one step closer on your journey to growing as an engineer. You won’t grow if you never try.
My point is the code smell isn't always accurate, and there are times and even for 2000 line monsters other devs agreed that it was the best way to hide complexity away from the rest of the codebase in that case. If we ever needed to factor things out (we never did), we could spend some effort and do it.
Some things are genuinely 2kloc-complex. Maybe not that many. Do check! But some are.
I'd like to clarify this was not necessarily 2kloc-complex, this was just 2kloc-long-and-not-really-meant-to-be-reused. It was a fairly long but linear process that was out of the ordinary for the rest of the codebase. It could easily have been split (hell, I had 9 fairly separate stages), but calling any of the intermediate stages out of order or without the context of the rest of the execution flow... would have been a foot gun for someone else. And, as time showed, we never needed those stages for anything else.
Why? Why can't the functions say "to be used by <this other function>, makes assumptions based on that function, do not use externally"? Breaking out code into a function so that the place it came from is easier to maintain... does not mandate that the code broken out needs to be "general purpose".
Good thinking. Now they’ll just add 50 flags and ten levels of nested ifs instead which is much simpler.
I read you have "end to end" tests.
One question though: Wouldn't each part benefit for having their own unit tests?
I found that I like to write tests at the level of abstraction I want to keep an implementation stable. I'd be totally fine if someone went in and changed the implementation details of that long process if needed. We cared that stuff got cleaned up at the end of the process, that the output matched certain criteria, that certain user interaction was triggered and so on... In that case it made more sense to test all our expectations for a larger scope of code, rather than "fix" the implementation details.
Tests usually "fix" expectations so they don't change from build to build. Tests don't ensure correctness, they ensure stuff doesn't alter unexpectedly.
Not necessarily better, especially since this allows for the case where individual unit tests pass fine, but the combined logic fails.
You are wrong here.
> this is one of those “programming is a craft” things, where experience helps you determine what is right in a situation.
You are right here.
The key insight on why giant linear functions are often more readable (and desirable) is because they allow you to keep more concepts/relationships simultaneously together as a single chunk without context switching which seems to aid our comprehension. An extreme proponent is Arthur Whitney (inventor of the K language) who writes very terse (almost incomprehensible to others) code so as to accommodate as much as possible in a single screen.
Two examples from my own experience;
1) I found reading/understanding/debugging a very large Windows message handler function (i.e. a WndProc with a giant switch statement containing all the business logic) far easier than the same application rewritten in Visual C++ where the message handlers were broken out into separate functions.
2) The sample code for a microcontroller showed an ADC usage example in two different ways; One with everything in the same file and another where the code was distributed across files eg. main.c/config.c/interrupts.c/timer.c/etc. Even though the LOC was <200 i found the second example hard to understand simply because of the context switch involved.
> Linear code is more readable
^ Wrong
> Linear code is sometimes more readable
^ Better
See for example Cyclomatic Complexity - https://en.wikipedia.org/wiki/Cyclomatic_complexity
I agree with this, but I think the combination of those two sentences winds up being "linear code is sometimes easier to comprehend, and sometimes not". The statement "linear code is easier to comprehend" is misleading. Your statement makes it seem like "at certain sizes" is the edge case; whereas, in my opinion, it's the only case that really matters. For a small enough block of code, "easier to comprehend" becomes a moot point.
> See for example Cyclomatic Complexity
I think that's only tangentially related. Cyclomatic Complexity deals with branching, which is somewhat orthogonal to refactoring out code to separate functions (though refactoring can make the branching easier to read, since it shows more in a smaller area).
But k has a small set of built-in commands and a built-in database; it was made for fast analysis of stock information, so with that you have everything you need and you use the same semantics. The only thing you need to know is the data structure and you can build whatever you need.
So in this way, it's very likely that, given two tables A + B and 'bunch of operations' X on A and 'bunch of operations Y' on B where Y depends on the result of X, and given the tasks to;
- create X' = X
- create XY' = X + Y
to implement XY without knowing X already exists rather than figure out X exists and reuse it.
The problem with not k (or programs written in similar style; it doesn't really matter what the programming language is), that we have learned to use the second style from the article, and, more extreme, to separate everything out in layers. You cannot even reach the data model without going through a layer (or more) of abstractions which makes it necessary not only to know the datamodel in detail but also find the matching findXinAandApplyWithYToB(). Where X & Y & A & B are often some kind of ambiguous and badly named entities. And then there is of course badly designed databases which is also quite the norm as far as we see, so there is a much lower data integrity which means that if you create something without checking all the code that touches it, that you might change something and the data becomes inconsistent.
I notice the same when working on systems built with stored procedures on MSSQL/Postgres; it is far quicker to oversee and (at least basically) understand the datamodel (even with 1000+ tables, which is rather normal for systems we work with) than it is to understand even a fraction of a, let's say Go, codebase. So when asked to do do a task XY', you are usually just not searching for X'; you are simply reading the data used in X & Y and whop up a procedure/query/whatever yourself. It's simply much faster as you have a restricted work surface; the model and sql (I know, you can use almost any language in postgres, but let's not here) and you can reason about them and the tasks at hand when you shut off internet and just use your sql workbench.
Yes, it is often just easier to write the linear code than figure out if you can reuse anything because the space is small. I think a good 'feeling' for this is, if you need internet search/package managers/copilot etc for something or if you can just write working code sitting on a desert island, quite possibly on paper. For instance, for C, asm (arm/68k/z80/8080 and older intel), k and some others I can write working code like that for non-toy applications in specific domains. And, at least for me, those languages lend themselves very well for this linear programming. Incidentally, but not related, this is for me also the most enjoyable way of programming; I kind of really hate using libraries. That's also because we work in heavy regulatory areas where you cannot just add them; you have to inspect them and sign of on them and, of course, most of them are terrible...
May we all be so lucky :-)
PS: You might want to consider adding your contact info. to your profile.
The problem with giant linear functions is that those concepts get separated by sometimes thousands of lines. Separating out the high-level concepts vs the nitty-gritty details, putting the latter in functions that then get called to implement the high-level concepts, does in my experience in most cases a better job of keeping related things together.
The issue is one of Policy vs. Mechanism - https://en.wikipedia.org/wiki/Separation_of_mechanism_and_po...
It is "Mechanism" which should be separated out and encapsulated while "Policy" (aka business logic) is what is better centralized as a linear (possibly large) function.
These pieces of code then often end up being private functions of a class. With state. Since they are private functions now, they are not really testable.
So now we got a lot of private functions that are only called once and typically modify side effect state. When these functions are grouped together with the caller, it is actually still a bit readable in simple cases.
But then after a while someone adds other functions in between the calling function and the factored out ones.
Now we have bits and pieces modifying different side effect state that no one knows if they are called from different places without getting a call graph or doing a search in the class file.
If you insist on making the code non-linear, I'd beg you to at least consider making these factored out private funcs inner funcs of the calling function if your language supports that. This makes it clear that these functions won't be called from anywhere else.
As with so many things in life, in a real codebase this is not an either/or, but an art of combining the two into something that stays readable and maintainable.
Will people bother testing all of them? Or will they write a single test, pass in a pizza and just glance at it actually working? My guess is the latter, as testing multiple branches from outside is often tedious, vs testing smaller specialized functions.
...that's basically why common sense and taste in programming is still required, it's not a purely mechanical task. That's also why I'm not entirely a fan of automatic code formatting tools, they don't understand the concept of nuance.
99% of the time, you haven't actually come up with a good abstraction, so just write some linear code. Prefer copy/pasting to dubious function semantics.
If we consider the concepts orthogonally meaning we don't consider the fact that readability can influence scalability then "everyone" is fully correct. Linear code doesn't scale as well as modular code. The dichotomy is worth knowing and worth considering depending on the situation.
That being said I STILL disagree with you. Small functions do not cause readability issues if those functions are PURE. Meaning they don't touch state. That and you don't inject logic into your code, so explicitly minimize all dependency injection and passing functions to other functions.
Form a pipeline of pure functions passing only data to other functions then it all becomes readable and scalable. You'll much more rarely hit an issue where you have to rewrite your logic because of a design flaw. More often then not by composing pure functions your code becomes like legos. Every refactoring becomes more like re-configuring and recomposing existing primitives.
Small functions increase the chances of you having to do this. Larger ones decrease it, but can cause other issues.
Also, many small functions doesn't make code modular. Having well defined, focused interfaces (I don't mean in the OO sense) for people to use makes it modular. Small functions don't necessarily harm it, but if you're not really good at organizing things they definitely can obscure it.
And I totally agree, this is how you write large code bases without making them unmaintainable.
Where to go “linear” vs “modular” is an important design choice, but it’s secondary to the design choice of where to embed state-altering features in your program tree.
I think people dislike modular code because they want to have all the “side-effects” visible in one function. Perhaps they’ve only worked in code bases where people have made poor choices in that regard.
But if you can guarantee and document things like purity, idempotency, etc, you can blissfully ignore implementation details most of the time (i.e. until performance becomes an issue), which is definitionally what allows a codebase to scale.
12 months later you have:
print_table(
rows,
headers = None,
is_unicode = False,
left_align = False,
align = [],
remove_emoji = None,
max_width = 80,
potato_mode = 7,
_debug_frontend = not FLAGS.dont_debug,
ellipsis_for = 0,
no_print = False,
)> no_print = False
love this
Is print_table() + print_table_without_emoji() better than print_table(remove_emoji= False)?
Whenever function semantics need to change, everything degrades further because of refactoring uncertainties (=> you end up with even more parameters).
This will also be extremely resistant to optimization because even finding the "happy path" is non-trivial.
`print_table() + print_table_without_emoji()`
vs
`print_table(remove_emoji= False)`
vs
`switch table_name: case emoji: print(table) case no_emoji: print(table no emoji)`
Without understanding the implementation no one can truly say which is the better approach, but this idea of "surface area for bugs" is something that should be considered when approaching these types of decisions.
if needed remove the emoji, then print. if performance/table size is an issue, working via streams/generators/etc. should be on the (heh) table anyway.
But if you have conceded to being in quick&dir^H^Hpragmatic-land anyway, IMO both can be ok depending on the context.
`prepare` is a horrible name for a function. I would expect a seasoned Gopher to call it something like `NewPizzaFromOrder`.
I don't see any reason for putting `addToppings` in its own function. If you have to have it, I personally would have made it a method on Pizza something like `func (p *Pizza) WithToppings(topping ...Topping) *Pizza { /* ... */ }`. Real pizza is mutable, so the method mutates the receiver.
Why is a new oven instantiated every time you want to bake a pizza? You should start with an oven you already have, then do `oven.Preheat()`, and then call call `oven.Bake(pizza)`. You can take this further by having `oven.Preheat()` return a newtype of Oven which exposes `.Bake()` so that you can't accidentally bake something without preheating the oven first. Maybe elsewhere `Baker` is an interface, and you have a `ToasterOven` implementation that does not require you to preheat before baking because it's just not as important.
Without changing the code, I'd also reorder the declarations to be more what you'd expect (so you don't have to jump up and down the page as you scan through functions that call each other).
IDK I have to leave now but there are just so, so many ways in which the code is already a deeply horrible example to even start picking apart the "which is more readable" debate.
Some code needs to be non-linear for reuse. Then execution is a graph. If you code does not exploit code reuse from a graph structure, do not bother introducing vertexes where a single edge suffices.
http://number-none.com/blow/blog/programming/2014/09/26/carm...
pizza.Toppings = get_pizza_toppings(order.kind)
in this case to keep the mutation of the pizza front and center in the main function here.Because what is going to happen when the code in those examples run in production over a decade is that each segment is going to change. If you’re lucky the comments will be updated as that happens, but they more than likely won’t. The unit test will also get more and more clunky as changes happen because it’s big and unwieldy, and maybe someone is going to forget to alter the part of it that wasn’t obviously tied to a change. The code will probably also become a lot less readable as time goes by, not by intend or even incompetence but mostly due to time pressure or other human things. So yes, it’s more readable, and in the perfect world you probably wouldn’t need to separate your concerns, but we live in a very imperfect world and the smaller and less responsibility you give your functions the easier it’ll be to deal with that imperfection as time goes on.
I obviously prefer the former for readability, correctness, and testability etc. However, in most PL changing the type of an object involves creating a new object and has a runtime cost. For hot code path, it makes sense to mutate in place, but in that case it's better to keep it all in one linear function.
Discussion: https://news.ycombinator.com/item?id=12120752
The basic tension here is between locality [0], on the one hand, and the desire to clearly show the high-level "table of contents" view on the other. Locality is more important for readable code. As the article notes, the TOC view can be made clear enough with section comments.
There is another, even more important, reason to prefer the linear code: It is much easier to navigate a codebase writ large when the "chunks" (functions / classes / whatever your language mandates) roughly correspond to business use-cases. Otherwise your search space gets too big, and you have to "reconstruct" the whole from the pieces yourself. The code's structure should do that for you.
If a bunch of "stuff" is all related to one thing (signup, or purchase, or whatever), let it be one thing in the code. It will be much easier to find and change things. Only break it down into sub-functions when re-use requires it. Don't do it solely for the sake of organization.
What about for testing? What about for reducing state you need to keep in mind? What about releasing resources? What about understanding the impact of a change? Etc.
Consider an end of day process with 10 non-reusable steps that must run in order and each step is 100 lines. Each step uses similar data to the step before it so variables are similar but not the same. You would really choose a 1000 line single function?
For "use-case" code like this with many steps, you are typically testing how things wire together, and so will either be injecting mocks to unit test, in which case it is not a problem, or wanting to integration or e2e test, in which case it is also not a problem.
If complex, purely logical computation is part of the larger function, and you can pull that part out into a pure function which can be easily unit tested without mocks, that is indeed a valid factoring which I support, and an exception to the general rule.
> What about for reducing state you need to keep in mind?
Typically not a problem because if the function corresponds to a business use-case, you and everybody else is already thinking about it as "one thing".
> What about releasing resources?
Not a problem I have ever once run into with backend programming in garbage collected languages. Obviously if you are in a different situation, YMMV.
> Consider an end of day process with 30 non-reusable steps that must run in order and each step is 100 lines.
I would use my judgement and might break it down. Again, I have never encountered such a situation in many years of programming.
You seem to be trying to find the (ime) rare exceptions as if those disprove the general rule. But in practice the "explode your holistic function unnecessarily into 10 parts" is a much more common error than taking "don't break it down" too far.
let DebugFlags = {StepOne=false, StepTwo=false, StepThree=true};
if (DebugFlags.StepOne) { ... }
if (DebugFlags.StepTwo) { ... }
if (DebugFlags.StepThree) { ... }
Your training in structured, DRY and OOP will recoil at this: More branches! Impossible. But your spec says "must run in order". It does this by design. Every resource can be tracked by reading it top to bottom, and the only way in which you can miss it is through a loop, which you can also aim to minimize usage of. The spec also says "uses similar data to the step before it". If variables are similar-not-same, enclose them in curly braces so that you get some scope guarding. The debug flags contain the information needed to generate whatever test data is necessary. They can alternately be organized as enumerated state instead of booleans: {All, TestOne, TestTwo, TestThree}.Long, bespoke linear sequences can be hairy, but the tools to deal with them are present in current production languages without atomizing the code into tiny functions. Occasionally you can find a useful pattern that does call for a new function, and do a "harvest" on the code and get its size down. But you have to be patient with it before you have a good sense of where a new parameterized function gets the right effect, and where inlining and flagging an existing one will do better.
For me the biggest reason is state. The longer the function, the wider the scope of the local variables. Any code anywhere in the function can mutate any of the variables, and it's not immediately clear what the data flow is. More functions help scopes stay small, and data flow is more explicit.
A side benefit is that "more functions" helps keep indentation down.
At the same time, I don't like functions that are too small, otherwise it's hard to find out where any actual work gets done.
> Any code anywhere in the function can mutate any of the variables
Regardless of the language I'm using, I never mutate values. Counters in loops or some other hyper-local variables (for performance) might be the inconsequential exceptions to this rule.
> More functions help scopes stay small, and data flow is more explicit.
Just write your big function with local scope sections, if needed (another local exception to the rule above). Eg, in JS:
let sectionReturnVal
{
// stuff that sets sectionReturnVal
}
or even use IIFE to return the value and then you can use a const. "A function, you're cheating!" you might say, but my goal is not to avoid a particular language construct, but to maintain locality, and avoid unnecessary names and jumping around.> A side benefit is that "more functions" helps keep indentation down.
This is important and I maintain it.
See "Align the happy path to the left" (https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea...)
It is also worth noting that solving this problem with function extraction can often be a merely aesthetic improvement. That is, you will still need to keep hold the surrounding context (if not the state) in your head when reading the function to understand the whole picture, and the extraction makes that harder.
Using early returns correctly, by contrast, can actually alleviate working memory issues, since you can dismiss everything above as "handling validation and errors". That is, even though technically, no matter what you do, you are spidering down the branches of control flow, and therefore in some very specific context, the code organization can affect how much attention you need to pay to that context.
> I don't like functions that are too small, otherwise it's hard to find out where any actual work gets done.
Precisely, just take this thinking to its logical conclusion. You can (mostly) have your cake and eat it too.
For it to stay this readable, though, you'd need to move the functions around if you change the order of the steps. And that's fine if they're private functions, called only from the table of contents. Only, nothing forces you to keep them in order, or even to think about how it reads overall.
It often happens that functions start being reused in a way that can't be linearized anymore. Sometimes people give up and sort them alphabetically, or it's just random.
But the next person to come along has to bounce back and forth, performing mental stack push/pop operations to create the same mental model which is much harder to do when you don't have any of the original context
The whole point of interfaces is that you're not supposed to care how a method is implemented, only what it does which is explained by a combination of context, naming and documentation. But a lot of devs don't understand(or care about) this, so they write code that doesn't make sense and then it doesn't matter whether they made it linear or modular. They do things like make a service class where you have to call one method to get some data and then you have to call another to get some other data and then you have to call a third method to get some data that needs to be consolidated with the other two and now what the hell is the point of your service? It exposes all the internal complexity to the outside.
You aren't supposed to force small methods, there's no point having 20 ~5-line functions that are all only called once and do super specific stuff and have to be called in the right order etc. That's not clean code, that's more like cargo cult programming. You are supposed to abstract things appropriately so that they make sense both to new and seasoned team members, are easy to reason about and hide complexity in places where the complexity makes sense.
This is not easy to do but it is possible.
That's exactly how you end up with O(N^4) code. Your job is to care.
This has nothing to do with performance. I'm explaining general rules for designing maintainable systems - it is possible to follow them and write performant code at the same time. It is also possible to break them if necessary. Though it usually isn't a problem at all, you're just reducing it to absurdity in order to make your point.
That said pretty sure the GP hasn't had a deep dive into whether say the C library or Linux kernel has some funny O(n^4) stuff happening.
There are, however, cases where code is a better explanation of "what it does" than naming and documentation. Both naming and documentation are hard and can become out-of-sync. Code is less ambiguous than natural language.
As an example I've never read the source code of any language's String implementation, but I've used them in many different languages.
I also don't like the "they can become out of sync" reasoning. That's like saying speed limits are pointless because people break them. If you change the code you update the name and comment. That's your job. I'm not saying you should document every class in your system like it's the Java standard library. A standard library doesn't change that much and its documentation is viewed by millions of devs so it makes sense to spend a lot of time documenting it.
That's the gold standard, it would be great if our code could be like that but it would be impractical given the frequency of change in most active development systems. So we find a middle ground, we focus our effort on the interfaces between subsystems. You section your codebase into subsystems so that the application's core can interact with the database without worrying about database details, or get some data from an API without worrying about whatever weird quirks the API has. You construct a subsystem around the API which handles all the API details so that your core can interact with the API without having to worry about auth or weird API quirks or the fact that the API entities have 50 properties and you only need 7 of them. You hide away all that stuff in a subsystem and then you design a nice and clean interface that the application core interacts with. If there are any implementation details that the consumer of the interface needs to know to use it, you document it.
Just try your best to make your subsystems usable without having to deep dive into them for implementation details every 5 minutes.
Only if they can't read code! Code is meant to be read as written, at least the first time; if you try to read it as executed, you are in the wrong.
If the previous developer wrote a function BakePizza, just assume that the pizza will be properly backed and move to the next line. If you start dwelling in details like oven temperature while trying to understand how to run the restaurant, you will not understand how the restaurant works, and you will forget the correct oven temperature.
My son struggled in school despite easily being smart enough for it, and one of the many people we spoke to about his needs explained to him that schools tend to teach bottom-up, whereas he was very much a top-down learner. He first needs an overview before he dives into the details, whereas others first need to grasp the details before they can assemble an overview. And schools tend to teach to the second group.
It's possible we've got something similar with programmers here.
There should be an editor toggle to inline functions temporarily.
No more bouncing.
Neither example is easily tested.
Neither support injecting the dependencies, which make mocking really difficult.
On the left, you're testing one big method with a whole bunch of conditionals, which leaves you with a whole ton of tests for that one big method.
On the right, there is a bake() method and it does oven.New(), but where does oven come from? Is it some global somewhere?
Pass in an order, assert the pizza that comes out is correct.
The entire function is a unit which can fit on my phone screen and has no external dependencies other than possibly oven, which was discussed in the article, it should probably have been passed in, aka dependency injection.
It isn't necessary with golang to do this at all, but it really helps build consistent structure throughout the entire app, so I do it.
Speaking from personal experience. I built a small golang process that ran on around 25k worker machines. It had to be bug free cause if it crashed and stopped running, it meant updating a whole lot of computers across multiple data centers, by hand.
We unit tested everything and the project worked out really well because of that.
Functions should exist at the level of concepts:
1. arr | flat | map | collect as HashMap makes sense.
2. CreateFlattenMappedHashMapFromArr does not.
boxPizza should take a CookedPizza, and BakePizza should take a RawPizza and return a CookedPizza etc.
Why would you care where the oven comes from, if the function deliver perfectly baked pizza? A unit test should test against the public interface and not be coupled to implementation details, since that will hamper refactorings.
> which make mocking really difficult.
Mocking is an antipattern anyway, and should be avoided except for nondeterministic components like current time or stateful external services.
> The code that is more easily unit testable, is the code I care about.
Author argues that his code is more readable. Sounds like you're saying that being unit-testable is more important than being readable.
> Neither example is easily tested.
Only if you're a unit testing zealot. Integration/E2E testing is easy for both.
> Neither support injecting the dependencies, which make mocking really difficult.
Mocking is not a virtue. Also, if mocking is the sole reason you're using DI, you're doing it wrong.
Come on, look in the mirror.
If you do some "real world pizza making" instead of toying, that function would be like at least 1k lines, including how you carefully shape the dough, how to handle exceptions when you tear some holes, and how you should observe and rotate in the oven by how much, how you should redo it if the roller blade just didn't cut through properly, so on and so forth. Of course it's better to have top-down overview like prepare -> bake -> box otherwise the readers will surely lose themselves in details without figuring out what is happening.
People in the game industry told me their horror story of helping designers with a Lua script that they were writing over the years. And it turned out the "Lua script" was a single file, with 100k+ lines, that bearly had several functions in it. That would be SO linear.
But nobody reads the code anymore. Why bother? You're not going to stay on a single project for long enough for the attention investment to pay off. So the common best practice at the moment is to pretend that you read the code without actually reading it. For this purpose, the green code is much much better.
If you ever start plateauing in your code skills, start digging into the code of your favorite open source project. Accept that things have been done in another way than you would for a reason and try to understand that reason.
Try joining advent of code[0], and make sure to spend half you time block on reading and understanding alternative solutions.
Not if you're in the business of writing novels. What happens if you decide to edit out a scene - do you re-read the entire book to double-check that the deleted scene wasn't referenced anywhere?
I think that applies to code. When I read something I wrote, if I'm annoyed at how it reads, I try to refactor it to be what I wanted to read, and remember to do it that way in the future.
But sometimes what the reader wants is too much work for the writer, so I don't push that effort beyond what it's worth.
The problem with the example is that it is both extremely artificial and shows a single use case. Even with the artificiality, one can easily imagine baking a calzone instead, which could reuse all the factored-out oven functions in the split version.
(The comment about pizza vs baked pizza is one about using typing to encode your logic, but is separate from the issue that your functions should do one thing.)
(ns restaurant.pizza
(:require [restaurant.oven :as oven]
[restaurant.package :as pack]))
(defn make-order [size sauce cheese kind]
{:size size
:sauce sauce
:cheese cheese
:kind kind})
(def toppings-map
{"Veg" "Veg toppings"
"Meat" "Meat toppings"})
(defn prepare [order]
(assoc order :toppings (:kind order)))
(defn bake [prepared-order]
(oven/bake prepared-order :pizza))
(defn box [baked-pizza]
(pack/box baked-pizza :pizza))
(defn pizza [order]
(-> order
prepare
bake
box))
(comment
(def order (make-order 26 "Tomato" "Mozzarella" "Meat"))
(pizza order))
It's short and overwhelmingly granular, but for the sake of illustration. Large and complex codebases sliced up this way has not alternative in terms of ease of testing and reasoning about the code.Your only other option is to freeze them and never make changes, that doesn't happen much in real-world code (though it probably should).
Trying to in-place refactor those things is an exercise in frustration. That’s part of why they grow so large, from observing their proponents in action. They don’t actually know what the functions do, only where to add a new path and repeat themselves.
The deal here is that both versions are fairly readable, written by someone with intent to make it clear what the code should be doing. As a result the two versions are just examples of two expression styles, while the focus is on showing how the transition between these styles could be done.
What's worth underscoring here is the cohesiveness of stretches of code, such that their execution could be summarized by a descriptive function name.
Often in grand god-functions the contexts are so intertwined and mixed that it is hard to see cohesiveness in stretches of code.
Thus, the refactoring is very much a tool to creating such cohesiveness and proper logical sequencing.
Scooping out the code into separate function or commenting it out is more of a style judgement. Though putting the code into a function with a descriptive name indeed enforces this sort of analysis.
Despite the code being not very visually attractive (long corridors of imperative statements reading and writing from SMBus addresses), I was always surprised how easy it was to maintain the code, and how quickly I could get back "in the zone" after not working on it for months.
There is something painfully trivial about old clunky languages that makes them somewhat easier to get back into. The cost in abstraction capabilities is obvious though. The only reason I can afford to write concise, linear, imperative code for this project is its narrow, specialized scope that most of modern programs cannot afford to limit themselves to anymore.
The internal complexity doesn't necessarily come from complex abstractions. Take for example some implementation of a tax code, i.e. code calculating taxes. There is probably gonna be a lot of interdependencies, dealing with special cases. That's your typical "business logic". This code is not inherently complex because the primitives are complex, but because there is a lot of dependencies in the calculation. That fact in itself makes it difficult to unit test.
On the other end of the spectrum, we have something like a library of functions, for example, mathematical functions. The inner workings of how to calculate, say, a gamma function, can be very complex to understand, but the surface (API) of each of the function is very small, and that makes the library itself simple and easy to unit test.
We can make an analogy with books instead of programs. On one end, you have a novel, which despite being written in a plain language, has many interdependencies of the characters interacting. You cannot "unit test" a novel by reading a single chapter, you have to read it all. You can have a summary of the novel (like the top function in exhibit B in the OP's example), but the summary of the novel is not exactly the novel, you're not really testing the novel if you read just the summary.
On the other end, there are reference works like dictionary or encyclopedia. We can unit test these easily, since each entry should stand on its own (if you want to evaluate quality of a reference work, you can pick a few entries and test that, and it's gonna be pretty representative). They are not emergently complex like a novel is, despite the fact that entries might use specialized jargon and be harder to read.
Verifying a tax code implementation is a good place to make use of property based testing.
If the answer is yes, a function gets created. I never do what I assume authors did here, find the smallest logical units code can split into and generate a bajillion functions. I'm not paid by the line of code after all.
The same reason makes me like object programming (especially inheritance, abstract functions, operator overloading). IMO with a good IDE such code is much more succinct(within the constraints of the language) and more readable, but taking it to extreme is a mistake.
The problem in the second approach is the functions aren't clean abstractions, they often hide logic&state transformations that only make sense in the calling context. So the dear reader is forced to jump back and forth between the multiple functions to understand the entire process.
And just to throw a bit of shade, I encountered this type of programming more in webdev, and especially devops communities-- than with data scientists, ml, or data engineers. ;) And also when the director of eng wanted to get their feet wet every now and then.
The comments are metadata, and where function names are tied into the code. One is going to stay up to date. The other isn't.
The prepare and addtoppings functions should be one function, prepare effectively just fills in a struct and calls add toppings, its pointless to seperare them.
The Bake function simply prepares the over for cooking, which the author mentioned should be a dependency with a method and then factors 4 lines of code into a new function for no reason. The bake and bake pizza function should be one function.
You can then keep the box function as is.
That would be both easier to maintain and easier to read.
The box function is broken too. You box the pizza and then return the pizza...but the box is logically a wrapper for the pizza. `box(pizza)` should return a boxed pizza. A box with contents=[pizza]. Maybe some sauce and pepperoncini in there too.
Plus all these functions are impure. Which isn't always bad but if you can prevent things like boxing it before baking it, you should.
And what even... this entire example is just horrendous. You box the pizza and then slice the pizza? Ready = box.Close()? Can the Close() operation fail? And then the pizza is not ready? Why not throw an error, now the caller has to check if the pizza that got returned to them is even ready...? And that fact is even more hidden on the right side. Same for Sliced and Boxed.
I'd argue the entire box.Close() method is slideware and wouldn't exist since it likely is just a return true. You can just as easily just say pizza.Ready = true. Reading this code afterwards I would think there was some stupid requirement somewhere for a pizza.Ready property so someone added it and would check a commit log to see if it can just be removed.
Decent catch there though, the box can also be a dependency that get's passed in.
“I’m gonna return a pizza because I want a pizza”
When of course, what one really wants is a pizza in a box. And the oven objection is also kind of funny. It leads to a “but computers are so fast, why can’t they build me a new oven for each pizza?”
People think they want real-world analogies, which they hope will make code easier to reuse and maintain when what they really want are deep modules with clean interfaces, for which object orientation is not necessary in the least.
That's what Boxed<Pizza> is for, but this is more costly than a Pizza directly.
Another article that touches on this idea (among others) is https://loup-vaillant.fr/articles/source-of-readability which advocates that "code that is read together should be written together" (reading it made me confused until I realized it meant "placed together"), specifically "Consider inlining functions that are used only once".
The amount of places I’ve worked at that don’t even have accessible DB schemas is mind-boggling.
Code on the right isn't good because it's non-linear. It's good because it outlains business processe clearly, making it easy to get a grasp on it, if you never baked pizza before and aren't an author of the original piece.
It's possible to write non-linear code using for eg unnecessary events or to much levels of abstraction and have the same issues for completely opposite reason.
the cooking process won't though. stuffed crust? add some stuff in the middle. square? add some stuff in the middle. deep dish? add some stuff in the middle.
iterate a while and your "one golden test" is what falls down.
Refactor when those things are needed, right now the cooking process is stick it in a warm over for x minutes.
What are you testing there?
The oven was preheated? Put in an assert, that doesn't need a test.
That it stayed in for x minutes? You assuming the builtin sleep function is broken? Don't test library code, that's not your job.
That the oven actually preheated correctly, that was discussed in the article, the oven and it's preheat method should be a dependency that gets passed in, again not needed to be tested here.
Also in your example you are testing whether an if condition was evaluated as true.
Give me an example of a stuffed crust pizza cooking process that has a unit test which cannot be checked by looking at the resulting pizza.
But it's always implementation details all the way down!
If `prepare` is not worth testing, why would `createPizza` be worth testing? `createPizza` is someone else's implementation detail.
Breaking the code up into smaller functions can reduce what you need to keep in your head, if the functions can be understood standalone just by their parameters, and if any side effects they may have on their parameters (in case of mutable objects) are straightforward enough to understand from their naming and/or comments (rather than from their implementation).
One purpose of functions is to separate interface from implementation. If for some part of the code an interface is easier to understand than the implementation, then that’s a clear case for making it a separate function.
The points of separation should therefore be the points where the least context is needed to understand the seperated-out operation.
The prepare function is the main issue: it creates the pizza and adds toppings. If the pizza had been constructed at the top of createPizza, then `addToppings` `bake` and `box` were called, it'd be strictly clearer than it is now.
Now obviously this is all from a contrived example, but I think the underlying lesson is: bad linear code is less tedious to deal with than bad non-linear code. With bad overly long linear code, at least the whole mess is in front of you. With bad non-linear code stuff is hiding stage-left, there's side effects that names are hiding, you're at the mercy of tooling for navigation, etc.
Maybe if you know what you're making is doomed to be bad code (think convoluted business logic driven by the real world), maybe prefer linear?
I do not enjoy navigating and bouncing through 100s of files to work out how something works. Where algorithms are obfuscated and there's indirection everywhere.
I enjoy reading dense code where everything is clearly linear because I do not need to context switch.
But when you need to change something, you probably prefer the many function approach.
// FIRST, Create the pizza object
...
// NEXT, Add the toppings
...
// NEXT, Heat the oven
...
By all means, move a "paragraph" into its own function if it's called more than once; but otherwise this provides a number of useful features:* The FIRST/NEXT comments serve as useful headers, making it possible to navigate the function without reading the code in detail.
* I know that no one's going to call one of the blocks from outside.
* I can see at a glance what chunks of code go together.
I've often gone back and read code I wrote five, ten, twenty, thirty years ago using this method, and found it perfectly readable.
Can the average dev keep all of the variable states & side effects of the function in your head as they read through it? Great! Linear may be a good fit.
Or does one need to jump up and down in the function to /really/ understand it? Probably time to consider abstracting it.
Not amount of linearity and abstractions, or silly comments, can make bad code readable. I see this stuff at work all the time. To my team's defense I deal with a lot of chemists and physicists who like to write their own algorithms.
In the basic case, the linear version is better until the code is duplicated. Adding constants and function aliases before the code has duplicated is generally a bad idea.
In real systems where things are spread, A, B, C, and D can be very far apart indeed. And it's totally fine! What matters is that from the starting position,let's say X, I can 'navigate' to A, B, C, or D, in an equal and speedy manner.
Plus human brains love to navigate things in a 'spatial' way like this. It's natural. Really when you think about it, the perceived loss here is not that big compared to the benefits.
However, I do believe that there is no one right answer to this argument. And the right answer is with that team who in the end have to read, write and maintain that code. The metrics that I collect with my co-workers who work on same code base as me are
* What is the cognitive load to grasp the code for members in the team?
* How easy is it to onboard a new member to this team?
* Are we able to move fast and have confidence in the code changes we make?
In my opinion, metrics like these are usually the ones most of us care about in the end.There's a lot of spaghetti code.
we found There's a strong correlation between method/function size and bugs.
There's not a lot of confidence because there's implicit mutable state and side effects all over the place.
- A linear control flow - Named Blocks with explicit, named, typed parameters and return values
I understand that one can use anonymous functions, immediately called to simulate this style.
And after half a year of halving to always step into a method (or out of it) to continue reading or debugging, this resonates with me very much.
> Given a straight-line program, we find half of our participants traced a program from the top-down line-by-line (linearly), and the other half start at the bottom and trace upward based on data dependencies (on-demand)
So it's possible that both viewpoints are correct in some sense and we should pursue languages which allow us to switch between the two viewpoints.
So much time spent mentally remembering what is in what variable based on the naming.
I often find myself adding “// e.g. foo, bar” to show example cases for some lines of code…like recedes for example. Wallaby.js is a godsend for this though.
File names, folder names/hierarchies, function names, class names are all _arbitrary_. You could randomize them all and your code would still run.
What is not arbitrary is: the call graph, and the data flow/dependecy graph.
Every line/block of code could be wrapped in a function.
And classes...your class methods are just functions with an implicit parameter of an object of a certain type...and practically, not the entire object, just the parts it that it actually uses in the function body.
So if you just focus on what your functions do, the boundaries and groupings of your code will become self-evident.
CLI, RPC, Rest, are all different methods of interfacing with the underlying core function. I too-often see folks make a "microservice rpc server" rather than "function exposed via rpc microservice".
I often run into conflict with developers who believe in the single return statement. This is flatter but irks a lot of devs:
if (!condition) {
return
}more code
return
* functional style makes it easier to split stuff since you mostly transform immutable data
In the Article IO, State (side-effects) and Data transformation is mixed in both versions. That leads to unnecessary complexity. In that case worse if it hides in sub-functions. But separate it and the right version is better. (you can answer the question about idempotency easily now)
* Comments over blocks of code are harder to keep in sync with the code below, they require more discipline from all team members
So in theory they are nice, but you will never have them unless you enforce them through proper code reviews.
Because of the fact that, keeping comments over blocks of code in sync with the code below requires the same exact amount of effort (arguably less) as keeping function names (and ideally, documentation) in sync with the code inside..
Except that, if the second one is not done, that is a lot more dangerous than the first- exactly because, every time you define a function, you are declaring an abstraction, and if the abstraction changes silently, that's where the real mess begins.
That's what ends up happening in my experience. (See also sibling other comments about the proliferation of flags in a function signature)
(Needless to say, I strongly resonated with the OP as I also love linear code with comments, but in the end it's also a matter of taste..)
There are certainly cases where linear makes sense. But beyond the length of your ticker tape memory is about it, and since that varies quite a lot from person to person, I like it best if I can choose the level of abstraction with which to read code. This has never been a problem.
Where indirection becomes a problem is inheritance and black box operations (as you might find in rails-y) frameworks. Django’s block model of extension for templates is so devious it should be considered felonious.
If coding for an employer, it's their business. But for use in collaborative public projects, I want a linter with experimentally measured effect on readability. Not stories from the field.
This is the same as someone arguing for scrolls when books with table of contents and appendices are far superior.
See my other comment here: https://news.ycombinator.com/item?id=37518275
>” From time to time, a complex algorithm will lead to a longer routine, and in those circumstances, the routine should be allowed to grow organically up to 100-200 lines. (A line is a noncomment, nonblank line of source code.) Decades of evidence say that routines of such length are no more error prone than shorter routines. Let issues such as depth of nesting, number of variables, and other complexity-related considerations dictate the length of the routine rather than imposing a length restriction per se.
If you want to write routines longer than about 200 lines, be careful. None of the studies that reported decreased cost, decreased error rates, or both with larger routines distinguished among sizes larger than 200 lines, and you’re bound to run into an upper limit of understandability as you pass 200 lines of code.”
[0] https://books.google.co.in/books?id=LpVCAwAAQBAJ&pg=PA174
Instead of repeating myself, i point you to my other comments in this thread for details.
Deeply nested code, especially with many functions that are called once, is really horrible to debug.
Extract functions when you see obvious repetition, not just to appease some dogmatic abstraction goal. Incidentally, this also helps the CPU (cache locality).
Along the same lines, I'd rather have a directory with several dozen source files than several dozen nested directories that may contain only one or two files each.
Big advantage of the right-hand style: the various steps are laid out in a simple 5-line function. You immediately see what making a pizza involves. Want to know more about it (like whether baking involves the creation of a completely new oven), you can zoom in on the details, but you never have to look at details that are irrelevant to you, unlike on the left side, where you have to dig through a page of code to figure out which part is relevant to you.
Mind you, there are a lot of ways in which the right hand style could go wrong: if you don't separate your concerns, and have global or member variables manipulated by different functions in ways that are not immediately obvious, then superficially clean code could be hiding some terrible spaghetti. But at least the right-hand style punishes you for that and encourages you to do better (in fact, I'm currently refactoring a bit of code that did exactly that). The left hand side would allow terribly messy code with complex interactions between different parts of the code without making it obvious that those interactions are there, and will make it more intimidating to refactor them. Small functions are easier to test and easier to refactor.
Pretending that a comment header is the same as a function is a bit silly. We can navigate to functions, not to comments.
one can rationalize all sorts, but certain real-life metaphors don't have to map closely to the digital realm.
if my CreatePizza function relies on remote/dynamic code/features (realtime functionality), then it's simpler and possibly safer to re-create than re-use. Depends on the use-case.
code
code
uses (a, b, c, d) { // Step 5: Foo the bar
code
code
}
more code
more code
It's a block that defines the variables it uses, with no other access to the outer scope. It would help break up a linear function into blocks with clearer dependencies.The caller might not care about the oven, does not know the processes needs a oven, or does not even have a oven.
The injection pattern could be used.
As soon as you try and write a function that actually uses the data, you find out that every book has been written for CRUD application programmers.
function myFunction() {
/************************************
* Subsection 1
************************************/
// code
/************************************
* Subsection 2
************************************/
// code
/************************************
* Subsection 3
************************************/
// code
}
better than splitting into multiple function calls with a bunch of variable-to-parameter and return-type to variable renaming going on. Helps if your language allows you to limit some variable scopes, but usually I wouldn't botherIt's not possible to bake in a cold oven. Why does your type allow it then? Why don't you encode the state directly?
oven := ColdOven.heat()
bakedPizza := oven.bake(pizza)That is surely about state and mutable data, not code structure. And factored code makes it _easier_ to write more stateless code.
Arguing about the difference between:
number := prepareZero
addOne(number)
addTwo(number)
addThree(number)
return number
and number = 0
number += 1
number += 2
number += 3
return number
When either of the following would be far better: addThree (addTwo (addOne prepareZero))
or return 0 + 1 + 2 + 3;there is even an academic book (normalized systems theory) which claims that having more concerns in one function, will inevitably cause an explosion of your codebase where you have to write a ton of code for a very small change. i doubt the validity of the proof they provide for this, but its something to keep in mind as i have not seen anyone more serious than I claim that it is wrong.
This approach requires one to keep the state in mind while manually "evaluating" the mutations along the way, forming a picture, or whatever one uses, in mind about the created artifact.
Especially when there's mixing of in-place mutation and return-values.
The first function in the green: func createPizza(order) { pizza = prepare(order) bake (pizza) box (pizza) return pizza }
`prepare` transforms an order into a pizza, but `bake` and `box` mutates one in-place.
Edit to add: and in 6 months, instead of being one short page of code, it'll be 600 lines long and impossible to understand or modify safely
Yes, maybe it's the wrong abstraction or it won't match in 6 months because people will start calling Hats Pizzas and the logic will be different. Maybe I'll be dead tomorrow. I don't concern myself with what-ifs over unknowns.
I want correctness and some assurance of it. I don't think writing 50? tests to cover this thing is a good way to spend my time (or someone else's). I'll write the left hand side first. When I'm looking at writing tests, it will become something like the right hand side.
Yes linear code is more readable. That's something to consider, but it's not my primary consideration.
I've used many code review tools, all suffered from that. (the only exception wasn't a tool: print out of all the code to physical paper and then we all went into a meeting room to review for several hours - this was the best type of code review for finding things wrong, but it was also extremely expensive so I've only done that once in my life)
You are putting something in there that turns out to be disinformation.
I used to believe as you did, but then I started actually looking at old code.
I stored many examples, but I'll give you one to indicate just how bad most comments are:
//Add 1 to x x:=x+2;
Now why the original author thought it was important to explain in English what the next line of code was going to do in the programming language I am not sure.
I am sure that someone figured out that it was a mistake and did the least they could possibly do to fix the mistake.
Something like 80% or more of the comments I've seen have been misleading, wrong, or useless.
If you do your own survey of someone else's old code so that your ego doesn't get in the way you will probably find something similar.
Although I did talk with people working at NASA and they had someone on their code reviews who was specifically looking at comments. If you are that disciplined then your code will not be that bad, probably.
This is to avoid the introduction of bugs by tempting someone to refactor or "fix" your code.
A properly written code, with good naming conventions for parameters, variables and functions should be easy to understand without comments.
I should not need anyone to tell me the next few lines will
//Heat oven
That should be self explanatory by just looking at the code itself.
First, you code the component you want with hardcoded styles and data. Then, you extract that to a function. Finally, you pull out the harcoded styles and data into parameters for that functoin. Now you have a reusable component.
You're coding for a future that might not exist. You might be coding for the wrong future and you painted yourself into a corner.
Been there, done that.
Maybe there's a way where I can code for every possible future with minimal effort. I'm talking about a pattern that isn't a form of premature optimization. Just a rule.
Your way of coding is, coding for the most probable future. Distinctly different of coding for every possible future.
> Ah, the good old premature abstraction.
The function will get called from different places. Once from its caller, and a second time from its unit test.
So you entire premise is wrong.
pizza.Toppings = getToppings(kind string)
That's a side effect.
If the naming and architecture are good, then it reads like a book.
It's not extensible, It's not composable, It's hard to test, and frankly, it's complicated and complex for no other reason than a particular type of engineer thinks its easier to read, because they feel all programming should be imperative.
Don't get me wrong, there's a time and a place for this style of programming. (Manual memory management, algorithmic design that maximizes speed or memory usage, or even taking a bunch of services and dictating the order in which they are supposed to be executed.)
For business logic, especially the type thats supposed to model "the world," this is terrible code.
How is the bottom, not better code?
type Oven struct {
Temp int
}
type Box struct {
// Box properties
}
type Pizza struct {
Base string
Sauce string
Cheese string
Toppings []string
Baked bool
Boxed bool
Sliced bool
Ready bool
}
type Order struct {
Size string
Sauce string
Kind string
}
func preparePizza(order *Order) *Pizza {
toppings := map[string][]string{
"Veg": []string{"Tomato", "Bell Pepper"},
"Meat": []string{"Pepperoni", "Sausage"},
}
return &Pizza{
Base: order.Size,
Sauce: order.Sauce,
Cheese: "Mozzarella",
Toppings: toppings[order.Kind],
}
}
func bakePizza(oven *Oven, pizza *Pizza, cookingTemp int, checkOvenInterval int) *Pizza {
// Simulate oven heating
for oven.Temp < cookingTemp {
time.Sleep(time.Duration(checkOvenInterval) * time.Millisecond)
oven.Temp += 10 // Simulate oven heating
}
pizza.Baked = true
return pizza
}
func boxPizza(pizza *Pizza, order *Order) *Pizza {
box := &Box{}
pizza.Boxed = true // Simulate putting pizza in box
pizza.Sliced = true // Simulate slicing pizza
pizza.Ready = true // Simulate closing box
return pizza
}
// I just need to really understand this imperative part
// this is the meat and potatoes
func createPizza(order *Order, oven *Oven, cookingTemp int, checkOvenInterval int) *Pizza {
pizza := preparePizza(order)
pizza = bakePizza(oven, pizza, cookingTemp, checkOvenInterval)
pizza = boxPizza(pizza, order)
return pizza
} defmodule Pizza do
import Pizza.Pizza, only: [new: 0]
import Pizza.Order, only: [new: 0]
def create_pizza(order, oven_temp) do
order
|> prepare_pizza()
|> bake_pizza(oven_temp)
|> box_pizza()
end
defp prepare_pizza(%Pizza.Order{size: size, sauce: sauce, kind: kind}) do
toppings =
case kind do
"Veg" -> ["Tomato", "Bell Pepper"]
"Meat" -> ["Pepperoni", "Sausage"]
_ -> []
end
%Pizza.Pizza{base: size, sauce: sauce, toppings: toppings}
end
defp bake_pizza(oven_temp, %Pizza.Pizza{baked: _baked} = pizza) when oven_temp >= 400 do
%Pizza.Pizza{pizza | baked: true}
end
defp bake_pizza(_, pizza), do: pizza
defp box_pizza(%Pizza.Pizza{boxed: _boxed, sliced: _sliced, ready: _ready} = pizza) do
%Pizza.Pizza{pizza | boxed: true, sliced: true, ready: true}
end
endHuh, I can say the same about you by the reaction to this post.