This isn't the main thrust of Sandi Metz's famous "All The Little Things" talk, but it's front-and-center in the approach she takes to cleaning up the code. Half the talk - basically all the parts where she keeps mentioning, "You'll notice our complexity metrics are still getting worse even though the code is getting better" - largely consists of getting the way the software models the business domain whipped into shape.
Or, to go full analogy, no number of trips to The Container Store will leave your house feeling clean and organized if half your possessions are junk that you don't need and the furniture arrangement is awful.
I think it's consensual by now (at least 'round these parts) that we've gone too far with clean and patterns and being too clever for your own sake (or that of your company) and even agile (once management took the concept over and changed it to its very opposite, kinda like democracy nowadays versus the Ancient Athenian spirit if you think about it). It's all pop culture by now, see that Silicon Valley show.
The most tragic, in a sense, is that the people who created those concepts, who formalized them (extreme programming, uncle Bob, etc) very clearly stated that these are not "hard rules" and cannot work as such, ever — like we say "it's a 6" or "it's a 2" by instinct, there's no science there, no genius, just practical methods.
The minute it became some sort of KPI the whole essence turns sour, possibly detrimental to the mission.
When you approach things this way, with this prior or meta-knowledge so to speak, you can very much apply "clean code" principles to an otherwise very domain-driven model and software architecture; in fact one could argue you're making a true mathematical model — which is really not about people and compile times but about exactitude, precision, an as-honest-as-can-be mapping of a real phenomenon. Had we built physics like we approach "great code" nowadays, we'd have much prettier formulas that approximate truth in abstraction but never quite reach it. A treat for theoreticians, a nightmare for people with real-world, industrial problems.
Indeed. Keeping code "clean" is more about how it represents the concepts and relationships it is dealing with and less about whether it follows any specific style rules. In my experience, code that is easy to understand and maintain often has a relatively simple design that stays close to its problem domain as much as possible.
Of course, good style is still important, and being able to write tidy code when you have to move away from the real world concepts and get into the implementation details is also important. However, without some overall structure that reflects what the software really does and how it's used, it's easy to lose focus and concentrate too much on the supporting cast at the expense of the stars of the show.
I really disagree. To take a prime example, short functions are almost always better than long functions. Regardless of the content, codebases that are refactored into smaller, more self-contained functions almost always improve in reliability, performance, readability, and extensibility.
I think the reason for this largely comes down to reducing the surface area of interactions. A 100 line function can introduce subtle interactions between the first and 99th line. Whereas a three line function, the graph of potential dependencies is much smaller.
A similar logic extends to smaller, more self-contained code blocks, classes, modules and even applications.
Interesting. And what happened with the other 97 lines of code? A fair comparison would be a 100 line function and maybe 20 functions of 5 lines and maybe 20 additional lines for function signatures plus maybe 20 lines coordinating the 20 functions. We also then have introduced 20 function names.
This one is so very disputed, though.
My own sense is that it's not the length of the function, it's the length of the loops and the complexity of the branching. Short functions help when they prevent those from occurring. But, in cases where neither is present, I haven't seen a refactoring to short functions that I thought was an unambiguous improvement. Even that variable from line 1 influencing line 99 doesn't end up being particularly difficult to see or understand as long as the control flow is linear.
The more lines of code that exist in a block, even with linear control flow, the exponentially higher chance that one line inadvertently clobbers something that subsequent line depends on. Consider a function like
def foo (msg: str):
head_index = find_header(msg)
... # 50 lines of code
log(msg, head_index)
... # 50 lines of code
return msg[head_index]
Now say somebody adds some chunk of logic in the middle def foo (msg: str):
head_index = find_header(msg)
...
head_index += offset_digest(...) # Needed for log formatting
log(msg, head_index)
...
return msg[head_index]
A seemingly innocuous change to the function, has inadvertently clobbered the value of a variable we're relying on in a slightly different context. With 50 lines of code separating each reference to the variable, it's very easy for this bug to fly under the radar unnoticed.An engineering culture that emphasized small functions avoids this problem, because the change would instead move the logic to its own self-contained function. That avoids the problem of new code accidentally clobbering the old code.
def foo (msg: str):
head_index = find_header(msg)
...
log_offset(msg, head_index)
... # 50 lines of code
return msg[head_index]
def log_offset (msg, head_index):
head_index += offset_digest(...)
log(msg, head_index)If a variable is declared at the beginning of a block, its use has to be audited throughout the entire block. It's a lot easier to confirm that a variable is never mutated across 4 lines than it is across 200.
When the size of a code block exceeds the capacity of working memory (about 8 chunks), then devs will skip the cognitive effort and fall back to the most permissive types by default. Not always, but bugs mostly come from the codebase at its worse, not its best.
Considering your point from the other direction, the most mutability disciplined codebases tend to naturally bias towards short functions. In my experience most long functions are long, because the locally-scoped variables are being used to haphazardly track statefulness across the control flow. To contrast, Haskell's immutability and purity strongly restricts that pattern. Unsurprisingly, one of the most striking features of Haskell codebases are how short the functions tend to be compared to something like Java.
Well, if the variable is declared at the beginning of the block and is scoped to the entire block, we have two possibilities.
One is that the variable really is relevant throughout the block, or close to it. In that case, full mutability makes it more difficult to understand the behaviour, but if the variable is immutable or can only be mutated using explicitly controlled tools then the audit you mention is trivial.
The other is that the variable is only relevant to some relatively small part of the block. In that case, it is possible that the function is trying to do more than one thing and that part of it could beneficially be separated into its own function, taking any variables that are only relevant to that part with it. It is also possible that the variable is being declared prematurely and starting its lifetime earlier than necessary, in which case it may be better to move the declaration later so its lifetime is shorter and the area where it is in scope is reduced.
The latter cases do tend to lead to shorter functions, or at least reduced scopes, and for good reasons.
However, what you can't do is magically take a mutable variable that is genuinely relevant throughout some algorithm that really is complicated enough to require 200 lines of code to describe it, say the current state of some fundamentally complicated state machine, and make that complexity go away just by factoring out smaller functions. You're still going to have to pass the variable or some equivalent around, and you're still going to need to mutate the state in lots of places. But now your logic for doing so is scattered across 50 4-line functions, and to establish the true behaviour you don't just have to audit one of them, you have to audit them all and all the code that connects them.
It's even easier when the language you're using doesn't let you mutate or rebind in the first place. You mention Haskell codebases having short functions, but that isn't an automatic given. Haskell and other ML languages are quite friendly to long functions specifically because the language guarantees no mutation, among many other reasons.
In fact, the Elm project explicitly tells Elm developers to not worry about the length of functions because of this property. You can't accidentally mutate or change something and not notice, because you simply aren't allowed to do that.
This is a popular claim in some circles, but I don't think it stands up to scrutiny.
As others have observed, while you may gain locally through having simpler individual functions, you may also lose globally because you now need to manage the complexity of how all those functions relate.
There seems to be very little evidence to support the argument that shorter functions are objectively better, and if anything the evidence we do have suggests that extremely short functions are objectively worse in terms of things like defect density.
I would rather argue that functions should do one thing well. This will tend to correlate with shorter functions rather than 100+ line behemoths, but the important thing is that a function represent a single, coherent idea. Maybe that idea can be expressed as some neat one-liner in a functional programming language. Maybe it's some 200+ line ordered sequence of conditions that systematically determines how to shift between states in a state machine that is modelling a fundamentally complicated situation.
only if you can also divide and conquer the amount of persons that will dev, because now when there was one name to learn and assign meaning to, there are ten different names.
I am speaking as someone who used to have a hard limit on function length (20 lines, in Allman style :-) ) - this ended up being really harmful and creating way too complex code sometimes.
To give you an example, when using Java's stream functionality, I tend to insert every method call in the chain after stream() in a new line, for readability:
...
entityIds.stream()
.map()
.filter()
.collect();
...
When I scan the method to see if it should be broken down, the fact that this code occupies 4 lines doesn't really contribute much to my decision to refactor the method into several methods, because those 4 lines are accomplishing one thing.Then I point out that, if they just refactor to a single method that's 20 lines, they can read it from beginning to end and it makes sense.
Developers do deep work with a framework which ties them to a specific language and the accompanying ecosystem. So, what you see is the "not invented here" syndrome, and how already solved problems are tackled again in that specific ecosystem.
Many solstices ago, I was a PHP developer. If you'd ask me to split a 2.55 Gb CSV file in several CSV files as a one-off request, I would have fiddled with a PHP script, because that was my bread and butter. Probably would have taken something like https://csv.thephpleague.com/ off the shelf to get the job done.
Well, no, you can do exactly that in a single line with tail, head, split and cat.
Since I moved away from PHP, I have come to find a new, deep respect for the already existing general purpose commands which are part of the Unix spec. And reflecting on my earlier work, I realize that I could have solved some challenges a lot quicker.
Now, this is not a stab at clean code or PHP. Both are valuable tools.
Rather, a big lesson for young developers is that the cleanest code out there... is the code you didn't write at all, as you leverage what's already there.
You can't, because csv records may span multiple lines. So dicing them with standard tools may subtly break them. Unless you know your particular CSV data only has single line records.
(I take your broader point though. It's just that CSV is a bad example, because you usually wind up needing specialized tools to deal with them. It's likely that your PHP script would actually be more correct!)
I should have mentioned another lesson for junior developers: learn to understand the structure of your data and avoid making assumptions about formatting.
To GP: also - CSV headers. Still possible in one line of shell, but it is starting to get unwieldy.
In any case, I think the core issue is really how much faith you have in the proficiency of your teammates. If you can't trust the developers in your team to actually only write functions that do what they say in the name (without nasty side-effects), then I can see why not having all the logic in front of you in one giant function can be annoying - you have to jump into all the definitions and manually double check to be sure what's happening.
I've been thinking about this a lot lately actually. At some level, when you create a hierarchy of classes and/or a collection of objects, you're deciding that some logic is best implemented with that set of objects and the way they talk to each other as the fundamental building block. It's very much like designing a language; you're designing the lego bricks and then putting them together to make something. The hope is of course that you're going to be able to use those lego bricks in other situations by composing them in different ways.
Where I think we can go wrong is in getting stuck on the one abstraction type; particularly in this case it's falling into a mindset where the only way to compose software is by comnposing objects. Yet at some level, you're going to be composing your logic out of function calls (or what essentially look like them). You need to know at what level you're building and viewing the system. Sure, you can build a set of objects that you can then use to build some other logic that's implemented by their composition and collaboration, but sometimes maybe all you need is a method that implements this logic more directly, using function calls and their results. From far away, it's OOP with objects collaborating via messages; from up close, it's just procedural (or even functional) code with restrictions on the state it has access to.
This mistake I think is embodied in a sentiment I used to pick up on a lot at the tail end of my CS education (around the time Java was really starting to come into fashion). It was a sentiment I'd describe as something like "OOP has superceded procedural programming". To which I'd always want to ask the question "but what about inside your objects?"
It's called imperative programming. Funny how rarely you see the word.
For this reason, it's better to prefer as little structure as you can reasonabley get away with, not more. Now, this is a nuanced view, a senior person can absolutely insist on more structure up front and avoid issues, but they'll have a good reason for doing so that doesn't involve "it's clean code".
But in general, you want as little structure as you can get away with. It's a lot easier to change something that hasn't been abstracted to death with guesses about what the future is going to hold.
I think part of the issue is a perspective thing. If I spend a day throwing something together and it sits for 6 months, great. I got ROI from that code. If 6 months in new requirements come in and I decide I need to rewrite it, who cares. It worked day in and day out for 6 months off a days worth of effort.
Too many people view code as needing to be long lived and unchanging.
There's a lot of social pressure that pushes people to view code as a thing that's either done and permanent, or not done, unfinished and therefore unacceptable.
I don't think code reuse is itself a bad thing, but it can easily be overused and cause far more confusion than benefits.
I disagree. First 20 lines in a single block means that any single line can have interact with any other. The 19th line's behavior can be subtly influenced by what's on the first line. That means it's not just an issue of reading line-by-line. You have to keep every line in the entire code block in working memory simultaneously.
The limits of human working memory is about seven or eight chunks. When functions get longer than this, they take exponentially longer to understand. 100 lines of code broken up into 25 line functions can take ten times longer to understand than 1000 lines of code broken up into well-abstracted four line functions.
I contend that one of the ways a junior developer becomes a senior developer is learning about what works and what doesn't that comes from thrashing about in a codebase. They're being paid less than the senior folks so don't worry about them thrashing about (and pair with them using an open and inquisitive mindset to help them think through the consequences of their changes).
I couldn't put this into the words as well as you did, I know myself one of the turning points of realising I had become a better developer was learning from those who taught me how to identify the design patterns and how to build up requirements.
Clean code is usually less important than a good structure, if you have bad deverlopers or a lot of juniors, your processes should be in place to not allow them to go too far off course. This is what really improves the development time and the maintenance.
The messy code is usually localised to a small area and if a refactor is required, its much smaller.
Not every situation requires the same level of verbosity and be structured in the same way, for example.
Juniors just thrash around from one design pattern to the next, never really achieving anything other than learning what doesn't work.
A much more experienced developer will have an understanding about what clean code looks and where it's necessary. They will recognise when there is a huge amount of uncertainty, for example, so spend less time writing loads of fantastic code before they've firmed up the requirements.
So it really depends on the nature of the project and who you have on your team as to whether you need to worry about 'clean code'.