There’s No Such Thing as Clean Code
steveonstuff.com
steveonstuff.com
And apart from a few gifted individuals (Rob Pike, Fabrice Bellard Bobby Bingham [ffMpeg team] etc) I'm HIGHLY suspicious of ppl and programmers who claim "they can program correctly" and that "this xyz is the correct way/stack/method/arch".
Background:CS grad, start coding at around 13 (thank you dad !) I am well versed in C,C++,PHP,Go,TypeScript and to a lesser degree Java and a few bits and bobs in between F#,Haskell,LISP...
The above is not to brag (not that it's brag worthy after 25 years you bound to pick up a few tools) just to put it in perspective.
On Code quality: I've noticed it's easy to agree/identify the "extreme cases" the VERY VERY BAD CODING and the very very GOOD CODING. But most of the coders and codebases falls somewhere in between where the code-quality-water quickly gets murky.
PS2. Oh and on 'code reviews': It would be cool if code-reviews were done "anonymously" i.e I should not know until the very end WHO wrote the code. Helps to keep any personal biases at bay - my 2cents.
The best thus far advise I've seen on code-quality guidelines is from a comment on HN:
>I try to optimize my code around reducing state, coupling, complexity and code, in that order. I'm willing to add increased coupling if it makes my code more stateless. I'm willing to make it more complex if it reduces coupling. And I'm willing to duplicate code if it makes the code less complex. Only if it doesn't increase state, coupling or complexity do I dedup code.
Source: https://news.ycombinator.com/item?id=11042400
Sure I might just be a dumb idiot or completely not gifted in the "art or science" of coding. :)
Maybe THAT is the metric to focus on. The value of the code versus the quality ? I honestly don't know the answer ?
But I know how much of my "time and energy (as you get older you released the limiting step is your cognitive-energy not so much your actual wallclock-time) " I spent on "trying" todo code correctly which many times it means, the code doesn't get done and I end up frustrated by not shipping anything.
Working for a boss, the above used to upset me a bit. Now working for myself this REALLY upsets me and my 'income' :)
In these cases, I would DEF liked to have "Fabrice" getting shit-done-coding ability ? Again just my humble opinion :)
Maybe coding is like a marriage... a) "You can be right or you can be happy :)"
Squinting hard enough: b) "You can do things correctly or you can ship code in time that makes a difference ?"
I guess the "better" you are the closer the extremes are from "b". i.e IF you are the theoretical-best-coder-that-ever-lived there is should be no "distance/friction" between "coding correctly AND shipping on time code that has an impact" ??
Writing code for me made me indispensable, then it made me a bottleneck with a stream of uncomfortable distractions in the form of people queuing up for me to answer questions or fix bugs or implement features. It was exhausting, but I was happy enough. That is, until there was a cooler project I wasn’t allowed to transfer to because I was indispensable.
Figure out how to write code so other people can read it. Figure out how to write code so other people can modify it. It looks a bit like clean code, but sometimes isn’t. Occasionally it can be quite different.
> >I try to optimize my code around reducing state, coupling, complexity and code, in that order. I'm willing to add increased coupling if it makes my code more stateless. I'm willing to make it more complex if it reduces coupling. And I'm willing to duplicate code if it makes the code less complex. Only if it doesn't increase state, coupling or complexity do I dedup code.
That's beautiful advice. Something similar that I heard was "flow of data > dependencies > interfaces", in terms of what to worry about. Interesting how they're so similar but from different sources.
But yeah, I second your experiences. Focusing on this stuff, like in code reviews for example, ends up making all other downstream concerns so much easier.
Maybe that is clue they/we are on the right track ? Coming to the similar conclusion from different angles ? :)
This is a generally good idea, but in small teams it can't really work. Code is in that sense a bit like handwriting, everyone has their own style, and after a long enough time in a small team you learn to recognize whose the author of the code just by reading it.
IMO a practical benefit of a routine code review is just a shared understanding. As simple as that. Does the code look to be doing what it says it should do? Do the tests express the intended behavior?
Of course, the style, the quality of code could be of attention too, but that's extras, in my opinion, as long as the functionality is understood and confirmed.
I like relating this to an effort of understanding someone who speaks with an accent, be it a local dialect or foreign-originated. It does require some tolerance, but the shared understanding is the goal.
I've found this to be true in change control as well as any meeting involving people who feel they are compelled to "contribute" even if they don't have anything of value. You don't want to seem like the person who's just attending the meeting and slacking off, so if you're expected to contribute and you can't find any useful critiques, you just pick nits.
> everyone has their own style,
Which then you can asked is that not ALSO a red-orange-flag ? Should one of the metrics not be to be as 'uniform' as possible ? But I do get your point even with uniform guidelines etc, any seasoned def can quickly pick-up who wrote it.
I don't think so; someone's code 'handwriting' always seems to me to be more about how they think about problems than specific syntax conventions.
Everyone working on the same codebase should follow stylistic conventions - where do you put the opening bracket, how do you name functions etc. - but that still leaves a lot of room for personal style/approach. Faced with the same problem, two programmers might solve it in two different-but-valid ways, and that's recognisable.
I think the things I could point to in my coding practice which would make the code I write now better than the code I wrote 10 years ago would be:
- I minimize interdependencies (changing a line of code should not affect something un-related)
- I go for abstractions later, only when I need them, rather than trying to think of the perfect abstraction/design pattern which will solve the problem in the most elegant or clever way
Besides that, I'm a huge fan of disposable code. In my experience 99% of the time, the best approach is to just dive into the problem and try to solve the problem in the most pragmatic way. Maybe that's hard-coding a lot of things. Maybe that's having a small amount of repetition here and there. Maybe it's writing one long function with 1000 lines.
After that it's all about continuous improvement. You spend your time solving the problem, and when your code becomes hard to work on, you spend time improving your code to make it easier to work on by cleaning things up, and adding sensible abstractions which solve the problems you actually have.
In my experience, over time this approach leaves you with a very ergonomic and easy to understand codebase.
For a project we had to write a program that differentiates mathematical equations.
I dove in and just started writing code. Eventually, I realized that I had made a design error and that my code was much more complicated and cumbersome than it needed to be and I was getting stuck due to the complexity of the monstrosity I created.
Unfortunately I figured this fundamental design flaw the night before we had to hand the assignment in and there was no way I could rewrite everything from scratch. I had to push through and put lipstick on this pig as best I could.
In the end, I had spent way more time working on this project than my friends who had spent time upfront designing their programs instead of just jumping in.
This has taught me the lesson to always first try and think things through and come up with some kind of initial design, instead of just jumping in and writing code blindly. Yes you can always refactor, but some early design mistakes can cost you a lot of time, and perhaps make refactoring unfeasible compared to a complete rewrite.
As they say "measure twice, cut once".
If you could delete half your house and re-build it at zero cost, it might be more valuable to just go for the first attempt and learn from it rather than trying to do everything in theory up front.
If you find yourself working on a "monstrosity" maybe you haven't seen the signs soon enough that you need to take a step back and refactor. But in my experience, at least starting on the problem with a POC gives you so much more high quality information that even if you have to scrap and re-write part way in, you're going to reach such a better result than if you try to map the whole thing out first without actually having tried to solve the problem.
I have made similar avoidable mistakes of not thinking it through enough, could have saved me a lot of rewriting, which was pretty expensive
If you're writing a function implementation without thinking about design, well you might be right.
Maybe some people have very small projects compared to what I work on? Or maybe they are talking about the design of a single small component?
I inherited a codebase that needed some refactoring because it was written “to just get it shipped”. If completely fell apart with more users and has taken me a year to get it where it needs to be.
As you need to add functionality, you can opportunistically find ways to improve the codebase toward a better/more maintainable structure.
I have several large codebases I work on which probably get totally rewritten every 18 months or 2 years, however incrementally.
If you have a working project and you think you need to throw it all out and start from scratch probably it's worth a second thought.
The second elephant in the room is job security. People who write baroque code are hard to fire. Nobody wants to invest energy in understanding their private little Bedlam.
Again to the house analogy, if you could just build 3 vestibules to see how they fit with just a little typing, that would be far and away preferable to committing to everything on paper before hand.
Example:
Dendrite[0] was going to be a Rewrite of Synapse (which was a prototype which ended up going into production). The rewrite started more than 5 years ago, had lots of development breaks and it still is nowhere complete or close to replace a existing Synapse instance (which even today is ... Well ... suboptimal software).
The current plans are to support and use both servers long term, because Synapse is already too widespread.
I wouldn't call that cheap.
Isn't your example proof of the benefit of pragmatic coding? Synapse is actually serving users right now. Dendrite sounds like a "better design" which is stuck in purgatory.
https://matrix.org/blog/2020/10/08/dendrite-is-entering-beta
You don't see all the perfectly designed codebases that never went anywhere.
We only talk about legacy code because it's the one that pays the bills.
Which isn't to say that rewriting can't be cheap, but some intentional design (or at least diligent maintenance and refactoring) must have gone into the system to support that style of development. At which point you're back to targeting "quality," even if it's no longer a focus of on the smaller scale.
My definition of "good-quality" code is pretty much exactly "how difficult would this codebase be for a new engineer to understand and modify safely."
Often the worst code comes from prolific people. There’s just so much if it. And if you touch it you will break it at least 1% of the time, so you have to pick your battles when you are trying to keep the ratio under control.
People who only have green field projects as their context are very frustrating in conversations like this. They make suggestions like, well, don’t fuck up in the first place. I don’t know what your history is but that’s the feeling I’m getting.
I could assume you're throwing shade on "prolific programmers" out of some sense of insecurity, but it wouldn't be fair to generalize about strangers on the internet ;)
I just punch up at condescending people. Every discipline has a bunch of armchair people who don’t understand the problem who think “get more exercise” is the response to depressed people or people with chronic fatigue, “eat fewer calories” is the answer to weight issues or diabetes, or “write it right the first time” is a useful response to people trying to solve real world problems.
Kindly let the grownups talk and keep your flash card answers to yourself.
Well one of us is certainly being condescending ;)
And did you really answer twice to the same comment? My goodness I must have really struck a nerve.
For everyone else this is tautological. Good code can continue to be good code.
Code has a lot of really great properties which make it easy to modify in provably safe ways if you know what you're doing.
If the amount of time that it takes to rewrite something is trivial (a week or less) then you aren't working on something all that big/complicated.
I look forward to that day when atoms are commanded like bits!
The hard part is done, but basically we had to switch the engine while the car was running so to speak. In order to switch this out you need to start writing the correct data shape, then switch everything over to reading that shape. When people work strict 9 to 5s this will take you forever especially when managing a large amount of volume, which requires you to be extremely risk-averse and slow.
In a couple of notable cases, that didn’t stop until I removed the last copy. My theory is that certain people were cutting and pasting code from one of the three surviving copies.
If I have to rewrite a bit of my code then them’s the breaks. But I work on a team, sometimes a big team. I don’t have “a house” I have a construction crew that is building many houses and will go on building them. If they’re doing it wrong then I have not only the problem in front of me but five copies elsewhere. And I can’t fix problems N times faster than they are made. And I can’t always sell them on the better technique, even when there are demonstrable problems with theirs.
And of course I don't mean that you should check in code which is a mess. But from the time you start a feature to the time you open a PR, you can go through several iterations of messy code before arriving at a solution which is fit to share with your colleagues.
It is also notoriously difficult to get the design of concurrency primitives correct without thinking things through.
I have no idea why there is a large (or at least vocal) community of developers whose dogma seems to be that thinking things through is a waste of time (though maybe it is just an overreaction to the equally dogmatic clean coders and similar prescriptivists.)
My point is more that coding itself is an excellent tool for probing for solutions. In many cases I think "software design" is over valued, and time spent prototyping is often more valuable than time spent thinking though the problem if you want to arrive at a high quality answer.
100% One of my favorite techniques is "Super Pseudo Code" ! Why SUPER ?
Lol cause the "pseudo code" I write can barley be called "code at all" - It is usually just a text-file with a bunch of loosey-goosey-function-calls and parameters.
You know just to get a "feel" for how different entities(classes,struct,tables or libs - pick your poison) will interact and what might be needed. We not talking any UML-Diagrams here - really just text-files and functions/entities
This also works super-well for any multi-step-processes.
The more code your design decision will affect, the more up-front planning it deserves.
You can't loosely couple around questions like these most of the time, at last not without excessive abstraction.
For "how do I structure this reasonably isolated 0-1kLOC component", I agree, easy to fix later if needed.
And like for example if you want to change web frameworks, that's something you can do incrementally. If you're talking about front-end, just find a way to encapsulate your old code behind a clean interface and start implementing new components in the new framework. If you're talking about back-end, then you can just implement new endpoints in a new language if you want even, and gradually migrate things over when you have to modify them.
Top down and bottom up architecture have their places. Being extreme in favor of one side is usually bad, as almost anything in life.
There is no incrementel rewrite between different stack/heap handling as those are an absolutely central parts of the design, which are pretty much impossible to try to encapsulate, as opposed to the 34th API endpoint. So what it means is that certain domains have much higher essential complexity and at that point the average encapsulation given by OOP/language tools are not sufficient to contain these parts, complexity will triumph and the whole program has to be viewed as one unified whole. Concurrent applications are a similar can of worms.
But it's interesting you mentioned compilers - I'm in the process of writing one, and I very much used an incremental approach.
The first pass I essentially wrote a parser and a component which walked the AST and produced output. At a certain point it was clear that local knowledge of the AST wasn't sufficient to capture non-local details about the program which were required to produce the correct output. I got away for a short time with dirty tricks, but eventually transitioned to a new design: I kept the lexer and parser, and implemented a data driven IR in the style of an ECS system to be built up before emitting output.
So I threw out the initial output component, but I learned a ton by starting with an end-to-end compiler, however incomplete. If I hadn't taken that step, and tried first to plan the perfect IR on paper, I am certain I would have reached an inferior result.
edit: and even the IR and compiler middleware is loosely coupled. The IR is essentially a set of flat data tables, each of which is built independently by walking the AST. And the compiler is implemented in a series of independent passes: i.e. one pass to build the IR, one pass to derive type information etc. so it's very much grown into a series of independent components, each of which could be independently rewritten without affecting the others very much.
I have applied incremental design to concurrent applications and a compiler + stack VM project that runs in embedded environments. You don't go in blind. You do need domain experience and broad strokes knowledge of the conventions. You make some major architectural decisions up front but these don't involve much planning or design. Contrary to your point about CRUD apps, API design is harder to achieve incrementally since it is an interface and requires cross-team (sometimes cross-organizational) iteration. It's still possible, but your organization needs to be equipped for incremental/agile work.
A bit of thinking about design and architecture can save you a lot of time. Start with the wrong data structures and maybe you'll have to patch a lot of thing or just redesign everything from scratch.
Be an architecture astronaut and you may never release whatever you're suppose to develop.
It's all about trade-offs.
and also the reuse of the term architecture.. don't put beams at random and see if it works, plan in advance and calculate. They probably had these realizations thousands of years ago, it's a generic economy principle.
I forgot what the saying is but solution unfold themselves when you thought about the problem long enough.
Oh and lastly, Grothendieck said he wasn't in the business of solving problems, but expressing them.
Yea I understand 100% where you coming from ! I also these days try to focus more on "just getting it done/shipped". Lol I even went so far as to have my own little PHP-MicroFramework called "JGID" which means "Just-Get-It-Done" to remind me at EVERY LEVEL even code that is PERFECT but not shipped is not as good as medicore-core that is shipped ! YMMV
>I think the things I could point to in my coding practice which would make the code I write now better than the code I wrote 10 years ago would be:
I like this approach - instead of asking what is "Good Code" You asking, "How has my code improve in 10 years" a subtle change but I there is a lot of value in there.
This type of introspection-question reminds me of the very very common life-purpose question:
a) What will you do if you have a million dollars ? Sure good question... But I always found the better version of this is:
b)What will you do if you KNOW you won't fail ?
To be honest I think it might have originated from Tim Ferris. You like or dislike him, but what I love about him is his great ability to ask interesting questions.
>- I go for abstractions later, only when I need them, rather than trying to think of the perfect abstraction/design pattern which will solve the problem in the most elegant or clever way
Yup I've noticed this as well. I remember my very first paid coding job. Writing a roulette-wheel-number-reader. And I was so proud to have like a OOP-Hierachy of 3 levels and implementing AbstractFactories for multiple reader-types etc :D Only the "other cases/readers" never happen :)
>..After that it's all about continuous improvement..
Yes - I think I've also seen a similar effect, which is "better code" is a function of repeated-iteration of some process. It's like how woman like men that have a plan. Doesn't even have to be a good plan, just have a damn plan :D
I sort of agree with this, but not quite. I care about code quality, but not code aesthetics. Aesthetics was really important to me when I was younger and more inexperienced. I wanted my code to be pretty, and wouldn't be happy until I found an algorithm or abstraction that met my definitions of pretty code. I would polish it until it was shiny. I would even align my variable names to be pleasing to the eye.
20 years later, I know there are things that will just never quite look neat. FizzBuzz is a pretty accessible example of this phenomenon, but most non-trivial algorithms have that to some extent.
> Besides that, I'm a huge fan of disposable code. In my experience 99% of the time, the best approach is to just dive into the problem and try to solve the problem in the most pragmatic way. Maybe that's hard-coding a lot of things. Maybe that's having a small amount of repetition here and there. Maybe it's writing one long function with 1000 lines.
> After that it's all about continuous improvement. You spend your time solving the problem, and when your code becomes hard to work on, you spend time improving your code to make it easier to work on by cleaning things up, and adding sensible abstractions which solve the problems you actually have.
I too am firmly in the camp of write code first, refactor into a maintainable solution when it works. Maybe it's tacit experience refactoring messy code, sort of knowing in the back of your head how it will shape up when refactored.
But communicating those ideas to less experienced developers is where the problem comes in and all the prescriptive dogma arises.
But this happens in all fields. How many times do experts give some advice they don’t always follow themselves? Experts understand when and how to break the rules, and that comes with experience.
I think a lot of it is just lack of context. A lot of "best practice" advice is valuable in certain cases but it gets over-applied, and people end up wasting a lot of time on things which don't matter.
Novices need simple rules to follow. The world of a novice is filled with uncertainty, they have nearly zero intuition as to what's good or bad, so simple rules that get them 80% there are essential. Otherwise they'd get lost in the complexity.
However, with time, as they gather experience, learn, and mature, they should be able to figure out the reasoning behind the rules they were once given. That then will allow them to make decisions on whether the rule is appropriate for the given context or not.
Of course, getting to that point requires continuous improvement, which is why many, if not most, programmers don't get there.
Example: "Never use `goto` in C." - great advice for a beginner, they'd just make their code into spaghetti. However, a seasoned coder knows that `goto` is only a problem when used to jump to arbitrary points in the code, so using it to reduce duplicated error handling is perfectly fine.
It's funny, but those points you are describing are measures of code quality to me. The less interdependencies the higher I value the code, same for abstractions, most of the time it is the simpler the better. Except in those cases where they are explicitly required.
there's a lot of artistry, poetry, idealism in programming, but as a daily money making activity it just doesn't cut it. You have to reach goals as easily as possible (dry,yagni) fast without painting yourself in corners as much as you can (low coupling, min interdependencies). Finding the right variability points.
it's something school don't do well (saying this humbly) yet crucial
ps: I disagree with the article title. k&r, mccarthy's lisp, prolog, and many other have very clean general and beautiful code.
I only "lead" one project (a minuscule thing by all means) and it was the first time I was responsible, and instead of burning out in rabbitholes, I discovered the bliss of being sharp cause I had near no time to allocate to anything. Suddenly only the minimum amount of change was important and you stop caring for so many parameters. It leads to a saner sense of skills and progress.
You also start to understand systemic ideas better, every patch or change will have your brain think "is this gonna rot fast ? or will it be nicely decoupled from the rest and more pros than cons in the future?". No effort is wasted, every step improves the system. It feels very good, and makes you want to do more of that. Unlike 99% of side projects where you spend days on DESIGN.md or TODO or watching conferences about <trick>
There are many categories of code problem that you don’t have to get right the first time. Some things can grow organically. But there are combinations that can be extremely resistant to change.
In particular, shared state is both state and coupling. Global shared state is a fucking nightmare, and almost made me quit my current job a couple months in.
I thought I could fix it, but I underestimated the number of hands in the code, and the longevity of the alliances I could make to get it done. The thing with a hideous mess is that the people who don’t mind it stick around, and they probably made it in the first place. Mostly what’s kept me here is the pandemic, and he fact that a lot of my energy is now in a hobby, not invested in my job. The tiebreaker was that some self-aware wolves have found slightly better code patterns and I have been exploiting that all I can. Write more like that, delete code like this ASAP. We have spent much of the last 18 months working on things I identified in my first six weeks. Not always in the most healthy way, but at least some people are looking at it.
It worries me that with the leetcode fad in vogue people will pay even less attention to the things which actually matter in real software development.
My approach is slightly different.
I think for a while and then start on the path that gets me to what I think is a common case. If the whole seems more complicated than reasonable, I back out the part that seems to be troublesome.
Eventually, I get one case "working". That case might not be complete, but it does something.
I then work on an adjacent case, using the same "back out the troublesome" process, going into a bit more detail (or perhaps deciding that some detail on the first case was wrong).
I repeat "work on adjacent cases" until most/all cases are handled. As I do more cases, I use opportunities to reduce the cognitive footprint.
Sometimes during code-reviews I find myself wanting to leave 100+ comments. Instead I settle for the few major ones and leave behind most of the minor stuff. I don't want to be perceived as someone who is difficult to work with, so try to pick my battles wisely.
Wonder if anonymous code-review that helps with that.
An anonymous code review might only be to make sure everything technically works (as securely as possible).
But when I was in your shoes, I figured out that some other people would make the same comments I would if they saw them first. The simplest thing to do was to wait or poke them to review the code, then mop up anything they missed. Or I could gamble that they would eventually review it and leave empty space for them to fill.
I think it’s not unlike what the smart kids learn in school: you might know the answer before the question is even out of the teacher’s mouth, but give everybody else in the class a chance before you put your hand up. What’s important is that everyone learn the answer, (and to keep the flow of the lesson, that the question gets answered) not that the answer comes from you.
You can also bury the lede by saying too much. If you complain about eight things how are they to know that #3 is the most problematic?
I'm 42 and have been coding since 14, so I feel I can also chime in :).
I don't think anyone can really "code correctly". Sometimes, the code just falls into place, and you end up with something simple and clean.
Other times, you're struggling, and your gut feelings tells you it doesn't feel right. But your feature needs to be finished, so you mark it as "good enough".
Sometimes when you need to apply changes to some old code, it fits in nicely. That means your original code was pretty spot on. Oh what a wonderful feeling.
Other times, your old code was messy to start with, and this new thing means you have to rewrite a big part of it.
Like I said, coding is party art, where you need to balance a lot of opposing forces.
Don't get me wrong, he's a fucking wizard. He's Mozart when the most talented of us can only ever hope to be Salieri. But part of that is, he dashes off brilliant code without much thought to its maintainability, then leaves for the next project.
>Don't get me wrong, he's a fucking wizard. He's Mozart when the most talented of us can only ever hope to be Salieri. But part of that is, he dashes off brilliant code without much thought to its maintainability, then leaves for the next project.
Absolutely agree's - Well that is "maybe" one unpopular-metric we need to consider as well ? - How much brilliant code can you ship that has a big impact ? I could spent 1 year to "correctly-code some system" but if no one cares or uses it.. does the code or the "better quality" even matter ?? #BigPictureKindOfWay ?
To be clear, I have no idea what is the correct answer here :)
Outside of work in the free and open source community, I find myself often wishing that I could just get a random person who has no context of the codebase to review a piece of code I've written.
Context is obviously important, but the lack of context can also highlight issues that you would be blind to if you have the context.
I would want this to exist within an ide where you could highlight code, submit for review and wait. And if you want to review you can explore pieces of code to review filtered by language within the ide.
There are two types of design methods: those that help you to generate a solution to the problem and those that only help you to optimize a solution, in some dimension, once you have one. Clean Code guidelines clearly belong to the latter.
When you first write code you should not think of Clean Code. It will distract you from solving the problem at hand, while not providing any help in solving it. Only after you have a correct working solution you should consider Clean Code and other optimization guidelines. (Code Complete is much better IMO.)
Knowing when often pays better than knowing how.
At least a third of the time if a promising junior doesn’t “get it” it’s because there’s nothing to “get”. The code I/we wrote doesn’t explain itself well, misses a useful behavior that would make it more obvious how to use it, or drastically simplify a feature. These people, particularly these situations, teach me more than I teach them.
I love your priority list, although I'm scratching my head over the order - not that it is wrong, but I don't know if it is exactly what I would do. There are cases where I will take more state to be (a lot) less complex, for instance. Big surprise that even that priority list is a bit messy ;)
I think the most damning thing about Clean Code is that the author has barely written any software for decades. I mean, I like the guy well enough, and he has some ok stuff to say, but he is far from someone people should listen to like a messiah. Go listen to Carmack or watch Jonathan Blow's code stuff. Far more interesting things there.
Absolutely ! :)
>Go listen to Carmack or watch Jonathan Blow's code stuff. Far more interesting things there.
Oh ja ! I definitely should have added Carmack to that list !!
Could you please give the links?
I recommend the Game Engine one and the Compiler one. Note, these are not high level fluff talks like Uncle Bob gives, but in-depth, really deep thoughts and watching him make complex decisions while he is actually coding.
Carmack has a bunch of talks, here's a starter: https://www.youtube.com/watch?v=dSCBCk4xVa0
Again, he has produced immense amount of software, from Doom to Quake to rocket control tech to Oculus Rift VR devices.
I’ve worked on teams where a competitor could add features faster than us and eventually they pulled ahead and took customers. I’ve been on the other team too. Early lead matters if you planning a quick sale and to run with the money. But if you miss then someone will eat your lunch. If you learn to write less brittle code you can recover.
In my opinion there's nothing wrong with calling code clean, we don't have to analyze every line of code we write. There's just too many things to consider and viewpoints to take, we'd be trapped in analysis paralysis forever, as you can continue it to no end. There's never a point where the analysis will be "done". You just have to be able to recognize when the analysis reaches a point of diminishing returns, and when moving on is the better option. Sometimes just calling it "clean" is enough.
My view is that while there is definitely a subjective aspect to quality—whether in code or in writing—a larger portion is objective, just hard to formalize or quantify. Just because we can't measure something directly or because we can't all agree about it in every instance doesn't mean it's subjective. There are a lot of things we can't know exactly but that are still objective, so why not writing or code quality as well?
Hmmmmmm not sure about that. AFAIK anything that we either can't measure objectively, or don't yet know how to measure objectively, is a subjective measurement, even if that measurement is subject to an objective apparatus (observer effect experiments; the measurement is subject to the circumstances of measurement)
Totally open to being wrong and you being right tho. Any examples come to mind for you?
Like music and other forms of art, different styles appeal differently to different audiences. Many people find the language that 8 year olds write in to be terrible, and some people find it endearing and honest and meaningful. Software is no different in my opinion
Audience always matters
Of course books get 1 major release. So while ability to maintain and change is much more important in software, that is by far not the only factor for clean-ness, and I'd say hardly the most important.
There are different writing styles (and so guide styles associated) for different objectives. Eg, journalism, with a pyramid model designed and refined to ease the process of assimilating information which is different from the playscript style model designed to give general instructions on how to flesh out and enact events.
Both these styles (I intentionally chose for their more obvious rules and usage) have different hard-and-fast rules and applying one instead of the other wouldn't work.
Now, there will be a subjective quality but there are also objective qualities to writings produced with these styles (structure, count of words, choice of wording for description, timeline of events reported, etc.).
Now, of course regarding novels, storytelling, the newyorker, writings that are intended to be read, alone, to tell a story, to convey more than facts and the most objective description of reality yada yada.
And then there are technical manuals. I think code is a technical manual for two kind of audiences: humans and computers, so compromises are made and rules for clarity and brevity can be layout.
I agree with you there - but this makes it even more bizarre that there are now tools (e.g. SonarQube) trying to automate this analysis. Of course, linters are a undeniably useful, but stuffing arbitrary rules into a tool which then gives grades to your code based on a strict interpretation of those rules is a bridge too far. I mean, can you really take software that uses terms like "blocker code smell" seriously?!
If that's what it is then I think such tools are "a good servant but a bad master". It does not replace manual analysis, but can be used well to reduce costs of removing common code smells. It still requires a human to work as a judge.
- the code complies with coding conventions in force
- it is correct
- it is not inefficient
I would really love coworkers stick to these three criteria during reviews.
There's a decent summary here - https://gist.github.com/wojteklu/73c6914cc446146b8b533c0988c...
Yes, if you take them as general guidelines, not dogmatic rules with variable interpretations
The latter is exactly what his fans do
> 2 .Keep it simple stupid
> 2. Prefer polymorphism to if/else or switch/case.
No, not at odds with one another...
Whatever approach is clean or not can vary wildly by language. Or framework.
I do like clean code, but I think the article has a point: it's a very vague concept.
Cyclomatic complexity would put if/else behind polymorphism while using lines of code and depth of inheritance would favour the if/else way.
That's the crux with software - there's not always an objectively better solution in terms readability/comprehensibility and some metrics that try to capture these qualities are inherently at odds with each other.
- you have the same if-then-else in 3 places in code with 3 different effects
- you refactor it to polimorphism (Enum with a few overloaded methods works well in java)
- now one of these places need additional condition so you make it a little more complex
- year passes
- you have eldritch abomination of OOP instead of 3 if-then-elses with slightly different conditions because at no point someone was brave enough to refactor back into if-then-elses as the special cases piled up
And notice how if one needs to add a new action, all objects are required to implement it. In typical OOP languages that touches many files even if the action is used only once.
So polymorphism works nicely if there are only few actions but many different objects, like in a typical GUI. But in cases of few objects and many actions, for example, doing a lot of different queries over few data sets it leads to a lot of boilerplate. Switch over actions will lead to less code.
It has benefits, if you do not apply it dogmatically everywhere
Suppose the program has to make a choice. When the choice exists only in 1 place, use an if/switch. When the choice exists in multiple places, polymorphism centralizes it and quickly becomes the simple option.
For example, a change in law required us to make a choice based on a date:
If (thing.someBusinessDate < 2010-06-01) { doTheOldWay();} else{doTheNewWay();}
after a while, a few edge cases sprang up, and we got variants of this code hiding in multiple corners. Then the law changed again with a new cutoff date. So we adapted to polymorphism: interface TheWay{ edgecase1(); edgecase2();...}
class TheOldWay implements TheWay{...}
class TheNewWay implements TheWay{...}
class TheNewNewWay implements TheWay{...}
TheWay wayDetector(thing){/* all the ifs here */}
Unfortunately, adding just one more 'if' was always the cheapest choice. The refactoring was nevertheless clearly a better choice: it enabled us to point out more forgotten edge cases, avoiding costly mistakes.That said, I think "simple" if/else or switches are much more common than the complex cases that fit polymorphism, so it seems a bit off to have it as a general rule to prefer polymorphism.
I remember going over unclebob github a while back and this wasn't the impression I got. The parts I read seemed like obtuse code that added pointless abstraction for the sake of following some convention.
It's that small changes like adding getters and setters give an easy-to-follow PR, a false sense of accomplishment for both the author and the reviewer, and ton of dopamine with that. While important changes, that actually deliver business value or repay technical debt, become increasingly difficult to do without replacing massive amounts of code and redoing the file structure.
Inertia is the biggest demotivator.
I hate commented out code, but this doesn't work in every situation. While working on a clinical information system, we had frequent "please bring it back" requests that came a few months after things were removed (also by request). Relying on source control is nice, but unless you document every feature/removal or use very extensive commit messages, finding the code that was removed (especially if it was spread over several classes) is easier if you just comment it out and write why it was commented out.
The important part here is to add why something was commented out.
And commented out code gets stale so quickly. Much better to check out an old version where it still worked than uncommenting a bunch of lines that now have compile errors and have become incomprehensible. Happens so easily, e.g. after some variable renames.
You can guess, but that will leave you with many classes/functions commented out only in the hope that one day one of them will be requested to be put back. This leads to nobody daring to remove old code for months/years.
Then, one day, a new coder on the team just ask WTF is this here, realize it's outdated and not running anymore, and will remove the whole thing. Then, the "senior" guy on the team comes shouting at him/her: "We might need it one day, what do you know about our team, bla bla bla, we had a feature 4 years ago that was removed and we had to add back, so now we have the rule of commenting things out". Yuck, what a nightmare.
Just create good commit messages, don't group irrelevant changes together, and you'll be able to find the commit if you really have to. And even if you can't, you know you can still just develop that thing again, right?
This sounds like you're travelling towards burnout at supersonic speed.
Rapidly changing environment/features is just newspeak for customer has no idea what they even want in the first place and project management is non-existent.
But I do wonder, if the better option would not have been to make the behavior that you commented out optional. Sometimes one can know beforehand whether a request is actually what people are going to want in the end. Once I wrote variant 1 of a certain feature. This was deemed not good and variant 2 was written. This was also not what people wanted so variant 3 was written. By then I got the inkling that also variant 3 would not be there to stay so I made it an option whether 2 or 3 was in effect. In the end, the final functionality was mostly a lot like variant 1 so the option turned out not be useful but maybe sometimes such optional behavior can sometimes be the best way....
git log -p -- fileNot exactly relevant to this discussion, but it sounds like a good case for a config flag. Let them turn it on and off as often as they like.
<< I would change "one assert per test" to "one concept per test". Uncle Bob makes a fairly big deal about how the former is a misconception. >>
I found that a lot of those guidelines lead to the exact opposite. Examples:
- Prefer polymorphism to if/else or switch/case (oh, the joy of tracing a simple task through 50 files)
- Use dependency injection (same as above)
- Hide internal structure (that "private" is the default in C++, Java and Rust just adds to boilerplate; the default case is that you want everything public (unless you like writing trivial getters and setters just for the fun of it); legitimate uses of "private" exist, but are rare)
- One assert per test (I guess I need to throw my property-based tests out the window)
Was meant to make a failed test instantly communicate what's wrong with the unit under test. As frameworks evolve and our practices around them change, it's absolutely fine to come up with new rules.
All of uncle Bobs rules come with pages of explanations of what problems they solve. If you don't have those problems, you may not need those solutions. The book is more about the spirit of the law than the letter.
I've read thousands of tests with multiple asserts that were perfectly plain about what was being tested and in many cases the logical sequence made it easier to understand.
Breaking them into separate tests with one assert would simply have meant a shit ton more code to read.
Uncle Bob's "rules" often come with mile long caveats that he seems to be blissfully unaware of. I think I get why he said it - overloaded tests are a thing - but he fucked up trying to turn his observation into a rule.
> Was meant to make a failed test instantly communicate what's wrong with the unit under test. As frameworks evolve and our practices around them change, it's absolutely fine to come up with new rules.
To put a finer point on that then, you're saying the rule is obsolete if your test framework allows assert statements to accept message strings?
Any time a class has invariants to maintain, you probably need private. That could be infrequent in your experience, but overall, invariants are a cornerstone of abstractions.
This is such a strange POV to me. There are cases where your classes are just data records, but any object with logic surely wants to restrict access to its state so it can maintain invariants? E.g. the simplest classes I can think of, like a vector that consists of a pointer to a buffer, a length and a capacity, wouldn't want to provide a setter for the buffer, or allow callers to set it to a random value?
It makes almost no sense to have all private class fields and methods. The only thing I can think of is fat handles with friend classes or functions.
Maybe it's something you could miss if you're coming from a strong C background and still thinking of C++ as C-with-classes, but otherwise it's something any serious user of the language would pick up.
But there are reasons and rationales to "lockdown" the code (beyond not trusting fellow devs!) and at that point I suggest that any mutable state language cannot be properly locked down - so use a functional language where its easier to reason about.
Usually (IMO) private classes etc are just trying to make it easier to reason about state changes - when its probably best to dump the whole mess.
I read that as "one operation per test". Specifically, both:
assertEqual(complex(1, 2) + complex(3, 5),
complex(4, 7));
and: auto c = complex(1, 2) + complex(3, 5);
assertEqual(c.re(), 4);
assertEqual(c.im(), 7);
would both satisfy that requirement, but: assertEqual(complex(1, 2) + complex(3, 5),
complex(4, 7));
assertEqual(complex(1, 2) + complex(-3, -5),
complex(-2, -2));
would not. -- In that case, Clean Code is advocating having something like a "test adding positive complex numbers" and "test adding a positive complex number to a negative one". This makes it clear in the test failure report which of the operations and conditions has failed. assertEqual(c.re(), 4);
assertEqual(c.im(), 7);
This is "logically" one assert, on two lines. So I would welcome itIn cases like this I find myself extracting a helper assert method if it gets used 3 or more times.
e.g.
assertEqualComplex(c, 4, 7);
which contains assertEqual(c.re(), x);
assertEqual(c.im(), y);
Which makes that a bit clearer. This is not the best example but you can get the idea. I might have 5-10 lines in an assertTransactionSuceeded(txn) method.This replaces a clear, well defined thing (assert) with a vague, ill defined thing. What constitutes an "operation"?
There's almost no point to the rule if it isnt trivial to distinguish one operation from a group.
"One assert per test" -> just test one "concept" per test, not literarily 1 assert statement per test. IE :
assert(loc.x == 10.3)
assert(loc.y == 10.4)
assert(loc.z == 10.5)
is absolutely fine (even you could do it in 1 assert).In any case, "concept" being such a loosely defined thing would render the rule a bit pointless.
There is something deeper behind this rule but think he fucked up articulating what it was.
However when you get to larger programs big if ... else blocks become painful. Not having a DI mechanism becomes painful. Encapsulation becomes important because you will find other parts of the program modifying something when it really shouldn't be.
I used a moan about these things until they were taken away and then I really missed them.
Even if you use many files and only put a tiny amount of logic in each of them, you still need to have an understanding of the overall system and how things work together, and scattering things in more places, each with multiple wrappings, just makes that even harder.
I think you forgot one of the most devastating points in Clean Code, namely short functions and DRY (when overdoing it). It leads to the same problem "jumping through 50 files".
My favorite subversive action to write clean code is using jumps. There is no end to how convoluted code people write to avoid the oh so harmful leap.
I feel like much of the need for short functions comes from nesting ifs moving to code more and more to the right. That is easily solved by jumps or early returns. Early returns can lead to too many functions if not doing with modesty, though.
EDIT: Dogmas taking the developer out of his comfort zone is probably the main culprit. Any advice is good, as long as you don't follow it.
Java has the record type. C# has struct and also property members that serve the same purpose.
I bet C++ and Rust also have better options for data records than plain classes.
If this is arduous it may be worthwhile to change your dev environment
In its absence other people take dependencies on internal details which makes code much harder to refactor later. Lack of scoping on visibility increases the surface area of your APIs and increases the likelihood of bugs. Lack of scoping of visibility on state means losing almost all control over preservation of invariants.
Fred Brooks:
> Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won’t usually need your flowcharts; they’ll be obvious.
In that situation, having to sprinkle "public"/"pub" all over them is very noisy and unproductive. It's the worst with Rust's enums, which require you to declare the visibility of variants and enclosed types of variants separately, so you see a couple "pub"-s per line of an enum definition.
That may be an argument for separate keywords for plain data, and for "APIs", although I'm not sure how I feel about it. Underneath it's just coupling of data with functions. Some languages have separate keywords for those, some don't.
I'd say that's only true for objects whose purpose is to merely group semi-related data together for copy/transfer/storage, nothing more. If your object has invariants/rules, it's a completely different story.
>legitimate uses of "private" exist, but are rare
So if you have a domain object with certain invariants (and any serious project has plenty of those), who makes sure its invariants are always upheld? If everyone is allowed to put whatever they want inside your object, it's easy to end up with inconsistent/corrupt data. Ideally, you want all the logic which deals with object mutations and their validation to be concentrated in one single place, and force all clients to use it, to avoid copypasting same code every time you mutate an object (which is error-prone). That single place can be the object itself, in the constructor or the setters, with fields made private (or const) to defend against misuse. Of course, you can validate object state in a separate function and treat your objects as just dumb bags of data, but then we aren't talking about OOP anymore (where the argument "private vs. public" actually makes sense).
I do not deny the usefulness of ADTs (which is what "private" is all about). I question whether it's the right default. My experience tells me it isn't. Whether you call my style of programming "OOP", is irrelevant to me. This term, after decades of evolution, doesn't mean anything anymore. I certainly don't treat it as a badge of quality and don't consult any definition of it before judging whether a particular technique in programming is useful or not.
In Java (or Java like languages [C#]), which is the language they were really written for. There are many languages for which the precepts of Clean Code are not that great a fit.
Also - It's probably time to stop recommending Clean Code - https://qntm.org/clean
I do think this article makes a lot of valid criticisms and it’s well worth a read. I remember being influenced by it early on but then re-read it as I gained more experience. This article sums up what I felt on rereading.
vs
> Needless Repetition.
> Prevent over-configurability,
because you're accepting an infinite range of possible callback methods instead of asking a simple yes/no question.
I don't get why this rule even exists, actually. If you take three boolean arguments, that's not okay, but if you wrap them all into a configuration object and pass that instead, that's suddenly... okay?
Only if you actually call it exponentially many different ways, which seems unlikely.
> I don't get why this rule even exists, actually. If you take three boolean arguments, that's not okay, but if you wrap them all into a configuration object and pass that instead, that's suddenly... okay?
Three or four booleans are not something semantically meaningful that you can reason about. Five or six classes representing the cases that actually occur in your program, that's something you can understand. Often passing a callback function is too.
Boolean flags fall into a hole in the middle somehow, probably because of the exponentially many possible combinations; they look like a finite set of possibilities, but in practice you almost certainly haven't tested all the cases and a lot of them probably don't make sense.
There is no secret to this, it's merely tests, no matter how clean my code is I would not feel confident unless I have tested my changes, whether manually or automatically and manually quickly becomes intractable.
There is the option of formal verification, but I have found the tooling to be disagreeable with me.
I believe that's because the term Clean can be intuitively defined in opposition of quick and dirty : "I have spent a lot time on it. So it isn't quick and dirty. It's clean."
I have worked with people who were super booked up on Go4 and all the design stuff, and yet they can be fairly insufferable to work with when they parrot design advice from all this stuff, precisely because they can't explain why in every given situation. Then, on the receiving end of things, most of the criticism people who are expressing in this forum wrt to uncle bob, do so about examples where they coming from the perspective of something they understand.
I think there is A LOT of value in all of this experience, but it has 0 utility if you don't understand it. In other words, you are much better off not considering how you feel about code at all, and always sticking to doing things because you understand why. (And I'd say 'understand' means in a way that you can explain to someone else who may be skeptical). Context is key... so it's like if you're about to do a live coding demo in front of the board, then yeah, it's ok to comment stuff out - I think that's obvious. Ideally, your outcomes will be much, much better if they're coming from those 'obvious' points of view in any given situation.
Any just to be fair, doing something because you're curious, or it's fun, is just a good a reason as any, it just depends on the context.
This is why, there are other people who I've worked with, who know all that stuff - I'm talking super senior people - and yet rarely ever cite it or even mention it. Usually, they will ask questions because they're patiently trying to get you (or whomever) to see for yourself the same, all-encompassing 'obvious' reason, that overrides all the other important factors in a given context, that they already see. If you ask them why a piece of code that doesn't 'feel right' to you is the way it is, they'll usually have a story about some situation, where they just had to do it that way, and maybe it's bad now but if you were there, and knew what they knew, you would have probably done the same.
So, my point is, yes uncle bob stuff is fairly sound, but no I don't agree that we should follow it if we don't understand it and we're better off almost always sticking to what we do understand. O but also cool analogy about the quick and dirty. I just realized though that if you invert it completely it's like 'slow and clean'.
...for trivial cases. For more complex, real-world scenarios, these kind of rules need to be applied sparingly, and where needed. Otherwise you get things like too much abstraction and several layers of inheritance, making it difficult to figure out what anything actually does - the opposite of nice to read
Uncle Bob et al have some good guidance, but cargo-culting them led to long-lasting, negative effects on some communities, such as Java and C# developers.
The code often tells you a story, it communicates how the programmer (author) described the problem, the solutions and the trade-offs. Very similar to writing.
Maybe it is a vague term. But it is not an excuse to write "bad" code. Perhaps every team need to have a "definition of clean code" for themselves.
This is the crux of it for me. I want to read code not solve code. If I have to "figure out what's going on" then it's not great code.
Am I unlucky or is what you are describing the rare exception?
100% recommend having your own hobby project so you can reach it.
I would say lofty height only possible in following conditions:
A model of reality / problem domain that the company has created themselves that does not have to integrate with any other company, or have to follow any laws or regulations.
For example let us suppose you create a company for users to send messages to each other using your app. You can totally control everything in your environment - just your app, your definition of users, you definition of messages. But once you need to connect your app to other apps doing similar things but differently than your model you are going to hit edge cases, and if you have to think about making your app work on multiple environments and there are problems as there generally are you will start to be less lofty, and then after you have been going a while you try to enter a market with regulations, or regulation is handed down affecting your app.
Things are getting less lofty quickly at that point.
on edit: grammar
But it requires a team that is committed to aggressively refactoring even the smallest of code smells, usually before even committing it. It also requires a team that is dedicated to its professionalism and not bend into a manager's will of refactoring being a time waste.
But when your team is committed to code quality, holy hell is it satisfying, easy and fast to work with. It really is like night and day. If you have not experienced the difference - I'm sorry to say - then you've just not worked with a high-quality team.
Often they were small dev teams in a small company that have high standards and given the trust & freedom to do things.
I think it boils down to how much the programmer actually cares and is given the necessary time to do a good job.
P.S: Been programming only for 6-7 years so can't say if my experience is any indicator.
The cleanest codebases are those where the project has a focused goal and is maintained by a few developers with a low churn rate.
https://github.com/bilibili/vlc/blob/master/modules/video_fi...
https://github.com/bilibili/vlc/blob/master/modules/video_fi...
It sure does.
Unfortunately, the genre is quite often Lovecraftian Horror.
When I interview candidates during recruiting I often ask the open ended question “what is good code for you” as it gets people talking. There is rarely a wrong answer. Usually, there is a difference in answers between more junior and more senior programmers. More senior candidates focus more on the readability & maintainability, while more junior programmers focus on accuracy, speed, following style-guides etc.
For me “good code” is code that is easy to change. Most things follow from that: easier to understand makes it easier to change. Do 1 thing (SRP), makes it easier to change, etc..
I don't mean to be snarky, but if there is rarely a wrong answer to this question, then it doesn't sound like a good interview question to me. Having been through a bunch of interviews, I don't understand the obsession over clean code. I would understand it better if these questions helped pass/fail candidates or rank candidates (to help decide salary or position inside company). But it doesn't seem to be the case. It seems like the feel-good talky-talk over clean code in interviews is nothing but a waste of time (with "rarely a wrong answer").
No worries. I should’ve included it more clearly in my comment. I use the question to gauge seniority and experience to help determine salary proposal.
As I mention, there is typically a difference in the answer of a junior programmer (or someone who’s used to being a solo-dev) vs a senior dev that worked on larger projects with bigger teams.
Moreover, the question is about _good_ code, not clean code.
All of these concepts are attempts to codify practices that worked really well for specific people in specific circumstances. Like Egg Shen, we take what we want and leave the rest.
> Doesn't force you to mentally juggle many balls at the same time.
Coincidentally, this is how I define complexity colloquially for my own purposes. It is extremely general, stupidly practical, and literally rooted in brain chemistry.
Applying this to programming is still a delicate art, since it depends on what the reader of the code wants to do. Most people focus on clean modules (e.g. a 100 LOC unit testable data structure), but that's only helpful when the next person wants to modify or replace a single module, which is generally the easy part. Most times when I'm groking a new code base is spent understanding data flow across a complex hierarchy of modules – usually, the dumber and flatter, the easier this task becomes.
But, I think there are cases where it's quite obvious that one solution is "cleaner" than the other. Sometimes a refactor is just strictly better in all ways.
I once had a service that wrapped a core algorithm in our code, but the code for the server and the code for that algorithm were all jumbled together. It became hard to write tests for the algorithm because the server wanted sockets and a specific protocol and blah blah blah.
So, I simply separated them - the server instantiated the algorithm and called simple functions in it. Tests could do the same - it was strictly better in every sense - it made each part easier to reason about, test, read, understand... And with no real performance difference. "Clean"
The company I worked for had a separate team, kind of a startup, and these guys were all in on discussions how to do things cleanly -- is it a code smell? -- but everything they did was overengineered and borderline useless.
If the things you do to make code cleaner make it more difficult to change things, or even to use it, you are doing it wrong. And I've done things wrong plenty and mostly still do, but I want to improve and what developers usually emphasize does not make things easier.
It has no intrinsic, defined meaning in the context of code.
Saying code is "clean" is like saying food is "tasty"...its a personal opinion, not a defined term.
Maybe: It has no intrinsic, defined meaning in the context of the computing machine.
My subjective experience of grapefruits is that they are yucky. That tells others that I don't like grapefruits, but it tells them nothing about the grapefruit.
It tells them nothing about, what exactly in the interplay between the chemical composition of grapefruit juice, the genetic programming behind my tastebuds, and the personal experiences which shaped my individual perception of "tastyness vs. yuckyness", makes them "yucky" for me.
It's not objective, but there is a consensus among peers.
Honestly, it is like art, but these analogies kind of fall flat since we're not all "artists" in the traditional sense, usually. The reality is, for a painter, there are things that come close to objectively "good" works, a lot more so than for the layperson, and an experienced painter can walk you through all the different "types" of "good" art, just like an experienced coder can walk you through all the different "types" of "good" code.
Just because we can't write down in a few pithy words what "clean code" is doesn't mean it doesn't exist, it just further confirms something we've all known for a long time now; code is art.
Same with "clean code". I think alot of people would agree that a 10000 line program where all the code lives in the main function is not "clean". But that doesn't mean this is what someone means when he says "this isn't clean code".
If this sounds trivial, you may be underestimating the extent to which your fellow developers, and, perhaps even you, casually "spend" code quality to attain something in the short term. It's even little things, like adding the 7th parameter to a function that has become what really ought to be three distinct functions in one (very common problem). Adding that seventh parameter is degrading the quality of the code yet further. Working out what you need to do to extract the three separate functions out correctly, and propagating that back to all callers, is improving the quality of the code.
We like to look at the sweeping refactorings, but a lot of times, it's very little things, as small as "Hey, I understand this variable better now that I've been in the function for the third time this month and it ought to have a better name, let me go ahead and use the IDE rename feature now". The quality of your code is much more determined by the sum total of all those little decisions than the big refactorings.
You also don't have to "maximize" your positive impact at any given point and do all the possible improvements. That's paralyzing. Just make sure every interaction is just a bit positive, and the cumulative impact is huge.
In one organization, I have even had two seniors go back and forth telling me to remove what the other senior told me to put in.
The entire process of defining "clean code" seems painfully arbitrary.
If your best practices are so great, make a sound business case for them. Uncle Bob did no such thing.
From: You
To: Senior1, Senior2, Senior1s_Manager, Senior2s_Manager, Your_Manager
Subject: Duke it out between yourselves
Senior1, you told me to put in Feature1, Feature2, and Feature3. A while later, Senior2 told me to take them out.
Senior2, you told me to put in Feature4, Feature5, and Feature6. A while later, Senior1 told me to take them out.
My job is to implement worthwhile features, not to be an implement in your quarrel over which features are worthwhile to implement. Duke that out between yourselves, and only then give me the ones you can agree on to implement. Thank you.
Your_Manager, I've looked over my job description. It says my job is to implement worthwhile features, not to be an implement in other people's quarrels. Yours says your job is to back me up in situations like this.
Senior1s_Manager and Senior2s_Manager, maybe Senior1's and Senior2's job descriptions need to be clarified with regards to which features each of them is empowered to request or veto.
Well sure, but that's like saying "there's no such thing as beauty" or "there's no such thing as a well-written book". Clearly bullshit. They might be somewhat subjective and have no mathematical definition but they clearly exist.
Try pressing F9.
In response to: > You definitely know it when you see it. the author suggests: > It’s good to build that intuition, but we can’t just stop there. We need to dig deeper beyond those feelings to understand and articulate why we think the code is good.
Maintaining low viscosity (resistance to local change) is a super useful concept for me when thinking about clean code and good architecture.
I think it's a pretty good signifier of elegance, decoupling, lack of spaghetti etc.
1: http://iihm.imag.fr/blanch/ens/2010-2011/M1/EIHM/cours/1998-...
My metric for cleanliness has become simpler over the years: Mean Time To Comprehension. The less time it takes for someone else (or future me) to understand how and why my code does what it does, the cleaner that code is. This is surprisingly accurate and actually useful.
Kids don't like broccoli because they haven't tasted broccoli.
But in some rare cases, that's not what you want. Some functionality may need to require to know exactly what it does, because everything affects each other.
And then i create a "class of mud" that does everything since everything is connected, where the naming conventions of Clean Code still applies.
In my case, it was proxying json request bodies from an api to a 3rd party internal secure network where I had 1 dedicated pc running, where it could fulfill the request over a message broker (Service Bus in this case, to the internet network), execute the request that should work in the internal network and then send the response back through the message broker and give a response in the original API.
That class was shared (and exactly the same) for the server and client. Only one was implementing it as a subscriber and the other one as a listener.
The class was also commented à la SQLite, but no other classes in that code needed any comments - clean code wise.
( fyi, it was an internal secure api, that needed a 3rd part y ( our clients ) network)
I worked in places where code was 100% conformant to the official style guides, but every single change was painfully slow and quite a lot of people complained about working environment. Code was handbook level beautiful though, even if not exactly bug free.
I also worked in feature factory where management was crazy, code was unimaginably inconsistent (same module, multiple files, one file - tabs, other file - spaces, third one - MIXED), but people were mostly chill and happy about pacing and themselves. No one believed in code so it was quite common to double and triple check most of the changes. Environment was very stable due to this.
Eternal dichotomy - code as an art or form of engineering - shows really good here. For some utility, infinite extensibility and minimal code even in exchange for implicitness is cleaner than expressiveness and strictness.
Thus, no code is clean. I could guess Java code isn't clean for Rubyist and likewise. There's a reason we have so many languages available.
//old version of dothing1 the programmer keeps around to copy code from later // which should never be used void dothing1_backup(Args..){...}
is not clean.
People often strive for explicit and complete inclusive definitions for concepts better defined by what they are not. The former is more powerful when it applies, but the latter case is much more common.
If you break it down to its principles, and then explain its advantages and disadvantages for the problem at hand, then it's respectable; but used as a buzzword it should ring alarm bells.
"Coding is generally a team sport. If you’re hacking away on your own then you can do what you want, but when we’re working with a team then we’ve got to discuss our ideas."
My current job has this quasi-team, where we all work from the same backlog, with very little coordination, collaboration, or communication. Everyone just pulls a ticket and runs with it in whatever direction they desire.
I can't stand it. When I raise the issue with my manager, it's just not registering. The turnover and retention is poor, so there are a bunch of new team members, and few people with tenure (like a single dev). The prevailing attitude is that if you reach out to collaborate with someone on your ticket, you're not only wasting the other person's time, but you yourself are incompetent. Because, why can't you just crush tickets all day every day without bothering anyone?
It's silly.
https://bitfieldconsulting.com/golang/crisp-code
https://overreacted.io/goodbye-clean-code/
well, you know, "Warm fuzzies aren't in the spec."
:)
https://www.original.moq.org/forum/michaelmeier/MeetingtheSp...
Clean code minimizes these measurements.
To minimize variable count I can use ternary expressions. To minimize indentation level I think carefully about early returns or extracting blocks of code to functions that do nothing if condition is true (early return).
To minimize grokking time, I revisit old code I've written and measure the time it takes me to understand it. It turns out it correlates quite a lot with line, variable, and token count, indentation level :D
Clean code as commonly defined is far away from any measurable metrics. That's why one might say that it does not exist.
Agree about indentation.
Grokking time, and time to make a correct, meaningful change are good metrics.
It’d be interesting to take some of the examples from Clean Code, and rewrite them procedurally, then functionally, etc, and then test which takes the least amount of time for an average developer to understand and modify.
My own definition of 'clean code' means maintainable (anticipates some broad future requirements) and avoids inventing unnecessary abstractions or adding layers of indirection (which just adds complexity and gives other people more work to familiarize themselves with the code).
"Clean code" primarily means to me descriptive function names and variables, with functions that do one thing, and are not overly long.
It's stuff like not using "temp" as a variable name for "temperature" as it unnecessarily adds to the cognitive burden of understanding the code ("Does this mean temporary or temperature?")
This what I took away from the Uncle Bob book.
I expect some will latch onto the title, however, to keep with traditions, like that somehow variable names are supposed to be abbreviated.
What should have been a quick bug fix (maybe 10 lines of code), turned into weeks and weeks of being told to rename, reorganize, and restructure aspects of the fix and surrounding code in order to adhere to a variety of undocumented conventions.
Despite following patterns used elsewhere in the very same files, my code was deemed brittle, confusing, and poorly tested (we don't do this, we're trying not to do that).
I patiently addressed each item of feedback, but of the dozens and dozens of PR comments, not one was ever about the accuracy of the logic that fixed the bug.
Welcome aboard!
1. Performant
2. Safe
3. Scaleable
4. Easy to delete
---
Performant: I can write you a nice clean bubble sort implementation, which will obviously not be performant.
Safe: You could write a nice clean server which reads input from a network socket and executes it via `system(...)`. This would not be safe.
Scaleable: That's not even properly defined. How would you "scale" my device driver? Or my microcontroller logic? ... does that mean those can never be clean?
Easy to delete: To delete and replace? To delete for building tests for other parts of the code? Didn't even get that.
The answer to this lies in quality of the final product, which is roughly determined by two opposing forces:
1. How simple is it for a team to make changes and continually meet the quality spec.
2. The velocity at which the team can make those changes to meet the product spec.
Anything apart from these, is down to someone's personal preferences (usually in search of trying to invent a DSL mimicking their favourite language).
The interfaces and abstractions needed to make a code testable is something I actively avoid in order to write code that others (including me in a few months) can glance and quickly-ish figure out what is happening and if there’s a big, where could it be.
Now I can intentionally know that is a compromise I’m taking instead of feeling like a failure because my code doesn’t lend itself to testing…
Our system also talks to a bunch of hardware via multiple protocols, for which we’d need to write simulators (so I’ve written but mostly for development and not for testing)
And apart from that, it being an async user facing gui app… well, automated testing is kind of a challenge :)
But you are right, I can think of a few things that can be tested, bigger than methods but smaller than end-to-end testing
I feel like good code is just decoupled code that lends itself to composition. Most writing about code is just about how to achieve that quality, not about identifying all these different and conflicting qualities and finding balance between them.
> “I like solution X. It decouples the error message presentation from the core logic. It’s easier to understand, because you don’t have to consider both at the same time. This separation also unlocks some testability, as we can mock either object whilst testing the other. It does come at the expense of requiring the parent object to inject the dependencies, but that’s a worthwhile tradeoff for the testability.”
Take this passage - is it saying anything other than "I identified two independently meaningful components here where you have one". What else can you do to clean up code other than separate things that don't have to go together?
There is clearly ugly code and clean code.. And generally speaking, ugly code is structured in a non obvious manner that can range from the specific implementation to naming..
Its a spectrum and after a certain point what is ugly/clean is very subjective but this is where semantics kicks in. Other than the occasional heated arguments, most teams can agree on what is ugly vs what is acceptable/subjective.
- New engineer joins a team with a mature codebase
- New engineer complains about code quality, convinces management on a total rewrite to improve code quality
- New codebase starts out simple and elegant
- Eventually the codebase gets just as bulky and convoluted as the old one, because the ugliness was just a reflection of the complexity of the problem space
A lot of times "ugly" is just a stand-in for "not written by me"
I've seen the same thing play out and more often than not, its a helix like cycle of projects starting simple and as time goes on trade offs are made and you get a lot technical debt then you rewrite and the cycle begins again.
I partly agree with you about "ugly=not written by me", I just think that only happens at the edges aka semantics.. Nowadays its pretty easy for teams to come to a consensus on whats clean/ugly vs acceptable/subjective.
Ehhhh. There is absolutely such a thing as clean code. But yes; what there isn't a way to measure code cleanliness (although there's lots of surrogate measures; see every linter) which means there's also no way to render it into a dogma...
...and to (perhaps) stick my foot in it, that's something that gets harder to grok the less important subjective human experience is to you. IMHO, this is part of why Ruby code has an easier time being cleaner (and can reach high levels of "clean") - subjective human experience is baked into the language.
(note: yes, you can absolutely write dumpster fires in Ruby, arguably easier than you can in Java; and you can absolutely write very clean code on Java and any other language)
> I’ve come to the conclusion that often when we describe code as ‘clean’ when we think it’s good but we’re not entirely sure why. It just feels like the right solution.
Yes, absolutely. There's a lot of things in popular wisdom that are signposts for "there's more here if you pay attention".
> absolve you from having to justify that with more concrete rationale.
...Why in the hell do you need absolution here in the first place?! What are you being absolved from?
(shameless quote: justice only matters to the just)
> you don’t really need clean code, you need _____ code
No. That's the trap of Goodhart's Law. You need something outside of your metrics so that your metrics don't become your target.
Well, in several epistemologies the second admission means there's no such a thing (or that, it might as well not exist).
How can you make a metric out of "we aren't really sure what the direction of change for this feature is going to be, so it should be easy to refactor"? and why would that metric be any more likely to fall victim to Goodhart's Law than "the code should be clean"?
But, if you were to just optimize for those metrics, I bet you'd run into issues, whereas if you use those metrics as part of your process for determining if it's easy to refactor, then yeah, I'd expect you to be less vulnerable to Goodhart's Law.
"Easy to refactor" is a goal, not a metric; low cyclomatic complexity is a metric, not a goal. IMO.
Clean code can be repetitive or concise. Readability is important, but only part of cleanliness. When you look at bad or dirty code you can tell immediately, because the logic is loose; it seems to try to handle edge cases at the end of a logic block, or catch errors that lead you to wonder why should this code ever encounter that error if the other code it's referring to is stable? Clean code displays confidence in knowing that edge cases are managed before they get to user functions.
The question is when somebody claims to have clean code, is it really? Why do you say it is clean? Does everybody agree it is "good code"?
It's much the same with "agile". Of course agile is good. Everybody can agree on that. But are people who claim to be agile really agile?
88% of drivers believe they are "above average". My guess would be that the same is true of programmers. Most of them are better than the average. :-)
https://www.adam-campbell.com/post/most-drivers-are-better-t...
"Clean", famously, is measured in WTFs/minute. The fewer WTFs, the cleaner the code; and having a low-WTF code is useful for a programmer to read and modify and be happy.
Apart from that you can only hope to avoid adding too much dirt.
I personally like "clean" as a term for code quality. We've all seen how focusing on easily measurable metrics (like code coverage) can help, but in the hands of the saboteur or the clueless it just becomes a meaningless number. "Clean", instead, focuses on the ultimately human judgement of how to make code which is fit for purpose.
- code that is inconsistent. This causes additional overhead and mental burden when thinking about code. Trying to find what's truly logically different is hard when there are additional differences that shouldn't be there in the first place.
- incomplete code. This might be a concept that was done halfway and then stopped. So there's additional mental burden of, this is what is meant to be done, but it hasn't been done in these places yet, so understand the code is being pulled in two different directions, and also keep that original plan in mind so that you converge those past plans with your current line of work.
- buggy code. Code that looks like it should work but then doesn't causes issues when you can't tell if an error is in something new you're developing or something that existed already.
- code that is hard to accommodate the new change. This one finally gets a bit fuzzier, as the more infinitely flexible you make it, the worse it'll be at matching the task at hand. If you are better at predicting the future, you might be able to set yourself up so that it's slightly easier for anticipated future development, with minimal impact to what you currently need it to accomplish. So this might not be an issue of "unclean" code but just the reality of changing businesses. Of course, it is also possible to just make design decisions here that are objectively bad no matter what the future plans are.
You'll kind of find what is considered "clean" depending on your work place, what your engineering culture is like, what tradeoffs and priorities you have as an org. Basically, what is it that makes my job harder? That's what you then need to "clean" up. e.g. some orgs might say that lack of unit tests makes their job harder, and others might say it makes little difference, so then lack of testing might or might be a "clean" point depending on your org.
That reinforces the author's point that "clean" is synonymous for "good", without any added precision. Try interchanging the two in your comment - it reads exactly the same.
[1] https://en.wikipedia.org/wiki/Clean_(programming_language)
Exactly
The same code snippet might be good in one context and an annoyance in another context
Here's where "generic rules" fail. Like python's avoidance of lambdas and favouring just having functions. Well, but, depending on the context, they make a lot of sense.
"Oh but lambdas are less readable" not if you need 20 different short functions and you need to combine them. Too implicit and it's hard to read, too explicit it gets boring and also hard to read.
Same for anonymous functions. You test the functions that use them and that's usually enough. If not, then that is a good indicator of separating them out into named functions.
> 20 short functions definitely sound as though they should be explicit.
And you are right that it's pretty arbitrary when/if a lambda should specifically be tested or not. But the number of lambdas in a project isn't really a good factor to make that decision - it's individual for each function that contains a lambda.
My solution: array of lambdas
Your solution: array of references to functions named f1 .. f20.
Which is cleaner?
* if anyone not get it, ts/js can declare a key-value lambda like:
let myFunc = {f1: () => , f2: () => } and so on. It can be called by string key like myFunc["f1"](), and all it's props can be get by Object.keys(myFunc) so it can be randomized easily.
That's why you write functions like this:
def testXIsYAfterSettingAToBAndCToD():
A = B;
C = D;
assert(X == Y);
So much meaning provided by the name ;) books
.join(authors, book => book.author, author => author.id)
.filter(([book, author]) => author.lastName === searchText)
.map((book, author) => `The Book ${book.title}, by ${author.fullName()}, has ${book.chapters.count()} chapters, totaling ${book.chapters.sum(chapter => chapter.pages.count())} pages.`);
There are 5 lambda functions in there. Can you tell what the code is doing? Is it correct? Yes, and yes. This is the kind of code that, in my opinion, doesn't need tests at all, nor comments. You can look at it and understand what it's doing and know that it is doing it correctly, as long as it compiles. If it's mission critical, you should test that the join really should be on book.author and author.id, but you need to know the correct answer to write the test, so why not just look at the code and verify it's correct? If your answer is "because another change might break it": no, it won't! Given the preconditions, that code is correct, and no other code can effectively break (and still have your code compile) it without lying to the Type-checker. If someone breaks that code, it's because they're intentionally changing it to do something else, so they'd redline any tests you wrote for it anyway.It sounds like you're suggesting this code should be more like:
/// <verboseDocs purpose="Verbosity">
/// <purpose> Gets the author of a book
/// <returns> The author of the given book
/// <inputs>
/// <input param="book">The book to get the author of
/// </inputs>
/// </verboseDocs>
function getAuthorOfBook(book: Book) {
return book.author;
}
@testMethod()
function canGetAuthorOfBook() {
const mockBook: Book = {
author: 'Test Author',
title: 'Verbosity',
...
};
Assert.areEqual('Test Author', getAuthorOfBook(mockBook);
}
... /// <verboseDocs>
/// <purpose> Takes a last name and returns a function that returns true if the given (Book, Author) tuple contains an author whose last name matches the given string of the outer function.
/// ...
function getBookAuthorPairPredicateFromAuthorLastName(lastName: string) {
return function(pair: [Book, Author])
return pair[1].lastName === lastName;
}
}
And on and on and on, still needing the original code, but just a lot more obfuscated: books
.join(authors, getAuthorOfBook, author => getIdOfAuthor)
.filter(getBookAuthorPairPredicateFromAuthorLastName (searchText))
.map(describeBookAuthorPair)
Now I have no idea what the hell this code is doing and whether it's correct or not. Note that to avoid lambdas in the filter (filtering by a value in the closure) we need to write a function that returns the predicate we want to test on, instead of just sticking the right thing in the right place to start with. All to avoid a single "=>".What circle of hell are we in!?
The context I was talking in mentioned Python not having lambdas. It does have lamdbas (there's even a keyword) so I assumed it meant larger, multi-line lambdas. The code you wrote (glueing together a few mini-lambdas) I would not expect to test independently. I'd feel bad that you can't use SQLAlchemy's even nicer syntax, but I certainly wouldn't expect a test for each of those little things.
Specifically, when we first bang out that initial solution, the code is "dirty". As we refine that approach (better algorithms, adjusting terminology to better accommodate underpaid colleagues), we "clean" it, but the truth is the new code isn't in a "cleaner" state. The little bubble of local opinion might reflect better against said code, but that's hardly an objective measure.
I have a different point of view. I am the person that most often maintains the code that I write. It’s really important that I be able to understand it. Not just at a “tactical” level (This function does this), but at a strategic level (The architecture is designed as a functional abstraction of the model).
My code tends to be quite well-structured, highly documented, and a bitch to understand. I will often employ fairly byzantine architectures, pursuant to testability, usability, and localizability.
Works well, though, and ages nicely. I’ve written APIs that have lasted for 25 years.
What are these metrics you ask? Here's a suggestion: The number of neurons in your brain that light up in response to dissecting a codebase. Because you get frustrated when everything seems important, (something that happens when you dissect hardcore DRY-ish code).
This view will lead to “good enough” code that always appears to “work” but fails easily and creates more work for everyone else.
God forbid you actually care about a project and get stuck working with someone like this on it. You’re going to have some sleepless nights.
@author - "clean code" means improvement relative to some starting point. there, problem solved.
If you care so much about the look of the code, you should have gone into marketing.
Totally agree, these things matter most. Maybe I missed the point of the article if you think that’s what I’m arguing against.
Most unproductive and time wasting people I encountered were "clean code" and "we have to be professional" proponents.
Problem is that it was only "clean code" if it was their way and they refused to read and understand why someone else wrote something differently.
It is not bad code if you don't like it, yes it is pain to understand someone else code, yes it is pain to understand someone else article.
The job of the engineer is to make complexity simple, even when your target market is other engineers.
The complexifiers and verbose Vogons persist heavily today though, as well as project management processes that create an infested myriad of complexity from shallow code to legions of dependencies and asset flipping rather than an effort to be close to the actual standards that run things, abstraction overuse is a major problem.
Simplicity takes a professional to achieve and maintain. Simplicity and simple parts lead to "clean" code.
> make complexity simple, even when your target market is other engineers
including and especially if the target market is other engineers.
Simplicity though is achieved with additional effort. It almost always requires additional effort to make the implementation simpler while having the same outcome.
It's why my personal motto is "Think more, write less". This, however, is not something that's encouraged in software companies. You are supposed to be sitting there and typing non-stop, that's what they call a "software engineering job".
In fact I never call any code "clean" because the term is vague. All that matters is how simple it is and thus how maintainable and performant, though occasionally the two can contradict each other, but most of the time they don't. Say, a complex caching algorithm can improve performance, so that's an exception. But most of the time simple is a synonym of both maintainable and fast. Can't think of anything else that matters, has real practical consequences and is not just hand waving around "common practices".
Writing code isn't really about ticking boxes, it is about communicating ideas to other human beings. And in that sense, a lot of programmers are to code like some people on the autism spectrum are to social interaction. Lack of instinct pushes us to make up precise rules to try to make sense of things. Rules which do not always work because their application is highly context dependent. And when the rules break down and people still insist the rules are good, you get absurd situations.
For instance is composability good? Well, it depends. On paper this might seem like a simple thing to answer. In a given context composability might mean you have to bolt together a lot of moving parts to get anything done. If 99% of the time you bolt the same parts together the same way, you shouldn't explicitly have to do that. Perhaps composability isn't as important as you think in that instance.
Anyone remember early XML infrastructure in Java? You want to parse an XML file and instead you end up instantiating and plugging together what felt like an awful lot of moving parts you neither needed nor wanted to know about just to parse an XML file? Yes, composability can be good. But as in the XML example, people don't always understand when it ends up being crap. (Actually, you can have both composability and convenience, so I am not trying to construct a false dichotomy here, but the original XML infrastructure was so "socially awkward" that it didn't really offer much convenience)
One may think this is mere nuance. One could also say that uttering a sentence and picking the facial expression and tone of voice to go with it is nuance, and that the words you utter are always what matters. We know this isn't so.
What makes code good is hard to describe. So merely trying to express it more precisely isn't enough. Yes you can say "composability: good". But in a given context it can also mean the code becomes less useful for the purpose in the majority of contexts.
I like how the author applies the same idea to “clean”. Rather than just blankety apply clean he suggested expounding on the reason why it’s cleaner.
I think most clean code stuff is snake oil and fortune telling.
So much time gets wasted in pointless discussions and tweaks that will make no difference whatsoever.
The funny thing to me is that the bigger picture; actual logic and bugs, often gets missed while all the attention is focussed on stupid "clean" little pet peeve topics.
It does seem there is shift, more and more articles and opinions like this and I'm here for it!
Clean Code is just a concept, it’s up to you to define it within your culture or community.
I think most developers will have a basic grasp of the difference between clean and ugly code, you just might need to discuss the finer details as you learn.
Because it’s not well defined globally doesn’t mean there’s no such thing or it doesn’t exist.
Our industry features codebases that approach “big balls of mud” as complexity increases. Last week it was called “agile”, this week “clean code”. Perhaps “agile microserviced functionally hexagonal clean code” (AMFHCC™) next?
And I agree with him. Things like SOLID and "Clean Code" really are just buzzwords nowadays that don't mean anything.
I really appreciate his push for simple and efficient, but in reality, the industry has spawned hundred of thousand of software developers these last decades, and collectively we've simulated every possible way of building software. The simple reality is, teams who followed well established patterns and good practices have had more success.
Perhaps. I don't agree with everything he says all the time, however, from my own experience on working on distributed enterprise software I can say that following patterns blindly is an actual problem that impedes a good working solution designed for the specific problem(s) a lot of the time. Devs are pushed by DM's and PO's to have a working product ASAP and what might be a good idea for MVP prototype ends up being a burden down the line because now the entire system is built on top of that decision made in a moment in time without serious thought. Then some time passes and the whole thing crashes down. Then a rewrite happens in the exact same way and the wheel keeps on spinning.
My 0.02c are that I think we should build things more sustainably and expect software to work longer. We should hold ourselves to a higher standard.
kernel's code is terrible mess by java/c# web app standards (dependency injection, interfaces, mockability, testability, etc)
Even C#'s compiler code written in C# is below architect's dream web app code base full of DDD, various patterns, CQRS, Event Sourcing yada yada
The author lists a litany of traits that could be considered "good" things to aim for, but most of them are pretty granular, fuzzy, or superseded by others. The author says:
Words like ‘encapsulated’, ‘testable’, ‘mockable’, ‘reusable’ have meanings that we can all agree on. When we use more specific words that describe the various code traits that affect our project then we can be sure that we’re all on the same page.
Unfortunately, however, we're never really on the same page. Even these more precise terms are very context specific and amorphous. "Testable" may mean granularly accessible, modular, mockable, idempotent, or quick to spin-up for CI, or only a subset of these. Highly dependent on testing approach too (unit, TDD, BDD, E2E...)
This is why I like thinking in terms of tenets: broad concepts that are applicable to everything but enable context-specific meaning. E.g.
- Reliable
- Efficient
- Maintainable
- Usable
With these tenets, for example, we can then *contextualize* and find the "cleanest" code for whatever particular application we're working on. If we want to break these broad tenets down further then we can, and I hold the position that this _is_ very useful: - Reliability: Correctness, Stability, Resilience
- Efficiency: Time, Space, Second-order effects (ecology etc.)
- Maintainability: Adaptability, Familiarity
- Usability: Storytelling, Intuitive, Accessible
Most of these are independently VERY context-specific. Being time-efficient is about the available hardware, interfaces, UX, etc. Being accessible or usable is about WHO is going to use or access your abstractions and underlying implementation.To achieve these tenets, there are many principles and approaches to draw on to help us find the 'clean' solution each time: E.g. The Law of Demeter (LoD), SOLID, The abstraction principle, Functional programming, etc. These exist as tools to help us get closer and closer to the unreachable (yet desirable) goal of "clean".
So, I suppose I agree with the author but only semantically. Cleanliness does not have a fixed definition, but it is still useful to talk about as a goal or absolute to aim for. A bit like all manner of other superlatives.
I persisted in reading it only because the subject matter is compelling but authors should be aware that such typesetting errors can turn people away.
Otherwise if you want clean code you should wipe your monitor.
No, they are rarely at odds with each other. Readable code leads towards performance and safety naturally. Writing readable and simple code means that you understand the problem domain well, and the amount of abstraction needed for tackling the particular problem at hand. Readable code is also much easier to analyse for performance and safety.
Junior frontend engineers coming fresh out of college are typically very ignorant on this front. Ignorant as in they have yet to rediscover this wealth of information through reading books, articles, etc. As well as by messing up a few code bases or by absorbing it from their more senior peers through e.g. code reviews or venting on blogs, twitter, and whatnot. That is because this stuff just isn't taught in a lot of places. Or if it is taught at all, it isn't taught very well typically by people who don't do a lot of coding. I know, because I used to be such a person before I switched to doing some actual software engineering 20+ years ago.
Combined with the arrogance of youth and the tendency to label everyone above 27 as a senior, unclean code is rather common. And even acknowledging it is bad is a problem when you lack the intellectual tools to reason about code quality.
It's not a matter of taste to consider high coupling and low cohesiveness a bad thing. It's easy to see when that is out of wack (long list of imports, 4000 line source files, etc.). It's easy to measure as well if you care too but typically redundant to do so. Likewise, having a high level of indirection would be a violation of e.g. Demeter's law. Likewise deep inheritance hierarchies aren't great (just don't). And that is equally easy to observe. Finally having a large function with insane nesting of loops, ifs, and what not produces a high complexity that you can measure or just look at it an go "gee, this looks rather complicated". It's not that hard to spot bad code if you know what to look for. It's also not that hard to improve such code once you understand why it is bad.
Every bad code base you've ever seen probably has a lot of things that you can objectively point at and say this just isn't very clean because X. It stops being a matter of esthetics when you phrase it like that. And the broken windows theory combined with a level of ignorance/indifference usually leads to more bad code making it into the code base.
There are plenty of studies that will suggest that having a lot of hard to read code does not help maintainability, extensibility, and a few other -ilities. Those are called quality attributes. And yes, you trade them off sometimes. There are yet more studies that point to maintenance as a dominant thing in overall software cost. There are also several standards that have lots of things to say about quality attributes. People developing e.g. embedded software for cars or medical devices tend to take that stuff rather seriously.
But understanding that high cost is bad is relatively uncontroversial. Producing code that isn't very clean leads to higher cost. It's not about the esthetics but about cost. Both current and future.
Sometimes you can trade that cost off. E.g. if you are going to throw away your UI code in 6 months, it's OK to put a few cheap fresh out of college people on that project. It's going to be a dumpster fire in terms of code quality but it might be useful as an MVP and you'll replace it anyway before it gets out of hand. I make tradeoffs like that all the time. And I also use such projects as an opportunity to train up young people quickly. Let them make a few mistakes and then show them how to fix it.