It's probably time to stop recommending Clean Code (2020)
qntm.org
qntm.org
Coding at scale is about managing complexity. The best code is code you don't have to read because of well named functional boundaries. Without these functional boundaries, you have to understand how every line of a function works, and then mentally model the entire graph of interactions at once, because of the potential for interactions between lines within a functional boundary. The complexity (sum total of possible interactions) grows as the number of lines within a functional boundary grows. The cognitive load to understand code grows as the number of possible interactions grow. Keeping methods short and hiding behavior behind well named functional boundaries is how you manage complexity in code.
The idea of code telling a story is that a unit of work should explain what it does through its use of well named variables, function/object names, and how data flows between function/objects. If you have to dig into the details of a function to understand what it does, you have failed to sufficiently explain what the function does through its naming and set of arguments.
This is the problem right here. I don't just read code I've written and I don't only read perfectly abstracted code. When I am stuck reading someone's code who loves the book and tries their best to follow those conventions I find it far more difficult - because I am usually reading their code to fully understand it myself (ie in a review) or to fix a bug I find it infuriating that I am jumping through dozens of files just so everything looks nice on a slide - names are great, I fully appreciate good naming but pretending that using a ton of extra files just to improve naming slightly isnt a hindrance is wild.
I will take the naming hit in return for locality. I'd like to be able to hold more than 5 lines of code in my head but leaping all over the filesystem just to see 3 line or 5 line classes that delegate to yet another class is too much.
I would extend this one level higher to say managing complexity is about managing risk. Risk is usually what we really care about.
From the article:
>any one person's opinions about another person's opinions about "clean code" are necessarily highly subjective.
At some point CS as a profession has to find the right balance of art and science. There's room for both. Codifying certain standards is the domain of professions (in the truest sense of the word) and not art.
Software often likens itself to traditional engineering disciplines. Those traditional engineering disciplines manage risk through codified standards built through industry consensus. Somebody may build a pressure system that doesn't conform to standards. They don't get to say "well your idea of 'good' is just an opinion so it's subjective". By "professional" standards they have built something outside the acceptable risk envelope and, if it's a regulated engineering domain, they can't use it.
This isn't to mean a coder would have to follow rigid rules constantly or that it needs a regulatory body, but that the practice of deviating from standardized best-practices should be communicated in terms of the risk rather than claiming it's just subjective.
Go back to earlier machines and each version had it's own assembly language and instruction set. Nobody would ever go back to that era.
OOP was pitched as a one-size-fits-all solution to all problems, and as a checklist of items that would turn a cheap offshored programmer into a real software engineer thanks to design patterns and abstractions dictated by a "Software Architect". We all know it to be false, and bordering on snake oil, but it still had some good ideas. Having a class encapsulate complexity and defining interfaces is neat. It forces to think in terms of abstractions and helps readability.
> This isn't to mean a coder would have to follow rigid rules constantly or that it needs a regulatory body, but that the practice of deviating from standardized best-practices should be communicated in terms of the risk rather than claiming it's just subjective.
As more and more years pass, I'm less and less against a regulatory body. Would help with getting rid of snake oil salesman in the industry and limit offshoring to barely qualified coders. And simplify hiring too by having a known certification that tells you someone at least meets a certain bar.
The problem I see with this is that programming could be described as a kind of general problem solving. Other engineering disciplines standardize methods that are far more specific, e.g. how to tighten screws.
It's hard to come up with specific rules for general problems though. Algorithms are just solution descriptions in a language the computer and your colleagues can understand.
When we look at specific domains, e.g. finance and accounting software, we see industry standards have already emerged, like dealing with fixed point numbers instead of floating point to make calculation errors predictable.
If we now start codifying general software engineering, I'm worried we will just codify subjective opinions about general problem solving. And that will stop any kind of improvement.
Instead we have to accept that our discipline is different from the others, and more of a design or craft discipline.
That seems like such a hard problem. Why not tackle a simpler one?
> then mentally model the entire graph of interactions at once
...partially applies even if you have well-named functional boundaries. You said it yourself:
> The complexity (sum total of possible interactions) grows as the number of lines within a functional boundary grows. The cognitive load to understand code grows as the number of possible interactions grow.
Programs have a certain essential complexity. Making a function "simpler" means making it less complex, which means that that complexity has to go somewhere else. If you make all of your functions simple, then you simply need more functions to represent the same program, which increases the total number of possible interactions between nodes and therefore the cognitive load of understanding the whole graph/program.
Allowing more complexity in your functions makes them individually harder to understand, but reduces the total number of functions needed and therefore makes the entire program more comprehensible.
Also note that just because a function's implementation is complex doesn't mean that its interface also has to be complex.
And, functions with complex implementations are only themselves difficult to understand - functions with complex interfaces make the whole system more difficult to understand.
Having hundreds or thousands of simple functions is the opposite of this advice.
You can also consider this in more scientific terms.
Code is a mental model of a set of operations. The best possible model has as few moving parts as possible, there are as few connections between the parts as possible, each part is as simple as possible, and both the parts and the connections between them are as intuitively obvious as possible.
Making parts as simple as possible is just one design goal, and not a very satisfactory or useful one in its own terms.
All of this turns out to be incredibly hard, and is a literal IQ test. Mediocre developers will always, always create overcomplicated solutions. Top developers have a magical ability to combine a 10,000 foot overview with ground level detail, and will tear through complex problems and reduce them to elegant simplicity.
IMO we should spend less time teaching algorithms and testing algorithmic specifics, and more on analysing complex systems and implementing them with minimal, elegant, intuitive models.
The semantics of the language and the structure of the code help hide irrelevant functional units from the global namespace. Methods attached to an object only need to be considered when operating on some object, for example. Private methods do not pollute the global namespace nor do they need to be present in any mental model of the application unless it is relevant to the context.
While I do think you can go too far with adding functions for its own sake, I don't see that they add to the cognitive load in the same way that possible interactions within a functional unit does. If you're just polluting a global namespace with functions and tiny objects, then that does similarly increase cognitive load and should be avoided.
Agreed
> Allowing more complexity in your functions makes them individually harder to understand
I think that that can mostly be avoided, by sometime creating local scopes {..} to avoid too much state inside a function, combined with whitespace and some section "header" comments (instead of what would have been sub function names).
Can be quite readable I think. And nice to not have to jump back and forth between myriads of files and functions
I've always felt it had a strong correlation to top-down vs bottom-up thinkers in terms of software design. The top-down folks tend to agree with your stance and the bottom-up group do not. If you're naturally going to want to understand all of the nitty gritty details you want to be able to wrap your head around those as quickly as possible. If you're willing to think in terms of the abstractions you want to remove as many of those details from sight as possible to reduce visual noise.
That's only 1 part of the complexity equation.
When you have 100 lines in 1 function you know exactly the order in which each line will happen and under which conditions by just looking at it.
If you split it into 10 functions 10-lines-long each now you have 10! possible orderings of calling these functions (ignoring loops and branches). And since this ordering is separated into multiple places - you have to keep it in your mind. Good luck inventing naming that will make obvious which of the 3628800 possible orderings is happening without reading through them.
Short functions are good when they fit the problem. Often they don't.
Sometimes there is too much code in mega-functions, poor separation of concerns and so on. These are easy mistakes to make, especially for beginners, so there are a lot of warnings against them.
Other times you have too many abstractions and too much indirection to serve any useful purpose. The ratio of named things, functional boundaries, and interface definitions to actual instructions can easily get out of hand when people dogmatically apply complexity-managing patterns to things that aren't very complex. Such over-abstraction can fall under YAGNI and waste time/$ as the code becomes slower to navigate, slower to understand in depth, and possibly slower to modify.
I think in software engineering we suffer more from the former problem than the latter problem, but the latter problem is often more frustrating because it's easier to argue for applying nifty patterns and levels of indirection than omitting them.
Just for a tangible example: If I have to iterate over a 3D data structure with an X Y and Z dimension, and use 3 nested loops to do so, is that too complex a function? I'd say no. It's at least as clear without introducing more functional boundaries, which is effort with no benefit.
Yeah, it's a lot of work, but working on recent projects have really taught me the value of good documentation. Naming a function send_records_to_database is fine, but it can't tell you how it determines which database to send the records to, or how it deals with failed records (if at all), or various alternative use cases for the function. All of that must come from documentation (or reading the source of that function).
Plus, I've found that forcing myself to write function documentation, and justify my decisions, has resulted in me putting more consideration into design. When you have to say, "this function reads <some value> name from <environmental variable>" then you have to spend some time considering if future users will find that to be a sound decision.
Code telling a story is a fallacy that programmers keep telling themselves and which fails to die. Code doesn't tell stories, programmers do. Code can't explain why it exists; it can't tell you about the buggy API it relies on and which makes its implementation weird and not straight-forward; it can't say when it's no longer needed.
Good names are important, but it's false that having well-chosen function and arguments names will tell a programmer everything they need to know.
More code, more bugs. More hidden code, more hidden bugs. There's a reason those who have worked in software development longer tend to prefer less abstraction: most of them are those who have learned from their experiences, and those who aren't are "architects" optimising for job security.
I don't know which is harder. Explaining this about code, or about tests.
The people with no sense of DevX see nothing wrong with writing tests that fail as:
Expected undefined to be "foo"
If you make me read the tests to modify your code, I'm probably going to modify the tests. Once I modify the tests, you have no idea if the new tests still cover all of the same concerns (especially if you wrote tests like the above).Make the test red before you make it green, so you know what the errors look like.
this quote scales
Secondly it is often better to start with less abstractions and boundaries, and add them when the need becomes apparent, rather than trying to remove ill conceived boundaries and abstractions that were added at earlier times.
That's fine in theory and I still sort-of believe that, but in practice, I came to believe most programming languages are insufficiently expressive for this vision to be true.
Take, as a random example, this bit of C++:
//...
const auto foo = Frobnicate(bar, Quuxify);
Ok, I know what Frobnification is. I know what Quuxify does, it's defined a few lines above. From that single line, I can guess it Frobs every member of bar via Quuxify. But is bar modified? Gotta check the signature of Frobnicate! That means either getting an IDE help popup, or finding the declaration. template<typename Stuffs, typename Fn>
auto Frobnicate(const std::vector<Stuffs>&, Fn)
-> std::vector<Stuffs>;
From the signature, I can see that bar full of Bars isn't going to be modified. But then I think, is foo.size() going to be equal to bar.size()? What if bar is empty? Can Frobnicate throw an exception? Are there any special constraints on the function Fn passed to it? Does Fn have to be a funcallable thing? Can't tell that until I pop into definition of Frobnicate.I'll omit the definition here. But now that I see it, I realize that Fn has to be a function of a very particular signature, that Fn is applied to every other element of the input vector (and not all of them, as I assumed), that the code has a bug and will crash if the input vector has less than 2 elements, and it calls three other functions that may or may not have their own restrictions on arguments, and may or may not throw an exception.
If I don't have a fully-configured IDE, I'll likely just ignore it and bear the risk. If I have, I'll routinely jump-to-definition into all these functions, quickly eye them for any potential issues... and, if I have the time, I'll put a comment on top of Frobnicate declaration, documenting everything I just learned - because holy hell, I don't want to waste my time doing the same thing next week. I would rename the function itself to include extra details, but then the name would be 100+ characters long...
Some languages are better at this than others, but my point is, until we have programming languages that can (and force you to) express the entire function contract in its signature and enforce this at compile-time, it's unsafe to assume a given function does what you think it does. Comments would be a decent workaround, if most programmers could be arsed to write them. As it is, you have to dig into the implementation of your dependencies, at least one level deep, if you want to avoid subtle bugs creeping in.
Which is often unavoidable, many functions are insufficiently explained by those alone unless you want four-word camelcase monstrosities for names. The code of the function should be right-sized. Size and complexity need to be balanced there- simpler and easier-to-follow is sometimes larger. I work on compilers, query processors and compute engines, cognitive load from the subject domains are bad enough without making the code arbitrarily shaped.
[edit] oh yes, what jzoch says below. Locality helps with taming the network of complexity between functions and data.
[edit] oh no, here come the downvotes!
This isn't always true in my experience. Often when I need to dig into the details of a function it's because how it works is more important than what it says it's doing. There are implementation concerns you can't fit into a function name.
Additionally, I have found that function names become outdated at about the same rate as comments do. If the common criticism of code commenting is that "comments are code you don't run", function names also fall into that category.
I don't have a universal rule on this, I think that managing code complexity is highly application-dependent, and dependent on the size of the team looking at the code, and dependent on the age of the code, and dependent on how fast the code is being iterated on and rewritten. However, in many cases I've started to find that it makes sense to inline certain logic, because you get rid of the risk of names going out of date just like code comments, and you remove any ambiguity over what the code actually does. There are some other benefits as well, but they're beyond the scope of the current conversation.
Perfect abstractions are relatively rare, so in instances where abstractions are likely to be very leaky (which happens more often than people suspect), it is better to be extremely transparent about what the code is doing, rather than hiding it behind a function name.
> The complexity (sum total of possible interactions) grows as the number of lines within a functional boundary grows.
I'll also push back against this line of thought. The sum total of possible interactions do not decrease when you move code out into a separate function. The same number of lines of code still get run, and each line carries the same potential to have a bug. In fact, in many cases, adding additional interfaces between components and generalizing them can increase the number of code paths and potential failure points.
If you define complexity by the sum total of possible interactions (which is itself a problematic definition, but I'll talk about that below), then complexity always increases when you factor out functions, because the interfaces, error-handling, and boilerplate code around those functions increases the number of possible interactions happening during your function call.
> The complexity (sum total of possible interactions) grows as the number of lines within a functional boundary grows.
What I've come to understand is that complexity is relative. A solution that makes a codebase less complex for one person in an organization may make a codebase more complex for someone else in the organization who has different responsibilities over the codebase.
If you are building an application with a large team, and there are clear divisions of responsibilities, then functional boundaries are very helpful because they hide the messy details about how low-level parts of the code work.
However, if you are responsible for maintaining both the high-level and low-level parts of the same codebase, than separating that logic can sometimes make the program harder to manage, because you still have to understand how both parts of the codebase work, but now you also have understand how the interfaces and abstractions between them fit together and what their limitations are.
In single-person projects where I'm the only person touching the codebase I do still use abstractions, but I often opt to limit the number of abstractions, and I inline code more often than I would in a larger project. This is because if I'm the only person working on the code, I need to be able to hold almost the entire codebase in my head at the same time in order to make informed architecture decisions, and managing a large number of abstractions on top of their implementations makes the code harder to reason about and increases the number of things I need to remember. This was a hard-learned lesson for me, but has made (I think) an observable difference in the quality and stability of the code I write.
I have witnessed more people bend over backwards and do the most insane things in the name of avoiding "Uncle Bob's" baleful stare.
It turns out that following "Uncle Sassy's" rules will get you a lot further.
1. Understand your problem fully
2. Understand your constraints fully
3. Understand not just where you are but where you are headed
4. Write code that takes the above 3 into account and make sensible decisions. When something feels wrong ... don't do it.
Quality issues are far more often planning, product management, strategic issues than something as easily remedied as the code itself.
The problem with all these lists is that they require a sense of judgement that can only be learnt from experience, never from checklists. That's why Uncle Bob's advice is simultaneously so correct, and yet so dangerous with the wrong fingers on the keyboard.
Instead of solving how I am going to solve the problem at hand ("Clean Coding"). What a fucking waste of time, my brain power, and my lifetime keystrokes[1].
I'm starting to see that OOP is more suited to programming literal business logic. The best use for the tool is when you actually have a "Person", "Customer" and "Employee" entities that have to follow some form of business rules.
In contradiction to your "Uncle Sassy's" rules, I'm starting to understand where "Uncle Beck" was coming from:
1. Make it work.
2. Make it right.
3. Make it fast.
The amount of understanding that you can garner from make something work leads very strongly into figuring out the best way to make it right. And you shouldn't be making anything fast, unless you have a profiler and other measurements telling you to do so.
"Clean Coding" just perpetuates all the broken promises of OOP.
[1]: https://www.hanselman.com/blog/do-they-deserve-the-gift-of-y...
> 2. Understand your constraints fully
These two fall under requirements gathering. It's so often forgotten that software has a specific purpose, a specific set of things it needs to do, and that it should be crafted with those in mind.
> 3. Understand not just where you are but where you are headed
And this is the part that breaks down so often. Because software is simultaneously so easy and so hard to change, people fall into traps both left and right, assuming some dimension of extensibility that never turns out to be important, or assuming something is totally constant when it is not.
I think the best advice here is that YAGNI, don't add functionality for extension unless your requirements gathering suggests you are going to need it. If you have experience building a thing, your spider senses will perk up. If you don't have experience building the thing, can you get some people on your team that do? Or at least ask them? If that is not possible, you want to prototype and fail fast. Be prepared to junk some code along the way.
If you start out not knowing any of these things, and also never junking any code along the way, what are the actual odds you got it right?
The problem is that people often need specifics to guide them when they're less experienced. Something that "feels wrong" is usually due to vast experience being incorporated into your subconscious aesthetic judgement. But you can't rely on your subconscious until you've had enough trials to hone your senses. Hard rules can and often are overapplied, but its usually better than the opposite case of someone without good judgement attempting to make unguided judgement calls.
well there goes the entire tech industry
My personal take away was that there were a few good ideas, all horribly mangled. The most painful one I remember was his treatment of the Law of Demeter, which, as I recall, was so shallow that he didn't even really even thoroughly explain what the law was trying to accomplish. (Long story short, bounded contexts don't mean much if you're allowed to ignore the boundaries.) So most everyone who read the book came to earnestly believe that the Law of Demeter is about period-to-semicolon ratios, and proceeded to convert something like
val frobnitz = Frobnitz.builder()
.withPixieDust()
.withMayonnaise()
.withTarget(worstTweetEver)
.build();
into var frobnitzBuilder = Frobnitz.builder();
frobnitzBuilder = frobnitzBuilder.withPixieDust();
frobnitzBuilder = frobnitzBuilder.withMayonnaise();
frobnitzBuilder = frobnitzBuilder.withTarget(worstTweetEver);
val frobnitz = frobnitzBuilder.build();
and somehow convince themselves that doing this was producing tangible business value, and congratulate themselves for substantially improving the long-term maintainability of the code.Meanwhile, violations of the actual Law of Demeter ran rampant. They just had more semicolons.
> Long story short, bounded contexts don't mean much if you're allowed to ignore the boundaries.
I'd like to read more. Do you know of a source that covers this properly?
const frobnitz = new Frobnitz({pixieDust: true, mayonnaise: true, target: worstTweetEver});oof. I mean, yeah, at least explain what the main thing you’re talking about is about, right? This is a pet peeve.
I don't recall where I picked up from, but the best advice I've heard on this is a "Rule of 3". You don't have a "pattern" to abstract until you reach (at least) three duplicates. ("Two is a coincidence, three is pattern. Coincidences happen all the time.") I've found it can be a useful rule of thumb to prevent "premature abstraction" (an understandable relative of "premature optimization"). It is surprising sometimes the things you find out about the abstraction only happen when you reach that third duplicate (variables or control flow decisions, for instance, that seem constants in two places for instance; or a higher level idea of why the code is duplicated that isn't clear from two very far apart points but is clearer when you can "triangulate" what their center is).
You want to extract common code if it's the same now, and will always be the same in the future. If it's not going to be the same and you extract it, you now have the pain of making it do two things, or splitting. But if it is going to be the same and you don't extract it, you have the risk of only updating one copy, and then having the other copy do the wrong thing.
For example, i have a program where one component gets data and writes it to files of a certain format in a certain directory, and another component reads those files and processes the data. The code for deciding where the directory is, and what the columns in the files are, must be the same, otherwise the programs cannot do their job. Even though there are only two uses of that code, it makes sense to extract them.
Once you think about it this way, you see that extraction also serves a documentation function. It says that the two call sites of the shared code are related to each other in some fundamental way.
Taking this approach, i might even extract code that is only used once! In my example, if the files contain dates, or other structured data, then it makes sense to have the matching formatting and parsing functions extracted and placed right next to each other, to highlight the fact that they are intimately related.
Also known as, “AbstractMagicObjectFactoryBuilderImpl” that builds exactly one (1) factory type that generates exactly (1) object type with no more than 2 options passed into the builder and 0 options passed into the factory. :-)
For example, when I did work at a microservices shop, I was deeply dissatisfied with the way the shared utility library influenced our code. A lot of what was in there was fairly throw-away and would not have been difficult to copy/paste, even to four or more different locations. And the shared nature of the library meant that any change to it was quite expensive. Technically, maybe, but, more importantly, socially. Any change to some corner of the library needed to be negotiated with every other team that was using that part of the library. The risk of the discussion spiraling away into an interminable series of bikesheddy meetings was always hiding in the shadows. So, if it was possible to leave the library function unchanged and get what you needed with a hack, teams tended to choose the hack. The effects of this phenomenon accumulated, over time, to create quite a mess.
I know this as AHA = Avoid Hasty Abstractions:
- https://kentcdodds.com/blog/aha-programming
- https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
Something I’ve mentioned to my direct reports during code reviews: Sometimes, code is duplicated because it just so happens to do something similar.
However, these are independent widgets and changes to one should not affect the other; in other words, not suitable for abstraction.
This type of reasoning requires understanding the problem domain (i.e., use-case and the business functionality ).
I like abstraction as much as the next guy but this is closer to obfuscation than abstraction.
[0] People who strive for "The One" architecture that will allow any change no matter what. Seriously, abstraction out the wazoo!
Honestly. If you're getting data from a bar code scanner and you think, "we should handle the case where we get data from a hot air balloon!" because ... what if?, you should retire.
Anyways, you are not alone with that experience - a common mistake I see, no matter what language or framework, is that people fall for the fallacy "separation into files" is the same as "separation of concerns".
const char_a = “a”;
And I couldn’t get my manager to understand why this was a problem.The Magento 2 codebase is a good example of this. It's both well written and horrible at the same time. Everything is so spread out into constituent technical components, that the code loses the "narrative" of what's going on.
Case in point: Bob Martin's "Video Store" example.
My best guess is that clean code, to them, was as little code on the screen as possible, not necessarily "intention revealing code either", instead everything is abstracted until it looks like it does nothing.
Also sat in their IRC channel for months and the lead developer was constantly discussing how he'd refactor it to be cleaner but never seemed to add code that did something.
I personally don't feel all that productive spending like half my time just navigating the code rather than actually reading it, but maybe it's just me.
What is mostly surprising I find most of developers are trying to obey the "rules". Code containing even minuscule duplication must be DRYied, everyone agrees that code must be clean and professional.
Yet it is never enough, bugs are showing up and stuff that was written by others is always bad.
I start thinking that 'Uncle Bob' and 'Clean code' zealots are actually harmful, because it prevents people from taking two steps back and thinking about what they are doing. Making microservices/components/classes/functions that end up never reused and making DRY holy grail.
Personally I am YAGNI > DRY and a lot of times you are not going to need small functions or magic abstractions.
A service has 3 "helper" services it calls, which may, in turn have helper services, or worse, depend on a shared repo project.
The only solution I have found is to move these helpers into their own project, and mark the helpers as internal. This achieves 2 things:
1. The "sub-services" are not confused as stand-alone and only the "main/parent" service can be called. 2. The "module" can now be deployed independently if micro-services ever become a necessity.
I would like feedback on this approach. I do honestly thing files over 100 lines long are unreadable trash, and we have achieved a lot be re-using modular services.
We are 1.5 years into a project and our code re-use is sky-rocketing, which allows us to keep errors low.
Of course, a lot of dependencies also make testing difficult, but allow easier mocks if there are no globals.
Dunno if this is the feedback you are after, but I would try to not be such an absolutist. There is no reason that a great 100 line long file becomes unreadable trash if you add one line.
That is not the fault of this book or any book. The problem is people treating the guidelines as rituals instead of understanding their purpose.
I'm all for the 100-200 line working version--can't say I've had a 500. I did once have a single SQL query that was about 2 full pages pushing the limits of DB2 (needed multiple PTFs just to execute it)--the size was largely from heuristic scope reductions. In the end, it did something in about 3 minutes that had no previous solution.
My clean code book:
* Put logic closest to where it needs to live (feature folders)
* WET (Write everything twice), figure out the abstraction after you need something a 3rd time
* Realize there's no such thing as "clean code"
so much this. it is _much_ easier to refactor copy pasta code, than to entangle a mess of "clean code abstractions" for things that isn't even needed _once_. Premature Abstraction is the biggest problem in my eyes.
Write Code. Mostly functions. Not too much.
So long as it remains identical. Refactoring almost identical code requires lots of extremely detailed staring to determine whether or not two things are subtly different. Especially if you don't have good test coverage to start with.
Just wanted to appreciate the nod to good nutritional advice here ("Eat food. Mostly plants. Not too much"), well done
DRY and WET are terms often used as objective measures of implementations, but that doesn't mean that they are rock solid foundations. What does it mean for something to be "repeated"? Without claiming to have TheGreatAnswer™, some things come to mind.
Chaining methods can be very expressive, easy to follow and maintain. They also lead to a lot of repetition. In an effort to be "DRY", some might embark on a misguided effort to combine them. Maybe start replacing
`map(x => x).reduce(y, z => v)`
with `mapReduce(x => x, (y,z) => v)`
This would be a bad idea, also known as Suck™.But there may equally be situations where consolidation makes sense. For example, if we're in an ORM helper class and we're always querying the database for an object like so
`objectContext.Orders.Select(e => e.id = y).Include(e => e.Customers).Include(e => e.Bills).Include(e => e.AwesomeDogs)...`
then it with make sense to consolidate that into `orderIncludingCustomersBillsAndDogs(id) => ...`
My $0.02:Don't needlessly copy-pastes that which is abstractable.
Don't over abstract at the cost of simplicity and flexibility.
Don't be a zealot.
I totally agree assuming that there will be time to get to the second pass of the "write everything twice" approach...some of my least favorite refactoring work has been on older code that was liberally copy-pasted by well-intentioned developers expecting a chance to come back through later but who never get the chance. All too often the winds of corporate decision making will change and send attention elsewhere at the wrong moment, and all those copy pasted bits will slowly but surely drift apart as unfamiliar new developers come through making small tweaks.
When we got absorbed by a larger team and were going to have to "merge" our code review / git history into a larger org's repos, we learned that a sister team had gotten in big trouble with the language cops in HR when they discovered similar language in their git commit history. This brings back memories of my team panicked over trying to rewrite a huge amount of git history and code review stuff to sanitize our language before we were caught too.
Its easy to refactor if its nondivergent copypasta and you do it everywhere it is used not later than the third iteration.
If the refactoring gets delayed, the code diverges because different bugs are noticed and fixed (or thr same bug is noticed and fixed different ways) in different iterations, and there are dozens of instances across the code base (possibly in different projects because it was copypastad across projects rather than refactored into a reusable library), the code has in many cases gotten intermixed with code addressing other concerns...
Think about data structures (types) first. Mostly immutable structures. Then add your functions working on those structures. Not too many.
Good code should be written to be easy to delete.
'Clever' abstractions work against this. We should be less precious about our code and realise it will probably need to change beyond all recognition multiple times. Code should do things simply so the consequences of deleting it are immediately obvious. I think your recommendations fit with this.
This article tends to make the rounds on here every once in a while: https://programmingisterrible.com/post/139222674273/how-to-w...
Etc... etc...
(Not saying that this is the only quality of good code or that you won’t have to trade some of the above for performance or whatnot at times).
There are two opposite situations. One is when several things are viewed as one thing while they're actually different (too much abstraction), and another, where a thing is viewed as different things, when it's actually a single one (when code is just copied over).
In my experience, the best way to solve this is to better analyse and understand the requirements. Do these two pieces of code look the same because they actually mean thing in the meaning of the product? Or they just happen to look the same at this particular moment in time, and can continue to develop in completely different directions as the product grows?
In practice (time pressure) you might end up duplicating it many times, at which point it becomes difficult to refactor.
It's a similar design and planning principle to building sidewalks. You have buildings but you don't know exactly the best paths between everything and how to correctly path things out. You can come up with your own design but people will end up ignoring them if they don't fit their needs. Ultimately, you put some obvious direct connection side walks and then wait to see the paths people take. You've now established where you need connections and how they need to be formed.
I do a lot of prototyping work and if I had to sit down and think out a clean abstraction everytime I wanted to get for a functional prototype, I'd never have a functional prototype--plus I'd waste a lot of cognitive capacity on an abstraction instead of solving the problem my code is addressing. It's best, from my experience, to save that time and write messy code but tuck in budget to refactor later (the key is you have to actually refactor later not just say you will).
Once you've built your prototype, iterated on it several times had people break designs forcing hacked out solutions, and now have something you don't touch often, you usually know what most the product/service needs to look like. You then abstract that out and get 80-90% of what you need if there's real demand.
The expanded features beyond that can be costly if they require significant redesign but at that point, you hopefully have a stable enough product it can warrant continued investment to refactor. If it doesn't, you saved yourself a lot of time and energy worrying trying to create a good abstract design that tends to fail multiple times at early stages. There's a balance point of knowing when to build technical debt, when to pay it off, and when to nullify it.
Again, the critical trick is you have to actually pay off the tech debt if that time comes. The product investor can't look bright eyed and linearly extrapolate progress so far thinking they saved a boatload of capital, they have to understand shortcuts were taken and the rationale was to fix them if serious money came along or chuck them in the bin if not.
That's still too much of a "rule".
Whenever I feel (or know) two functions are similar, the factors that determine if I should merge them:
- I see significant benefit too doing so, usually the benefit of a utility that saves writing the same thing in the future, or debugging the same/similar code repeatedly.
- How likely the code is to diverge. Sometimes I just mark things for de-duping, but leave it around a while to see if one of the functions change.
- The function is big enough it cannot just be in-lined where it is called, and the benefit of de-duplication is not outweighed by added complexity to the call stack.
Document.
Your.
Shit.
everything else can do one. Just fucking write documentation as if you're the poor bastard trying to maintain this code with no context or time.
Comments in code can lie (they're not functional); can be misplaced (in most languages, they're not attached to the code they document in any enforced way); are most-frequently used to describe things that wouldn't require documenting if they were just named properly; are often little more than noise. Code comments should be exceedingly-rare, and only used to describe exception situations or logic that can't be made more clear through the use of better identifiers or better-composed functions.
External documentation is usually out-of-sight, out-of-mind. Over time, it diverges from reality, to the point that it's usually misleading or wrong. It's not visible in the code (and this isn't an argument in favor of in-code comments). Maintaining it is a burden. There's no agreed-upon standard for how to present or navigate it.
The best way to document things is to name identifiers well, write functions that are well-composed and small enough to understand, stick to single-responsibility principles.
API documentation is important and valuable, especially when your IDE can provide it readily at the point of use. Whenever possible, it should be part of the source code in a formal way, using annotations or other mechanisms tied to the code it describes. I wish more languages would formally include annotation mechanisms for this specific use case.
One of the most difficult to argue comments in code reviews: “let’s make it generic in case we need it some place else”. First of all, chances that we need it some place else aren’t exactly high, unless you are writing a library and code explicitly design to be shared. And even if such need arises, chances of getting it right generalizing from one example are slim.
Regarding the book though, I have participated in one of the workshops with the author and he seemed to be in favor of WER and against “architecting” levels of abstraction before having concrete examples.
You can disagree over what exactly is clean code. But you will learn to distinguish what dirty code is when you try to maintain it.
As a person that has had to maintain dirty code over the years, hearing someone saying dirty code doesn't exist is really frustrating. Noone wants to clean up your code, but doing it is better than allowing the code to become unmaintainable, that's why people bring up that book. If you do not care about what clean code is, stop making life difficult for people that do.
Not sure why you're being downvoted, but as an unrelated aside, the quote you're responding to literally did not say this.
> that's why people bring up that book
I think the point is that following that book does not really lead to Clean Code.
> As a person that has to maintain dirty code
This is a strange credential to present and then use as a basis to be offended. Are you saying that you have dirty code and have to keep it dirty?
I mean yes it's more verbose, BUT it's also super clear and obvious what things do, and they do not leak the underlying implementation.
The other key thing I think is not to over-engineer abstractions you don’t need yet. But to try and leave ‘seams’ where it’s obvious how to tease code about if you need to start building abstractions.
Can you say more about this?
I think I may have stumbled on a similar insight myself. In a side project (a roguelike game), I've been experimenting with a design that treats features as first-class, composable design units. Here is a list of the subfolder called game-features in the source tree:
actions
collision
control
death
destructibility
game-feature.lisp
hearing
kinematics
log
lore
package.lisp
rendering
sight
simulation
transform
An extract from the docstring of the entire game-feature package: "A Game Feature is responsible for providing components, events,
event handlers, queries and other utilities implementing a given
aspect of the game. It's primarily a organization tool for gameplay code.
Each individual Game Feature is represented by a class inheriting
from `SAAT/GF:GAME-FEATURE'. To make use of a Game Feature,
an object of such class should be created, preferably in a
system description (see `SAAT/DI').
This way, all rules of the game are determined by a collection of
Game Features loaded in a given game.
Game Features may depend on other Game Features; this is represented
through dependencies of their classes."
The project is still very much work-in-progress (procrastinating on HN doesn't leave me much time to work on it), and most of the above features are nowhere near completion, but I found the design to be mostly sound. Each game feature provides code that implements its own concerns, and exports various functions and data structures for other game features to use. This is an inversion of traditional design, and is more similar to the ECS pattern, except I bucket all conceptually related things in one place. ECS Components and Systems, utility code, event definitions, etc. that implement a single conceptual game aspect live in the same folder. Inter-feature dependencies are made explicit, and game "superstructure" is designed to allow GFs to wire themselves into appropriate places in the event loop, datastore, etc. - so in game startup code, I just declare which features I want to have enabled.(Each feature also gets its set of integration tests that use synthetic scenarios to verify a particular aspect of the game works as I want it to.)
One negative side effect of this design is that the execution order of handlers for any given event is hard to determine from code. That's because, to have game features easily compose, GFs can request particular ordering themselves (e.g. "death" can demand its event handler to be executed after "destructibility" but before "log") - so at startup, I get an ordering preference graph that I reconcile and linearize (via topological sorting). I work around this and related issues by adding debug utilities - e.g. some extra code that can, after game startup, generate a PlantUML/GraphViz picture of all events, event handlers, and their ordering.
(I apologize for a long comment, it's a bit of work I always wanted to talk about with someone, but never got around to. The source of the game isn't public right now because I'm afraid of airing my hot garbage code.)
It might be hard to integrate related things, e.g. physical simulation/kinematics <- related to collisions, and maybe sight/hearing <- related to rendering; Which is all great if information flows one way, as a tree, but maybe complicated if it's a graph with intercommunication.
I thought about this before, and figured maybe the design could be initially very loose (and inefficient), but then a constraint-solver could wire things up as needed, i.e. pre-calculate concerns/dependencies.
Another idea, since you mention "logs" as a GF: AOP - using " join points" to declaratively annotate code. This better handles code that is less of a "module" (appropriate for functions and libraries) and more of a cross-cutting "aspect" like logging. This can also get hairy though: could you treated "(bad-path) exception handling" as an aspect? what about "security"?
Your top issue in the runtime game loop is always with concurrency and synchronization logic - e.g. A spawns before B, if A's hitbox overlaps with B, is the first frame that a collision event occurs the frame of spawning or one frame after? That's the kind of issue that is hard to catch, occurs not often, and often has some kind of catastrophic impact if handled wrongly. But the actual effect of the event is usually a one-liner like "set a stun timer" - there is nothing to test with respect to the event itself! The perceived behavior is intimately coupled to when its processing occurs and when the effects are "felt" elsewhere in the loop - everything's tied to some kind of clock, whether it's the CPU clock, the rendered frame, turn-taking, or an abstracted timer. These kinds of bugs are a matter of bad specification, rather than bad implementation, so they resist automated testing mightily.
The most straightforward solution is, failing pure functions, to write more inline code(there is a John Carmack posting on inline code that I often use as a reference point). Enforce a static order of events as often as possible. Then debugging is always a matter of "does A happen before B?" It's there in the source code, and you don't need tooling to spot the issue.
The other part of this is, how do you load and initialize the scene? And that's a data problem that does call for more complex dependency management - but again, most games will aim to solve it statically in the build process of the game's assets, and reduce the amount of game state being serialized to save games, reducing the complexity surface of everything related to saves(versioning, corruption, etc). With a roguelike there is more of an impetus to build a lot of dynamic assets(dungeon maps, item placements etc.) which leads to a larger serialization footprint. But ultimately the focus of all of this is on getting the data to a place where you can bring it back up and run queries on it, and that's the kind of thing where you could theoretically use SQLite and have a very flexible runtime data model with a robust query system - but fully exploiting it wouldn't have the level of performance that's expected for a game.
Now, where can your system make sense? Where the game loop is actually dynamic in its function - i.e. modding APIs. But this tends to be a thing you approach gradually and grudgingly, because modders aren't any better at solving concurrency bugs and they are less incentivized to play nice with other mods, so they will always default to hacking in something that stomps the state, creating intermittent race conditions. So in practice you are likely to just have specific feature points where an API can exist(e.g. add a new "on hit" behavior that conditionally changes the one-liner), and those might impose some generalized concurrency logic.
The other thing that might help is to have a language that actually understands that you want to do this decoupling and has the tooling built in to do constraint logic programming and enforce the "musts" and "cannots" at source level. I don't know of a language that really addresses this well for the use case of game loops - it entails having a whole general-purpose language already and then also this other feature. Big project.
I've been taking the approach instead of aiming to develop "little languages" that compose well for certain kinds of features - e.g. instead of programming a finite state machine by hand for each type of NPC, devise a subcategory of state machines that I could describe as a one-liner, with chunks of fixed-function behavior and a bit of programmability. Instead of a universal graphics system, have various programmable painter systems that can manipulate cursors or selections to describe an image. The concurrency stays mostly static, but the little languages drive the dynamic behavior, and because they are small, they are easy to provide some tooling for.
Do I force myself to follow every single crazy rule in Clean Code? Heck no. Some of them I don't agree with. But do I find myself to be a better coder because of what I learned from Bob Martin? Heck yes. Most of the points he makes are insightful and I apply them daily in my job.
Being a professional means learning from many sources and knowing that there's something to learn from each of them- and some things to ignore from each of them. It means trying the things the book recommends, and judging the results yourself.
So I'm going to keep recommending Clean Code to new developers, in the hopes that they can learn the good bits, and learn to ignore the bad bits. Because so far, I haven't found a book with more good bits (from my perspective) and fewer bad bits (from my perspective).
[0]https://en.wikipedia.org/wiki/Don%27t_throw_the_baby_out_wit...
I see a lot of people in here acting like all the advice in Clean Code is obviously true or obviously false, and they claim to know how to write a better book. But, like you, I will continue to recommend Clean Code to new developers on my team. It's the fastest way (that I've found so far, though I see other suggestions in the comments here) to get someone to transition from writing "homework code" (that never has to be revisited) to writing maintainable code. Obviously, there are bad parts of Clean Code, but if that new dev is on my team, I'll talk through why certain parts are less useful than others.
And to be honest that book will never exist, every knowledge contributes to growing as a professional, just make sure to understand, discuss, and use it (or not) for a real reason, not just becaue its on book A or B.
Its not like people need to choose one book and follow it blindly for the rest of their lives, read more books :D
You're not supposed to take it literally for every project, these are concepts that you need to adapt to your needs. In that sense I think the book still holds up.
The Dreyfus model identifies 5 skill levels:
Novice
Wants to achieve a goal, and not particularly interested in learning. Requires context free rules to follow. When something unexpected happens will get stuck.
Advanced Beginner
Beginning to break away from fixed rules. Can accomplish tasks on own, but still has difficulty troubleshooting. Wants information fast.
Competent
Developed a conceptual model of task environment. Able to troubleshoot. Beginning to solve novel problems. Seeks out and solve problems. Shows initiative and resourcefulness. May still have trouble determining which details to focus on when solving a problem.
Proficient
Needs the big picture. Able to reflect on approach in order to perform better next time. Learns from experience of others. Applies maxims and patterns.
Expert
Primary source of knowledge and information in a field. Constantly look for better ways of doing things. Write books and articles and does the lecture circuit. Work from intuition. Knows the difference between irrelevant and important details.
Meh. I'm probably being picky, but it doesn't surprise me that a Thought Leader would put themselves and what they do as Thought Leader in the Expert category. I see them more as running along a parallel track. They write books and run consulting companies and speak at conferences and create a brand, and then there are those of us who get good at writing code because we do it every day, year after year. Kind of exactly the difference between sports commentators and athletes.
If treated as guidelines you are correct Clean Code is only eh instead of garbage. But taken in the full context of how it is presented/intended to be taken by the author it is damaging to the industry.
In no way is he treating his book as a bible.
However, in most cases heading towards that goal is a beneficial thing--you just have to recognize when you're getting too close and bogging down in complying with every detail.
I still consider it the best programming book I've ever read.
I agree: Clean Coders and TDDers are cut from the same cloth.
I agree with the sentiment that you don't want to over abstract, but Bob doesn't suggest that (as far as I know). He suggests extract till you drop, meaning simplify your functions down to doing one thing and one thing only and then compose them together.
Hands down, one of the best bits I learned from Bob was the "your code should read like well-written prose." That has enabled me to write some seriously easy to maintain code.
That strikes me as being too vague to be of practical use. I suspect the worst programmers can convince themselves their code is akin to poetry, as bad programmers are almost by definition unable to tell the difference. (Thinking back to when I was learning programming, I'm sure that was true of me.) To be valuable, advice needs to be specific.
If you see a pattern of a junior developer committing unacceptably poor quality code, I doubt it would be productive to tell them Try to make it read more like prose. Instead you'd give more concrete advice, such as choosing good variable names, or the SOLID principles, or judicious use of comments, or sensible indentation.
Perhaps I'm missing something though. In what way was the code should read like well-written prose advice helpful to you?
For example, using what I learned I read this code (https://github.com/cheatcode/nodejs-server-boilerplate/blob/...) as:
"This module is going to generate a password reset token. First, it's going to make sure we have an emailAddress as an input, then it's going to generate a random string which I'll refer to as token, and then I want to set that token on the user with this email address."
Code should be simple and tight and small. It should also, however, strive for an eighth grade reading level.
You shouldn't try to make your classes so small that you're abusing something like nested ternary operators which are difficult to read. You shouldn't try to break up your concepts so much that while the sentences are easy, the meaning of the whole class becomes muddled. You should stick with concepts everyone knows and not try to invent your own domain specific language in every class.
Less code is always more, right up until it becomes difficult to read, then you've gone too far. On the other hand if you extract a helper method from a method which read fine to begin with, then you've made the code harder to read, not easier, because its now bigger with an extra concept. But if that was a horrible conditional with four clauses which you can express with a "NeedsFrobbing" method and a comment about it, then carry on (generating four methods from that conditional to "simplify" it is usually worse, though, due to the introduction of four concepts that could be often better addressed with just some judicious whitespace to separate them).
And I need to learn how to write in English more like Hemmingway, particularly before I've digested coffee. That last paragraph got away from me a bit.
Coding at scale is about managing complexity. The best code is code you don't have to read because of well named functional boundaries. Natural language is our facility for managing complexity generally. It shouldn't be surprising that the two are mutually beneficial.
I think the best compromise is small summary comments at various points of functions that "hold the entire thought".
Because of well structured abstractions, thoughtful naming conventions, documentation where required, and extensive testing you trust that the function does what it says. If I'm looking at a function like commitPending(), I simply see writeToDisk() and move on. I'm in the object representation layer, and jumping down into the details of the I/O layers breaks flow by moving to a different level of abstraction. The point is I trust writeToDisk() behaves reasonably and safely, and I don't need to inspect its contents, and definitely don't want to inline its code.
If you find that you frequently need to jump down the tree from sub-routine to sub-routine to understand the high level code, then that's a definite code smell. Most likely something is fundamentally broken in your abstraction model.
I think this is the best, honestly, it reaps most benefits of small single-use functions, without compromising readability or requiring jumping.
This is also how John Carmarck recommends in this popular essay: http://number-none.com/blow/john_carmack_on_inlined_code.htm...
When I get the error in the console/browser, the path to the error is included for me like "[generatePasswordResetToken.setTokenOnUser] Must pass value to $set to perform an update."
With that, I know exactly where the error is ocurring and can jump straight into debugging it.
1. The connect action could be replaced by doing the connection once on app startup.
2. The validation could be replaced with middleware like express-joi.
3. The stripe/email steps should be asynchronous (ex: simple crons). This way, you create the user and that's it. If Stripe is down, or the email provider is down, you still create the user. If the server restarts while someone calls the endpoint, you don't end up with a user with invalid Stripe config. You just create a user with stripeSetup=false and welcomeEmailSent=false and have some crons that every 5 seconds query for these users and do their work. Also, ensure you query for false and not "not equal to true" here as it's not efficient.
This is a good assertion but ironically it's not Bob Martin's line. He was quoting Grady Booch.
One long stream of commands is ok to read, if you are author or already know what it should do. But otherwise it forces you to read too many irrelevant details on a way toward what you need.
Of course reading Clean Code left me more confused than enlightened due precisely to what he presents as good examples of Code. The author of the article really does hit the nail on the head about Martin's code style - it's borderline unreadable a lot of times.
Who the f. even is Robert Martin?! What has he built? As far as I am able to see he is famous and revered because he is famous and revered.
I was around in the early 90s through to the early 2000s when a lot of the ideas came about slowly got morphed by consultants who were selling this stuff to companies as essentially "religion". The nuanced thoughts of a lot of the people who had most of the original core ideas is mostly lost.
It's a tricky situation, at the core of things, there are some really good ideas, but the messaging by people like "uncle bob" seem to fail to communicate the mindset in a way that develops thinking programmers. Mainly because him, and people like Ron Jerfferies, really didn't actually build anything serious once they became consultants and started giving out all these edicts. If you watched them on forums/blogs at the time, they were really not that good. There were lots of people who were building real things and had great perspectives, but their nuanced perspectives were never really captured into books, and it would be hard to as it is more about the mentality of using ideas and principles and making good pragmatic choices and adapting things and not being limited by "rules" but about incorporating the essence of the ideas into your thinking processes.
So many of those people walked away from a lot of those communities when it morphed into "Agile" and started being dominated by the consultants.
But I had more and more difficulty reconciling bizarrely optimistic patterns with reality. This from the article perfectly sums it up:
>Martin says that functions should not be large enough to hold nested control structures (conditionals and loops); equivalently, they should not be indented to more than two levels.
Back then as now I could not understand how one person can make such confident and unambiguous statements about business logic across the spectrum of use cases and applications.
It's one thing to say how something should be written in ideal circumstances, it's another to essentially say code is spaghetti garbage because it doesn't precisely align to a very specific dogma.
After you're a bit more seasoned, I think qualified comments are probably far more welcome than absolutes.
I don't think that's true at all. One of the old 'erlang bibles' is "learn you some erlang" and it full of qualifications titled "don't drink the kool-aid" (notably not there in the haskell inspiration for the book). It does not fail to inspire confidence to have qualifications scattered throughout and to me it actually gives me MORE confidence that the content is applicable and the tradeoffs are worth it.
https://learnyousomeerlang.com/introduction#about-this-tutor...
My recollection of the clean code book and Fowler books were very much “I think these are smells, but smells in the code are also fine”
Note: Robert Martin and Martin Fowler are different people. Are you saying Fowler said this?
Fixed the typo, thanks.
Now, some people will claim that having 8-character indentations makes
the code move too far to the right, and makes it hard to read on a
80-character terminal screen. The answer to that is that if you need
more than 3 levels of indentation, you're screwed anyway, and should fix
your program.
https://www.kernel.org/doc/Documentation/process/coding-styl...Does it say that though?
The point that I make about patterns and practices in programming is that they need to justify their existence in terms of value that they provide to the end user, to the customer, or to the business. If they can't provide clear evidence that they actually provide those benefits, or if they only provide benefits that the business isn't asking for, then they're just wasting time and money.
One example that Uncle Bob Martin hammers home a lot is separation of concerns. Separation of concerns can make your code a lot easier to read and maintain if it's done right -- unit testing is one good example here. But when it ceases to be a means to an end and becomes an end in itself, or when it tries to solve problems that the business isn't asking for, it degenerates into speculative generality. That's why you'll find project after project after project after project after project with cumbersome and obstructive data access layers just because you "might" want to swap out your database for some unknown mystery alternative some day.
Nobody thinks their house is going to burn down until it does.
Also, DALs are IMHO best practice for anything but the most rudimentary and not solely -- or even mainly -- for facilitating switching database.
> Martin says that it should be possible to read a single source file from top to bottom as narrative, with the level of abstraction in each function descending as we read on, each function calling out to others further down. This is far from universally relevant. Many source files, I would even say most source files, cannot be neatly hierarchised in this way.
The relevance is a fair criticism but most programs in most languages can in fact be hierarchized this way, with the small number of mutually interdependent code safely separated. Many functional languages actually enforce this.
As an F# developer it can be very painful to read C# programs even though I often find C# files very elegant and readable: it just seems like a book, presented out of order, and without page numbers. Whereas an .fsproj file provides a robust reading order.
> Many functional languages actually enforce this.
Don't they enforce the opposite? In ML languages (I don't know F# but I thought it was an ML dialect), you can generally only call functions that were defined previously.
Of course, having a clear hierarchy is nice whether it goes from most to least abstract, or the other way around, but I think Martin is recommending the opposite from what you are used to.
I see how this can be polemic because most code is littered w/ flags, but I tend to agree that boolean flags can be an anti-pattern (even though it's apparently idiomatic in some languages).
Usually the flag is there to introduce a branching condition (effectively breaking "a function should do one thing") but don't carry any semantic on it's own. I find the same can be achieved w/ polymorphism and/or pattern-matching, the benefit being now your behaviour is part of the data model (the first argument) which is easier to reason about, document, and extend to new cases (don't need to keep passing flags down the call chain).
As anything, I don't think we can say "I recommend / don't recommend X book", all knowledge and experience is useful. Just use your judgment and don't treat programming books as a holy book.
But if you don't let the function branch, then the parent function is going to have to decide which of two different functions to call. Which is going to require the parent function to branch. Sooner or later, someone has to branch. Put the branch where it makes the most sense, that is, where the logical "one-ness" of the function is preserved even with the branch.
> I find the same can be achieved w/ polymorphism and/or pattern-matching, the benefit being now your behaviour is part of the data model (the first argument) which is easier to reason about, document, and extend to new cases (don't need to keep passing flags down the call chain).
You just moved the branch. Polymorphism means that you moved the branch to the point of construction of the object. (And that's a perfectly fine way to do it, in some cases. It's a horrible way to try to deal with all branches, though.) Pattern-matching means that you moved the branch to when you created the data. (Again, that can be a perfectly fine way to do it, in some cases.)
People don't want to go through the trouble of reading several opposing points of view and synthesize that using their own personal experience. They want to have a book tell them everything they need to do and follow that blindly, and if that ever bites them back then that book was clearly trash. This is the POV the article seems to be written from IMHO.
Notably, this argument has very little to do with readability. I do prefer consolidating data and extracting data dependencies -- I think it makes it easier to get a big-picture view, as in Brook's "Show me your spreadsheets" -- but this argument is rooted specifically in not making the machine do redundant work.
This example (code is in the article) was very telling of the book author's core philosophy.
Best I can tell, the OOP movement of the 2000s (I wasn't a professional in 2008, though I was learning Java at the time) was at its heart rooted in the idea that abstractions are nearly always a win; the very idealistic perspective that anything you can possibly give a name to, should be given a name. That programmers down the line will thank you for handing them a named entity instead of perhaps even a single line of underlying code.
This philosophy greatly over-estimates the value, and greatly under-estimates the cost, of idea-creation. I don't just write some code, I create an idea, and then I write a bit of code as implementation details for it. This is a very tantalizing vision of development: all messy details are hidden away, what we're left with is a beautiful constellation of ideas in their purest form.
The problem is that when someone else has to try and make sense of your code, they first have to internalize all of your ideas, instead of just reading the code itself which may be calling out to something they already understand. It is the opposite of self-documenting code: it's code that requires its own glossary in addition to the usual documentation. "wayTooCold()" may read more naturally to the person who wrote it, but there's a fallacy where they assume that that also applies to other minds that come along and read it later.
Establishing a whole new concept with its own terminology in your code is costly. It has to be done with great care and only when absolutely necessary, and then documented thoroughly. I think as an industry we have more awareness of this nowadays. We don't just make UML diagrams and kick them across the fence for all those mundane "implementation details" to be written.
It's been years since I've read the book and I'm still having trouble with the bad ideas from there because they're so well stuck with me that I feel like I'm doing things wrong if I don't follow the guidelines in there. Sometimes I'll actually write something in a sensible way, change it to the Clean Code way, and then revert it back to where it was when I realize my own code is confusing me when written like that.
This isn't just a Robert C Martin issue. It's a cultural issue. People need to stop shaming others if their code doesn't align with Clean Code. People need to stop preaching from the book.
The author seems like they took a set of rules that are good for breaking beginning programmers bad habits and then applied them into the extreme. There's a whole lot of rules which aren't bad up until you try to apply them like laws of gravity that must always be followed. Breaking up big clunky methods that do way too much is great for readability, right up until you're spraying one line helper methods all over your classes and making them harder to read because now you're inventing your own domain specific language everyone has to learn (often with the wrong abstractions which get extended through the years and wind up needing a massive refactoring down the road which would have been simpler with fewer methods and abstractions involved at the start).
A whole lot of my job is taking classes, un-DRY'ing them completely so there's duplication all over the place, then extracting the right (or at least more correct) abstractions to make the whole thing simple and readable and tight.
Realize that your whole program is effectively one big function and you achieve nothing by scattering its guts out into individual sub-functions just to make the pieces smaller.
If something is too convoluted and does too much, or has too much redundancy, you'll know, because it'll cause problems. It'll bother you. You shouldn't pre-empt this case by just writing small functions by default. That'll just cause its own problems.
"An explanation should be as simple as possible, but no simpler" - Albert Einstein
Using Martin's suggestion with Functional Hexagonal Architecture would lead to beautiful code. I know because that's what I've been writing for the past 3 years.
I get the impression that it’s about the messengers and not the messages, and that people have had horrible learning experiences that have calcified into resistance to do with anything clean. But valuable insights are being lost, and they will have to be re-learned in a new guise at a later date.
I've found the Clean Code guidelines very useful. Your team's mileage may very. As always: Use what works, toss the rest, give back where you can.
For more about this see:
* Bob Martin's blog post 'Too Clean' - https://blog.cleancoder.com/uncle-bob/2018/08/13/TooClean.ht...
* Livable Code by Sarah Mei - https://www.youtube.com/watch?v=8_UoDmJi7U8
I've seen so many real world counter-examples to arguments made in that article and his other blog posts that I'm puzzled why this guy has such a large and devoted following.
His point is that you can’t practically force programmers to be diligent through safety features of a language itself, since edge-cases require escape hatches from those safety features, and those safety hatches will be exploited by our natural tendency to avoid “punishment”.
I’m not sure I agree with his point, but I don’t find it an unreasonable position. I’d be curious if Rust has escape hatches that are easily and often abused.
My favorite example here, and a counterpoint to Bob, is Reacts’s dangerously-unsafe-html attribute. I haven’t seen it in years (to the point where I can’t recall the exact naming), and perhaps it was removed at some point. But it made the escape hatch really painful to use. And so the pain of using the escape hatch made it less painful to actually write React in the right manner. Coming from Angular, I think I struggled at first with thinking I had to write some dangerous html, but over time I forgot the choice of writing poor React code even existed.
So I guess I disagree with Bob’s post here. It is possible to have safety features in languages that are less painful than the escape-hatches from those safety features. And no suite of tests will ever be as powerful as those built-in safety features.
I find that these two books are in many recommended lists, but I found them entirely unforgettable, entirely too long, and without any "meat."
So much of the advice given is trivial things you'll just figure out in the first few months of coding professionally. Code for more than a week and you'll figure out how to name classes, how to use variables, how scope works, etc. The code examples are only in C++, Java, and Visual Basic (ha!). Completely ignoring non-OO and dynamic languages. Some of the advice is just bad (like prefixing global variables with g_) or incredibly outdated (like, "avoid goto"? Thanks 1968!).
Work on a single software project, or any problem ever, and you'll know that you need to define the problem first. It's not exactly sage advice.
These are cherry-picked examples, but overall Code Complete manages to be too large, go into too specific detail in some areas, while giving vague advice in others.
All books are written in a time and a few become timeless. Software books have an especially short half-life. I think Code Complete was a book Software Engineering needed in 2004, but has since dropped in value.
I will say, that Code Complete does have utility as a way to prop up your monitor for increased ergonomics, which is something you should never ignore.
I have similar issues with Clean Code. One is better off just googling "SOLID Principles" and then just programming using small interfaces more often and use fewer subclasses.
A better alternative is (from above) The Pragmatic Programmer (2019), a good style guide, and/or get your code reviewed by literally anyone.
Rubbish! Multiple times I've had to track down the original author of code that was auto-refactored, reformatted, changed locations, changed source control, etc. "git blame" and such are useless in these cases; it ends up being a game of Whodunit that involves hours of search, Slack pings, and what not. Just put your name in the comment, if it's documenting something substantial and is the result of your own research and struggle. And if you're in such a position, allow and encourage your colleagues to do this too.
And has Martin ever even worked on large, non-greenfield projects? That's the only way I could see anyone professing such idealism.
I mean by all means assign a "domain expert" to a PART of your code, but no individual segment of code should belong to anyone.
Second: There's something to be said about avoiding churn. Everybody loves refactoring and rewriting code, present company included, but it muddles the version control waters. I've seen a few github projects where the guidelines stated not to create PRs for minor refactorings, because they create churn and version control noise.
Anyway, that's all "ideal world" thinking, I know in practice it doesn't work like that.
Either the code is recent (in which case 'git blame' works better since someone changing a few characters may or may not decide to add their name to the file) or it's old and the author has either left the company or has forgotten practically everything about the code.
Uncle Bob is ruining software - https://hillelwayne.com/post/10x/
Uncle Bob and Silver Bullets - https://hillelwayne.com/post/uncle-bob/
Moving from Java as my only language to JavaScript and Rust, this point was driven home in spades. A programming language can be dysfunctional, causing its users to implement harmful practices.
SetupDeardownIncluder is a good example of the kind of code you get when there are no free-standing functions. It's also one path on the slippery slope to FactoryFactoryManager code.
The main problem is that the intent of the code isn't even clear. Compare it with something you might write in Rust:
fn render(page_data: &PageData) -> String {
// render the page
}
If you saw that function at the top line of file, or if you saw render.rs in a directory listing, you'd have a pretty good idea of what's going on before you even dug into the code.Just randomly searching the Fitness repo, there's this:
// Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.
// Released under the terms of the GNU General Public License version 2 or later.
package fitnesse.html;
public class HtmlPageFactory
{
public HtmlPage newPage()
{
return new HtmlPage();
}
public String toString()
{
return getClass().getName();
}
}
https://github.com/unclebob/fitnesse/blob/3bec390e6f8e9e3411...Really, how does this file even justify its existence? Its a subsidy courtesy of language design decisions made a long time ago.
The problem is people applying principles dogmatically without seeing the larger picture or considering the context purpose of the rules in the first place.
This book or any book cannot be blamed for people applying the advice blindly. But it is a pervasive problem in the industry. It runs much deeper than any particular book. I suspect it has something to do with how CS education typically happen, but I'm not sure.
I know nothing of clean code. When I read the link, I assumed that clean code meant very simple and well commented code. I hit cmd+f # and nothing came up. Not one comment saying "this function is an example of this" or "note the use of this line here, it does this" etc, on a blog no less where you'd expect to see these things. The type of stuff I put in my own code, even the code that only I read, because in two weeks I'm going to forget everything unless I write full sentences to paragraph comments, and spend way more time trying to get back in the zone than the time it took me to write those super descriptive comments in the first place.
I hate looking at other peoples scripts because, once again, they never write comments. Practically ever. What they do write is often entirely useless to the point where they shouldn't have even bothered writing those two words or whatever. Most people's code is just keyboard diarrhea of syntax and regex and patterns that you can't exactly google for, assuming whoever is looking at the code has the exact same knowledge base as you, and knows everything that you've put down into the script. Maybe it's a side effect of CS major training, where you don't write comments on your homework because the grader is going to know what is happening. Stop doing that with your code and actually make a write up to save others (and often yourself) mountains of time and effort.
Good question. Funny thing is, I worked for a company that mandated that every method be documented, which gets you a whole bunch of "The method GetThing(name) gets a Thing, and the argument 'name' is the name of the Thing". Plus 4 lines of Doxygen boilerplate. Oof.
Of course, I've seen my share of uncommented, unreadable code. And also code with some scattered comments that have become so mangled over 10 years of careless maintenance and haphazard copy-pasting that their existence is detrimental. Many of the comments I come across that might be useful are incoherent ungrammatical ramblings. In large projects, often some of the engineers aren't native English speakers.
My point being that writing consistently useful comments (and readable, properly organized code) is hard. Very, very hard. It requires written communication skills that only a small percentage of engineers (or even humans in general) are capable of. And the demand for software engineers is too high to filter out people who aren't great at writing. So I guess many people just try to work around the problem instead.
Personally now I've held both sides of the argument at different times. I think in the end it's a trade-off. There's no hard and fast rule, you need to use your best judgement about what's going to be easiest for the reader and also for the people maintaining the code. Usually I just try to strike a balance that I think my coworkers won't complain about. The other thing I've realized that makes this tricky is that people will almost always overestimate their (or others) commitment to maintaining comments, and/or overestimate how "self-documenting" their code is.
Years ago we started using fitnesse at a place I was working, and we needed something that was not included, I think it was being able to make a table of http basic auth tests/requests.
The code base seems large and complex at first, but I was able to very quickly add this feature with minimal changes and was pretty confident it was correct. Also, I had little experience in Java at the time. All in all it was a pretty big success.
So apply with caution and care and you'll be fine.
All the "bad" examples discussed in the article are rather context dependent, representing uninspired examples or extreme tastes in the book rather than bad or obsolete ideas.
Shredding medium length meaningful operations into many very small and quite ad hoc functions can reduce redundancy at the expense of readability, which might or might not be an improvement; a little DSL that looks silly if used in a couple of test cases can be readable and efficient if more extensive usage makes it familiar; a function with boolean arguments can be an accretion of special cases, mature for refactoring or a respectable way to organize otherwise repetitive code.
There are books that have done something like this[0], but they are a bit high level. There is room for something at a lower level.
[0]: http://aosabook.org/en/index.html for example.
Living in a world where people regularly write single functions that: 1. loads data from a hardcoded string path of file location 2. does all the analysis inside the same loop that the file content iteration happens in and 3. plots the results ... that cleavage plane is a meaningfully good one.
The rest of the ideas fall into "all things in moderation, including moderation", and can and should be special-cased judiciously as long as you know what you're doing. But oh god please can we stop writing _that_ function already.
Because Clean Code is really a good book once you have enough experience to understand when each idea works and when does not. And why.
And noobs tend to over-engineer, so any book like CC or design patterns give them additional excuse and reason to over engineer and make a mess.
Leave off the word "clean" or whatever... DO have metrics and don't ignore them. You have people on your team that make it easier for the others, and people who take their "wins" at the expense of their teammates' productivity.
Come to think of it, is the original C code for Python's timsort equally non-Clean Code-ish?[2] Probably not!
[1] https://github.com/AdoptOpenJDK/openjdk-jdk11u/blob/master/s...
[2] https://github.com/python/cpython/blob/main/Objects/listobje...
However, the book also presents the psych study about people not remembering more than 7 (+/- 2) things at a time (therefore you should simplify your code so readers don't have to keep track of too much stuff) and it stuck with me. I must be one of the people with only 5 available slots in their brain...
(edited for clarity)
That study was done for specific stimuli (words, digits), and doesn't generalize to e.g. statements. There are studies that show that rate of presentation, complexity, and processing load have an effect. However, STM capacity is obviously limited, so it's good to keep that in mind when you're worried about readability. And I think it's also safe to assume that expert programmers can "chunk" more than novices, and have a lower processing load.
Yoda conditions? I hate those, they are difficult to read. Yes they are useful for languages which allow assignments in conditionals, but even then it's not really worth it. It's a very novice mistake to make. For me equality rarely appears in conditionals, it's either a numeric comparison or checking for existence.
I don't believe in being prescriptive to anyone about how they write their code, because I think people have different preferences and forcing someone to write small functions when they tend to write large functions well is a unique form of torture. Just leave people alone and let them do their job!
I don't think it is the perfect solution, but a lot of people assert "we can't do better, no point in trying, just write whatever you feel like" and I think that is a degenerate attitude. We CAN find better ways to construct and organize our code, and I don't think we should stop trying because people don't want to update their pull requests.
There is only years, decades of deep experience into certain domains (e.g. game rendering engine programming, or, Customer Relationship backends), extra hours reading proved high quality code, countless times of reflection on existing code (that also means extra hours reviewing existing code) based on the reading and a strong will to improve them, not based on some set of rules, but based on common sense on programming and many trials of re-writing the code into another form.
I think ultimately it goes down to something similar to 10,000-hour rule: We need to put down a lot of time in X, and not only that, we also need to challenge ourselves for every step.
I dont even take issue with name. It makes it easy to find the class just by remembering what it should do. I dont really care all that much about verbs vs nouns. I want to be able to find the right place by having rough idea about what it should do. I want to get hint about class functionality from its name too.
[0] https://pragprog.com/titles/tpp20/the-pragmatic-programmer-2...
One guideline I've always tried to keep in mind is that statistically speaking, the number of bugs in a function goes way up when the code exceeds a page or so in length. I try to keep that in mind. I still routinely write functions well over a page in length but I give them extra care when they do, lots of comments and I make sure there's a "narrative flow" to the logic.
All of these 'clean code' style systems have the same flaw. People follow them without understanding why the system was made. It is why you see companies put in ping pong tables, but no one uses them. They saw what someone else was doing and they were successful so they copy them. Not understanding why the ping pong table was there. They ignore the reason the chesterton's fence was built. Which is just as important if you are removing it. Clean code by itself is 'ok'. I personally am not very good at that particular style of coding. I do like that it makes things very nice to decompose into testing units.
A downside to this style of coding is it can hide complexity with an even more complex framework. It seems to have a nasty side effect of smearing the code across dozens of functions/methods which is harder in some ways to get the 'big picture'. You can wander into a meeting and say 'my method has CC of 1' but the realty is that thing is called at the bottom of a for loop, inside of 2 other if conditions. But you 'pass' because your function is short.
Isn't it because one is pre-allocated with a known size of n and the other is grown dynamically?
> And what of thread safety?
Indeed. If he had written the prime number class like the earlier example, with public static methods creating a new instance for each call and all the other methods being private instance methods, this wouldn't be an issue.
They will preach the awesomeness of clean code in terms of maintainability, scalability and all those fancy enter-pricey terms that at the end of the day, brought not enough value to justify their cost.
IMHO it's only when you can't see the top and bottom of a function on the screen at the same time that you should start to worry.
Love to know if there is an easy way of doing this!
I'm not the only one who is skeptical of this toxic, holier-than-thou and dangerous attitude.
Removing braces from if statements is a great example of another dangerous thing he advocates for no justifiable reason
https://softwareengineering.stackexchange.com/questions/3202...
Which caused the big OSX/iOS SSL bug in 2014, see https://www.imperialviolet.org/2014/02/22/applebug.html
This link and thread on hackernews is good too
https://news.ycombinator.com/item?id=15440848
>The current state of software safety discussion resembles the state of medical safety discussion 2, 3 decades ago (yeah, software is really really behind time). > >Back then, too, the thoughts on medical safety also were divided into 2 schools: the professionalism and the process oriented. The former school argues more or less what Uncle Bob argues: blame the damned and * who made the mistakes; be more careful, damn it. > >But of course, that stupidity fell out of favor. After all, when mistakes kill, people are serious about it. After a while, serious people realize that blaming and clamoring for care backfires big time. That's when they applied, you know, science and statistic to safety. > >So, tools are upgraded: better color coded medicine boxes, for example, or checklists in surgery. But it's more. They figured out what trainings and processes provide high impacts and do them rigorously. Nurses are taught (I am not kidding you) how to question doctors when weird things happen; identity verification (ever notice why nurses ask your birthday like a thousand times a day?) got extremely serious; etc. > >My take: give it a few more years, and software, too, probably will follow the same path. We needs more data, though.
> Page Not Found
> This question was removed from Software Engineering Stack Exchange for reasons of moderation.
This book was written to make developers think and consider their choices, not a script for good code.
what is an output argument?
Example: the caller could pass an empty list, and the method adds items to the list.
Why not return the list? Well, maybe the method computes more things than just the list.
sprintf(buffer,"formatstring", args)
'buffer' is an output argument.
For balance, perhaps in both statically and dynamically typed languages?
Thank you!
2. We might not use the same methods to write clean code. But when you see clean code, you know it is clean.
3. Some traits clean code can have
- When you read the function name, you understand what the function does
- When you read the content of a function, you can understand what it is about without reading line by line.
- When you try to refactor clean code, you find yourself sometimes ending up only changing one cog in the whole system.
0. Real clean code does not exist.So glad I work at a company now where we actually THINK on the right abstractions now and not peddle some book
Read with great skepticism, but don't feel bad if you decide not to read it at all.
if it weren’t in a published book id have though i were being trolled.
In truth however, like playing jazz, the only thing that really matters is results, and even those can be subjective. You can learn and practice scales all day long, but that doesn't really tell you how to make music.
I developed a style of software engineering that works really well for me. It's fast, accurate, and naturally extensible and easily refactorable. However, for various reasons, I've never been able to explain it junior (or even senior) engineers, when asked about why I code a certain way. At a certain point, it's not the material that matters, but the audience's ability to really get what's at the heart of the lesson.
Technically, you could say something that's indisputable accurate, like, "there're only 12 notes in an (western) octave, and you just mix them until they sound good", but that's obviously true to a degree that's fundamentally unhelpful. At the same time, you could say "A good way to think about how to use a scale is to focus less on the notes that you play and more on the ones you don't". This is better advice but it may altogether be unhelpful, because it doesn't really yet strike at the true heart of what holds people back.
So at a certain point, I don't really know if anyone can be taught something as fundamentally "artful" (I.e. a hard thing largely consisting of innumerable decisions that are larger matters of taste - which is a word that should not be confused with mere "opinion") as software engineering or jazz music. This is because teaching alone is just not enough. At a certain point people just have to learn for themselves, and obviously the material that's out there is helpful, but I'm not sure if anything can ever be explained so well as to remove the need for the student at a certain point to simply "feel" what sounding good sounds like, or what good software engineering feels like.
I'll add one last thing. Going back to what I was saying about not being able to explain to "junior (or even senior)" engineers. Were the same "lesson" to happen with someone who is very, very advanced, like a seasoned principal engineer who's built and delivered many things, time and time again, across many different engineering organizations and different technologies - someone like a jazz music great for example. Anything I would have to say on my approach to such a software engineer would be treated as obvious and boring, and they'd probably much rather talk about something else. I don't say this because I mean to imply that whatever I would have to say is wrong or incorrect, but rather at a certain level of advancement, you forget everything that you know and don't remember what it took to get there. There are a few who have a specific passion for teaching, but that's orthogonal to the subject.
I think it was Bill Evans who said something like "it takes years of study and practice to learn theory and technique, but it takes still a lifetime to forget it all". When you play like you've forgotten it all, that is when you achieve that certain sound in jazz music. Parenthetically, I'll add that doesn't mean you can't sound good being less advanced, but there's a certain sound that I'm trying to tie together with this metaphor that's parallel to great software engineering from knowledge, practice, and then the experience to forget it all and simply do.
I think that's fundamentally what's at the heart of the matter, not that it takes anyone any closer to getting there. You just have to do it, because we don't really know how to teach how to do really hard things in a way that produces reproducible results.
It's even more important to stick to the site guidelines when you're right, because otherwise you discredit the truth and give people a reason to reject it, which harms all of us.
https://hn.algolia.com/?dateRange=all&page=0&prefix=true&sor...
2014 HN discussion: https://news.ycombinator.com/item?id=8374345
A choice quote from Carmack:
> The function that is least likely to cause a problem is one that doesn't exist, which is the benefit of inlining it. If a function is only called in a single place, the decision is fairly simple.
> In almost all cases, code duplication is a greater evil than whatever second order problems arise from functions being called in different circumstances, so I would rarely advocate duplicating code to avoid a function, but in a lot of cases you can still avoid the function by flagging an operation to be performed at the properly controlled time. For instance, having one check in the player think code for health <= 0 && !killed is almost certain to spawn less bugs than having KillPlayer() called in 20 different places.
I think the right answer depends on a number of other factors.
Because, they're more like guidelines. If you try not to repeat yourself, you'll generally wind up with better code. If you try to make your methods short, you'll generally wind up with better code.
However, if you abuse partial just to meet some arbitrary length requirement, then you haven't really understood the reason for the guideline.
The problem is that Uncle Bob has a delusional cult following (that he deliberately cultivated), which takes everything he says at face value, and are willing to drown out any dissenting voices with a non-stop barrage of bullshit platitudes.
There are plenty of ideas in Clean Code that are great, and there are plenty that are terrible...but the religiosity of adherence to it prevents of from separating the two.
I suspect a lot of backlash against it centers around Uncle Bob's less than progressive political and social stances in recent years.
I cannot believe what I am reading here.
My open source community knows exactly what good code looks like and we've delivered great products in very short timeframes repeatedly and often beating our own expectations.
These kinds of articles make me feel like I must have discovered something revolutionary... But in reality I'm just following some very simple principles which were invented by other people several decades ago.
Too many coders these days have been misled into all sorts of goofy trends. Most coders don't know how to code. The vast majority of the people who claim to be experts and who write books about it don't know what they're talking about. That's the real problem. The industry has been hijacked by people who simply aren't wise or clever enough to be sharing any kind of complex knowledge. There absolutely is such a thing as good code.
I'm tired of hearing developers who have never read a single word of Alan Kay (the father of OOP) tell everyone else how bad OOP is and why FP is the answer. It's like watching someone drive a nail straight into their own hand and then complain to everyone that hammer and nails are not the right tool for attaching two pieces of wood together... That instead, the answer is clearly to tie them together with a small piece of string because nobody can get hurt that way.
Just read the manual written by the inventor of the tool.
Alan Kay said "The Big Idea is Messaging"... Yet almost none of the OOP code I read designs their components in such a way that they're "communicating" together... Instead, all the components try to use methods to micromanage each other's internal state... Passing around ridiculously complex instances to each other (clearly a whole object instance is not a message).
In my opinion the argument is more "famous paper X by scientist Y was wrong, let's stop citing it". Except that Clean Code isn't science and doesn't pretend to be.
Toxic. Avoid.
Because of stuff like this:
> Martin's reasoning is rather that a Boolean argument means that a function does more than one thing, which it shouldn't.
Really? Really? Not even for dependency injection? Or, you know, you should duplicate your function into two very similar things to have one with the flag and another one without. Oh but DRY. Sure.
> . He says that an ideal function has zero arguments (but still no side effects??), and that a function with just three arguments is confusing and difficult to test
Again, really?
I find it funny who treats him as a guru. Or maybe that's the right way to treat him, like those self-help gurus with meaningless guidance and whishy-washy feel-good statements.
> 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.
Wow, illumination! Salvation! Right here!
Until, of course you have to actually maintain this and has to chase down 3 or 4 levels of functions deep what is it that the code is actually doing. And think of function signature for every minor thing. And passing all the arguments you need (ignoring that "perfect functions have zero arguments" above - good luck with that)
Again, it sounds like self-help BS and not much more than that.
The art is to chain your short functions like a paragraph, not to nest them a mile deep, where the "shortness" is purely an illusion and the outer ones are doing tons of things by calling the inner ones.
That's a lot harder, though.
But it fits much better with the spirit of "don't have a lot of args for your functions" - if you're making deeply nested calls, you're gonna have to pass all the arguments the inner ones need through the outer ones. Or else do something to obfuscate how much you're passing around (global deps/state, crazy amounts of deep DI, etc...) which doesn't make testing any easier.
I'm not sure dependency injection has anything to do with boolean flags or method args. I think the key point here is that he is a proponent of object oriented programming. I think he touches on dependency injection later in the book, but it's been a while since I've read it. He suggests your dependencies get passed at object initialization, not passed as method options. That let's you mock stuff without needing to make any modifications to the method that uses that dependency easily.
> Until, of course you have to actually maintain this and has to chase down 3 or 4 levels of functions deep what is it that the code is actually doing. And think of function signature for every minor thing. And passing all the arguments you need (ignoring that "perfect functions have zero arguments" above - good luck with that)
I myself find it easier to read and understand simple functions than large ones with multiple indentation levels. Also, it definitely does not make sense to pass many arguments along with those many small functions. He recommends making them object instance properties so that you don't need to do that.
It may not be for everyone, but I'll take reading code that follows his principles instead of code that had no thought about design put into it any day of the week. It's not some dogmatic law that should be followed in all cases, but to me it's a set of pretty great ideas to keep in mind to lay out code that is easy to maintain and test.
That's what the wellness industry do all the time.