As I get older my code gets a little more verbose and a little less idiomatic to the language I am writing. I’ve been writing code, starting with C, since 95. Mostly Python these days, but I try to make it clear and easy read. Mostly for myself. Future me is always happy when I take the time to comment my overarching goals for a piece of code and make it clean and well composed with enough, but not too many, functions.
> well composed with enough, but too many, functions.
In my experience, code with too many functions is more difficult to grok than spaghetti code. It's like trying to read a book with each sentence reference a different page. So, I try to code like I would write, in digestible chunks.
> As I get older my code gets a little more verbose
I've seen too many of my previous projects die right when I moved on. Now I tend to write code as if it were written by a beginner: verbose and boring, with no magic.
On the other hand, no abstractions is like reading a book where each and every thing is spelled out in outmost detail. Instead of telling you “I’m fuelling the car”, I’ll tell you: “I’m walking to the entrance hall. I’m picking up the car keys. I’m putting on my shoes. I’m putting on my jacket. I’m unlocking the front door ...”. You see where this is going. And here we already assumed that things like “putting on shoes” are provided by a standard library.
There seems to be two types of programmers: one that can read a line of code like or theCar.fuel() and trust that you in the current context understand enough of what the call does that you can continue reading on the current level of code. This type of programmers don’t mind abstractions even if a function is called in only one place.
The other type of programmer must immediately dig into the car.fuel code and make sure she understands that functionality before she can continue. And of course then each and every call becomes a misdirection from understanding the code, and of course for them it is better is everything is spelled out on the same level.
I’ve seen quite a bit of code written by the second type of programmers, and if you don’t mind scrolling and don’t mind reading the comment chapter headers (/* here we fuel the car */) instead of all the code itself, it can be reasonably readable. But there’s never comprehensive testing coverage for this kind of code, and there’s usually code for fuelling the car in four different places because programmers 2-4 didn’t have time to read all the old code to see if there was something they could reuse, and just assumed that no one had to fuel the car before since there wasn’t any car.fuel() method.
I have had the good fortune to have never worked in a codebase with the characteristics you describe. But I have seen some issues with theCar.fuel(), and that’s generally around mutability and crazy side-effects. I think most of these are pragmatically overcome by adoption of functional paradigms and function composition over inheritance or instance methods.
Still lacking good tools in our own toolbox. If ides could expand function calls inline (not a header in a glassbox, but right in code), both worlds could benefit from that. Expand all calls depth 2 and there is a detailed picture. Collapse branches at edit-time based on passed flags/literals and there is your specific path.
Hmm. There’s a vim sequence to accomplish this that you could macro. But even so, don’t most IDEs give you somewhat more than a glassbox header? I’m almost certain I’ve seen people scrolling and even editing code in the “glassbox” preview pane in VSCode.
Afair, it doesn’t inline and overlaps with the code behind it. If that is not true, it may be closer to it, but my experiments somehow failed to show its benefits over “just open to the right pane”. Maybe I should check its config thoroughly. As a vim user, I’m interested in a method you described, is it handmade :read/%v%yp-like thing or an existing plugin?
Then you have an electric car, and you use the fuel method and add a special case for isElectric handling inside. And some other dev uses lamp.fuel since it already handled isElectric internally. But later, we have to differentiate between different types of charging and battery vs constant AC and DC power. Then someone helpfully reorganizes the code and breaks car.fuel because the car does have a battery too. And then ....
No, you don’t. And the alternative implementation is that you either go through all code where car is used and add conditionals for all the cases where kind of fuel matters. Or is very common for this kind of programming, just copy the whole car.roadTrip() where fuel is called and to the method electricCar.roadTrip and just change a few lines. Then of course all requirement changes or bug fixes must be done in several places thereafter.
My feelings about people that can’t handle abstractions is that they just don’t have had to create or maintain anything complex. Very few real world systems can be kept in ones mind in full.
I agree. This whole discussion prefering long functions seems like advocacy for bad code to me.
It is just ... I have seen both types of code and if written by someone else, coffee that at least attempt to segment things into chunks that clearly don't influence each other (functions with local variablea) is massively easier to read.
I think it’s honestly just folks talking past each other because these situations are isolated judgement calls, and some folks feel that
// #1, in essence
result = a => map => reduce => transform
is easier to read and understand, while others feel that
// #2, in essence
aThing = a => map
aggregation = aThing => reduce
result => aggregation => transform
is easier to read and understand. Folks in camp #1 think camp #2 is creating too much abstraction by naming all the data each step of the way, and camp #2 thinks camp #1 is creating too much abstraction by naming all the functions each step of the way.
Really it’s just these two mental modalities butting up against each other, because you will separate your layers in different ways for increased clarity depending on which camp you fall into. What makes things clearer for camp #1 makes things less clear for camp #2, and vice versa.
That’s my suspicion anyway: the premise of the discussion is just a little off.
The one caveat is that I want to easily be able to find out what fuel() is doing. Preferably nothing like car.getService('engine').run('fueling'). Code navigation is very important, preferably doable via ctrl+f since that makes review easier. Most people just use the browser tools for reviewing code and don't actually pull the branch into their IDE.
> In my experience, code with too many functions is more difficult to grok than spaghetti code. It's like trying to read a book with each sentence reference a different page. So, I try to code like I would write, in digestible chunks.
This is so true. The worst code that I've dealt with is the code that requires jumping to a ton of different files to figure out what is going on. It's usually easier to decompose a pile of spaghetti code than to figure out how to unwrap code that has been overly abstracted.
My experience has been that spaghetti is almost always in the real world mostly overly abstract and poorly thought out abstractions. You know you get a stack trace and you end up on a journey in the debugger for 5 hours trying to find any actual concrete functionality.
Compared to someone writing inline functions that do too much, the wasted brain hours don’t even come close
It's also often very deeply nested and follows different paths based on variables that were set higher up in the code, also depending on deeply nested criteria being met. Bugs, weird states, bad error handling and resource leaks hide easily in such code.
In my experience refactoring out anything nested >3 levels immediately makes the code more readable and easier to follow - I'm talking about c++ code that I recently worked on.
Decomposing to functions and passing as const or not the required variables to functions that then do some useful work makes it clear what's mutated by the sub functions. Make the error handling policy clear and consistent.
Enforce early return and RAII vigorously to ensure that no resources (malloc,file handles,db connections, mutexes, ...) are leaked on error or an exception being thrown.
And suddenly you have a code base that's performant, reliable and comprehensible where people feel confident making changes.
I disagree. I think the central thesis of Clean Code still holds up. You should never mix layers of abstraction in a single function.
That more than anything is what kills readability, because context switching imposes a huge cognitive load. Isolating layers of abstraction almost always means small, isolated, single-purpose functions.
I think the central thesis of Clean Code still holds up. You should never mix layers of abstraction in a single function.
I agree up to a point, but I find this kind of separation a little… idealistic? I prefer the principle that any abstraction should hide significantly more complexity than it introduces.
At the level of system design, there probably are some clearly defined layers of abstraction. I’d agree that mixing those is rarely a good idea.
But at the level of individual functions, I have too often seen numerous small functions broken out for dogmatic reasons, even though they hid relatively little complexity. That coding style tends to result in low cohesion, and I think the cost of low cohesion in large programs is often underestimated and can easily outweigh the benefit of making any individual function marginally simpler. If you’re not careful, you end up trading a little reduction in complexity locally for a big increase in complexity globally.
// v1, mixing layers of abstraction
x = a if exists, else first()
y = b if exists, else second()
result = third(x,y)
// v2, abstraction
result = getResult(a,b)
In v1, we have the semantics of x and y, so we understand that a “result” is obtained through the acquisition of x and y. Whether we need to understand this is a judgement call. But v2 opens a different “failure to understand” modality: “getResult” is so blackboxed that the only thing it really accomplished is indirection, without improving readability.
I love Clean Code, but I think it sometimes prematurely favors naming a new function and the resultant indirection.
The primary motivating reason to have abstractions in the first place is to prevent context switching - i.e. you shouldn't have to think about networking code while you're writing business logic.
I’d say that’s a sign that either it’s the wrong abstraction, there’s implicit coupling (a distinct variant of the wrong abstraction), or both sides of the abstraction are in so much flux that the context switching is inevitable until one or both layers settle down.
> It's like trying to read a book with each sentence reference a different page.
Yes!!! I've been trying to teach Juniors that if the function itself has 4 levels of abstraction, even if the names are readableFunctionThatDoesXwithYSideEffect ..... it is harder to understand, Ctrl+clicking downards into each little mini-rabbit -hole. Just keep the function as a 80-liner, not a 20-liner with 4 levels of indirection w/ functions that are only used once (inside parent function) ugh.
The key concept they always helps me is to minimize side effects per function. One thing goes in, one thing comes out (in an abstract sense). Multiple side effects starts getting dodgy as it makes the function harder to predict and reason about. I do err for longer easier to read functions. And don’t compose into functions until it’s clear you will actually reuse the code or you actually need to reuse it :) DRY is good but premature composition is just as annoying as premature optimization.
No, that is not for a specific case of high performance... It's for the non-specific case of keeping the code clear, understandable, and bug-free. The style was chosen for these reasons, not because it is more performant. It just happens to also be more performant than the layers of indirection that also harm understandability.
For a procedural code base, avoid subprocedures that are only called once.
For a pure functional codebase, e.g. Haskell, locally-scoped single-use functions can be beneficial.
You still have to test all the 80 lines if they're broken down into multiple functions, so it's something that you have to evaluate on a case-by-case basis.
It might even make it harder to test: if you break a function wrong, then you might end up with a group of functions that only work together anyway.
For example: when you break a big function into 3 smaller ones. If the first acquires a resource (transaction, file) and the third releases it, then it might be simpler to test the whole group rather than each one separately.
Breaking an 80 line function into to 8x 10 line functions does not necessarily make it easier to test. Most of the time it just adds unit testing busy work, for no clear benefit. This becomes more clear if you imagine you wanted to test every possible input. Splitting the function in 8ths introduces roughly 8x the work, if each new function has the same number of possible input states. The math is more complicated in the general case, so you have to evaluate it on a case-by-case basis. Also, if you're trying to isolate a known bug, it might be beneficial to split the function and test each part in isolation.
Depends on the language. In general I find the way many unit tests are written to be very brittle. There is a balance here. If the 80 lines are clear and easy to understand they will likely be easy to test also. It’s very situational though. An 80 line function isn’t that bad. Check out the SQLite code base, which is extremely well tested, or the linux kernel. C code tends to push out the line count. Whereas 80 in Python is probably a bit much. Some libraries, especially GUI code tend to take a lot of lines, mostly just handling events and laying things out and there you often see big functions as well.
Perhaps we just imagine different things, but I like when code is a list of human-readable calls to functions. The implementation of these functions isn't so important to understanding the code you're reading.
This works really well as long as you use pure functions, because their impact on behaviour is clearly restricted.
"I've seen too many of my previous projects die right when I moved on. Now I tend to write code as if it were written by a beginner: verbose and boring, with no magic."
Theres nothing wrong with charming magic in your code, if it really does something special and is not just used for the sake of it - it only gets into dark magic, when you forget or are too lazy to add proper documentation in the end.
Which ... happened to me, too many times.
But otherwise very much yes. Clarity and simplicity should be always goal number one. But since simplicity is hard to reach at times and time is short, it is always about the balance.
If you are comparing reading code with reading books, then surely you have read books that have unfamiliar words that you have to lookup the definition, and then you might have to recursively lookup the unfamiliar words in the definition as well. Then when you internalized the sub-definitions, then you return to what you were reading and have a better understanding.
The difference between code and books is that programmers can freely and naturally define functions. I wonder if some people complaining about too many functions never actually learned how to read code in the first place.
Generally used with a negative connotation. C2 also discusses how the layers can become entangled/stuck with one another and difficult to replace, which seems to fit the metaphor.
For describing layered code in a non-negative fashion, just saying "layered (or "modular") seems most typical.
It's easy to say ugh, but we juniors are more than willing to learn "the right way". This is the hardest part for me. I get anxiety about it and it slows me down.
How do I apply this to taking over someone else's 4 year old Magento project? We're out here doing our best, and sometimes our learning environments are in that context.
I would say, don't stress about it too much. There is no perfect way. Everyone makes misstakes. And about when to make abstractions and when not, is mostly about experience. There are modules worth optimizing and abstracting. And others are not.
You definitely will make wrong decisions about it and later found out, this optimisation was a waste of time, or that quick and dirty approach really cost you much later on, we all did that and still do.
Much worse than making a wrong (design) decision is making no decision at all - because mostly you have to decide for something and then just go with it.
Overthinking things seldom helps. What helps me sometimes is, putting a special hard problem to the side if I am stuck and solve something easier first. Then after some time, when I get back to it, things are much more clear.
But I also wasted too much time thinking about the right approach in a neverending, neverprogressing loop to achieve perfection.
Now my question is not, is it perfect or shiny, but: Is it good enough?
What matters is, that shit gets done in a way that works.
> wasted too much time thinking about the right approach in a neverending, neverprogressing loop to achieve perfection
A CEO from my past often muttered that "perfect software comes at infinite cost". It's key, imo, to identify which components of what you are building _must_ be perfect. The rest can have warts.
"to identify which components of what you are building _must_ be perfect"
Well, but by the words of your former CEO (and my opinion) those parts would then have infinite costs, too... if they really need to be perfect.
I mean, it is awesome, when you do a big feature change and it all just runs smooth without problems, because your design was well thought out, but you cannot think of every future change to come - and when you still try, chances are you get stuck and waste your time and risk the flow of the whole project. I rather tend to think about the current needs first and the immediate future second, but everything after that, I spend not much thought anymore.
Agreed. What I mean by "perfect" is: for a given part/component/decision/etc, take the time (an always-limited resource) to learn as much as possible and contemplate more than just the seemingly obvious path forward. Take security for example. I'd rather 'waste time' now making sure I'm covering any gaps in that realm before shipping.
OTOH maybe some jacked-up abstraction/incorrect tool choice/ugly-ui/etc is something that can wait a few sprints or longer. At least you can plan when to deal with these. Security breaches tend to plan your day for itself on your behalf. :)
I am a junior developer too. Questions like this are better suited to your manager. Mine gives me constructive feedback at regular intervals, and I also reflect on my own work and look at other people's work.
> Mostly Python these days, but I try to make it clear and easy read.
Which is why I enjoy languages that let me do this without getting too hung up on performance. It's curious that you bring up Python, because idiomatic Python (especially where math libs are concerned) seems to vastly favor brevity/one-liners over all else. It's nice to hear that a veteran is favoring clarity.
> idiomatic Python (especially where math libs are concerned) seems to vastly favor brevity/one-liners over all else
I can’t speak to math libs, but in my experience with server-side development, Python devs tend to (often even religiously) cite PEP style guides favoring explicitness and verbosity. I think there may have been a shift as Python got a lot of uptake in scientific and ML communities, and I hope that hasn’t seriously impacted the rest of the Python community because, while I don’t especially love the language/environment, I deeply appreciated the consistency of valuing clear and readable code.
> cite PEP style guides favoring explicitness and verbosity.
Explicit is better than implicit, always has been, always will be. Granted, I've been writing backend/server-side Python code for 15 years now, so that might be one of the reasons.
For what it’s worth, having spent the last few years writing server-side TypeScript, I’ve evangelized “explicit is better than implicit” fairly aggressively. A lot of even seasoned TS developers are still mainly accustomed to JS interfaces, and fairly often their first instinct is to cram a lot of semantics into a single variable or config flag. I’m glad I spent a few years working with PEP-8 fanatics. It made me much better at thinking about and designing interfaces.
I'm someone who came to the server side of things from the scientific Python community. IMO, that community is still learning how to incorporate Python's best practices to cater their very specific needs.
For example, if you're writing a plotting library geared towards data scientists, you're almost forced to pick brevity over verbosity even if that means violating some of Python's core philosophies. Data scientists usually come from non-compsci backgrounds and almost 90% of the codes they write, don't go to production. So, they usually prefer tools that help them get the job done quickly and they write tools following the same philosophy.
Right. And a lot have come from other languages like R where that’s more common.
If I were building a library for something like that, I’d build the core idiomatically, then expose an idiomatic API with aliases for brevity. I’d make sure the alias is documented in the docstring, and types refer to the idiomatic names. I know TIMTOWTDI isn’t entirely “pythonic”, but it’s a small compromise for probably a good maintainability boost.
There is a point where fitting a little more code on one screen actually helps. Usually not though. Our brains can only see a screen at a time. There is some optimal mix of terseness, especially when you know your reader (probably you in a few months!) will grok it, vs verbosity. If I find myself untangling a single line down the road in my brain it was too complex. Python is already so expressive! We all find our style, but generally I know I did it right if I look back at code at think “wow that’s easy to understand” vs “hmmn, what was I thinking there?” Heh.