So True !
After 25+ years of coding, I know one thing. I STILL don't know how to "code correctly".
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.
The more code I've written, the less I care about code quality.
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.
Back in university I had an experience that was really instructive.
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.
Yeah this just hasn't been my experience. If you're working on a house you measure twice and cut once because the cost of reworking physical materials is a lot more expensive than the cost of doing a second measurement.
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’ve found people often overlook that while code can be very quickly deleted, gigabytes or terabytes of production data is a huge pain to ETL later. Investing in the data model upfront has huge payoffs for your code and avoiding ETLs later on.
Rewriting is incredibly cheap! And you learn a lot from the failed attempts.
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.
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.
Well starting from scratch for a rewrite is generally a bad idea. I'm more a fan of incremental re-wrties. But I have code bases I have been working on for years with tens of thousands of lines where basically the entire thing gets rewritten ever 18 months or so, a bit at a time.
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.
Are you the only working on the codebase? It may be easy to rewrite your own codebase, but it's certainly not easy to rewrite someone else's. Especially if they haven't been caring about code quality and/or test coverage.
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."
You can go really really fast when you don’t give a shit about consequences.
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.
Nobody in this whole post is talking about loosely coupled code. If you find someone complaining about how hard it is to modify loosely coupled code, you have my permission to fire them.
For everyone else this is tautological. Good code can continue to be good code.
If it were loosely coupled it wouldn’t be an anecdote in this conversation. There is no “I”, there is no “you”. There is only “us”. I can only control Us so much, and I don’t have a time machine.
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.
Not that it matters but I've worked on a mix of green-field projects and mature codebases in various domains with teams of various sizes over more than a decade of professional experience.
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.
If everyone is discussing a problem that you don’t see, then why offer your simple solution except to appear smart? And who needs to appear smart? That’s your insecurity. Not mine.
Idk maybe it's because I did freelancing for quite some time and was often hired to clean up somebody's mess, but I think it's not so bad to rewrite someone else's code. What I normally do is quarantine the old code behind a clean interface, or else start fresh with a better code structure, and then copy and paste the good bits of business logic from the old project into the new one.
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.
That really depends on what you're working on and to what degree it's coupled to the system it's a part of. A form on a web application, or an API endpoint? Sure, rewriting it is probably trivial. A new process scheduler for Linux? The caching system in an HTTP server? Maybe writing the code will be easy (though probably not), but building any confidence that it doesn't break something that's unexpectedly load bearing will be anything but cheap. And if what you're rewriting that started out as "just code and see what happens" it's going to be more expensive still.
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.
How are defining cheep, and how big is the metaphorical house? Developer time is not free (unless you are working a personal project) and if an hour or two up front of planning things out stops you from making a lynch pin mistake that needs to be re-written it is better to do the planning.
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.
Probably not literally without cost, but if the code was written with disposability in mind combined with just a little bit of pre-planning, then rewriting or refactoring should be indeed trivial.
I sometimes wonder if there is a miscommunication. I’m scratching my head sometimes like “how can rewriting/refactoring an entire 10-20k project take negligible time?”
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.
We always talk about time when the elephant in the room is energy. People say we don’t have “time” for that and someone else gets out a calendar and tries to disprove them. Followed by a bunch of backpedaling with other excuses and followed up with foot dragging.
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.
When starting, yes it's easy to replace code, even with a shitty design. If it had to take more than ten hundred thousands lines of code to realize that the design is wrong, then it means the author lacks enough awareness or foresight to plan ahead, and no amount of planning will fix that. Code should be replaced/rewritten if the earliest signs faulty design show up, which should be trivial if the code was and remains "disposable".
Not having to pay for physical materials doesn’t mean there are no costs. Throwing hours, days, or weeks of focused work time out the window does not sound cost free to me or Whoever signs paychecks.
It's not throwing it out the window if it's an iteration toward a better solution. There's a reason nobody does waterfall anymore in software - up-front planning is less productive than rapid prototyping in most cases.
The development of the relational model of databases is an example where thinking things through led to a radically different and superior solution, going in a very different direction than ad-hoc development had produced, or was ever likely to produce. At the time Codd published his seminal paper, there were no implementations of those ideas.
It is also notoriously difficult to get the design of concurrency primitives correct without thinking things through.
Yeah, but >99% code is about using relational DBs and concurrency primitives not developing them. There are somethings that would depend on solid theoretical understanding but most of the software today is much more amenable to trying out things first and reworking failed parts.
In practice, one does not write a single line of code without feeling that it is somehow getting you closer to the desired outcome. If you have the ability to anticipate that it will not contribute to that goal, or that it will create problems on the way, or that there is a better way, even before you have written and tested it, then it would be counterproductive not to do so.
But there's also such a thing as over-planning. If every function you write, you are thinking about 100 different rules about "best practices" you can end up not writing anything at all. Sometimes it's better to accept some level of imperfection first and refine later.
Sure, but your reply to Jcbrand was dismissive of the idea that there is any value to thinking ahead. Jcbrand was not advocating 'thinking about 100 different rules about "best practices"', only that it is useful to try to work out the consequences of the choices you make, in advance of those consequences being revealed to you by failed tests (or failures in use.)
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.)
I mean it's always a bit of a middle ground isn't it? I'm not suggesting that you should literally just sit down at a keyboard and blindly start typing - of course you want to have at least some concept of how you want to approach the problem.
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.
As someone who is currently over a year into rewriting a massive system with a fundamental design error by the original designer I can assure you that failing to plan your data model up front can have huge costs not just for you but for anyone who picks up your code in the future, and can hamstring a system so that it is impossible to extend or evolve.
It's a platform for managing a kind of appointment, but it doesn't have an appointments table. The appointments data is combined with a different table, and there was no way to disentangle them easily because the entire system was built around that object model. Case study in failure to normalize.
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.
I don't know about your experience but refactoring is just not on the menu in most commercial environments I'm familiar with, therefore if you always pick the first solution that cones to find, it is likely you have to leave with the consequences of the hack for a long time (until the system crumbles under its own complexity).
I guess I have been lucky - I've always been able to negotiate for time to refactor if needed, or just find time for it in lulls between tasks. If you're in an environment where engineers don't have the freedom to improve their codebase I would not consider that to be a healthy practice, but that certainly would change the value proposition around upfront design.
I refactor all the time and have done it for 25+ years. The result is solid code that is easy to maintain and close to bug free (no bugs in production the last 5+ years).
Could you describe the process: did you need to justify the time spent on refactoring in any way? ("why known bugs, requested new features should wait until the refactoring is done"--I'm playing devils advocate here. I'm interested, how you justified it before the management if you had one)
I never asked permission to do it. I consider refactoring to be part of my job. Small refactorings I do right away when implementing a new feature. Large ones I split into many small steps and work on for months in-between working on new features. Never underestimate the power of making a small improvement every day. I make sure that the long term refactorings never breaks the system or introduce new bugs. I have solid tests in place making sure changes doesn’t change behaviour.
This is a very self-centered way to think about things. I don’t mean selfish, I mean thinking as a “me” problem instead of an “us” problem.
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.
I want to work with people who I trust to rewrite my code.
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.
Usually you can't delete half your codebase and re-build it at zero cost. The cost of developing a codebase is often the primary cost for software companies.
The cost of the knowledge of that code and the techniques that created is spread in five to fifty other brains. That’s the hard part. You keep finding new copies of patterns you’re trying to remove.
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.
I think this is the kind of thing you learn at uni and then potentially unlearn later on. Over the years I became better at using code to explore problem spaces and as a design tool. Nowadays I feel that incremental design delivers better results in less time than upfront design.
I think your incremental design delivers better results because you already know or at least have a hunch of what wouldn't work and avoid that. You have an abstract architecture when starting and change accordingly on the fly, while programming, using your own best practices.
Top down and bottom up architecture have their places. Being extreme in favor of one side is usually bad, as almost anything in life.
I'm just having trouble understanding what you're talking about. Like what would be a concrete example of how a poor up-front design decision would paint you into an unrecoverable corner?
My experience says that you might not need much design for a typical CRUD app, but try to write a JVM/compiler/database and you will quickly see that a bad design pretty much aborts the given project and you have to start from almost scratch.
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.
All the technologies you mentioned do undergo large component rewrites and refactoring very often. It's true that e.g. the JVM is sometimes hemmed in by decisions from the past. But it is a decades old project and it is not clear that more up front design and deliberation would have future proofed the project for the language and VM conventions of the 2020s.
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.
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.
If you have infinite time to recover, there is no problem, but you could also design something perfect using that infinite time.
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.
>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.
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.
I would argue if “high level design decisions” are getting in the way of coding, this is a sign of over-design or premature abstraction. If you write sufficiently loosely-coupled code, it’s not hard to re-organize later.
Some examples of high-level decisions:
- which web framework?
- which database? which ORM? which transaction isolation level by default?
- will this game/UI be multiplayer?
- what's our testing discipline?
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.
I think there are always ways to minimize coupling. For example if most of the code you write is pure functions operating on values, then swapping out your ORM might be a bit laborious, but it's going to be largely just a matter of typing, not tricky problem solving.
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.
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.
If architects could freely reposition beams in a building, they might take advantage of that fast iteration rather than spending a lot of time calculating up-front.
I think it's antithetical, their profession exist only to avoid moving complicated stuff when it's too late. Of course architect can design wrong and then it's costly too.
> The more code I've written, the less I care about code quality.
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.
People who have been coding for a while learn not to repeat their mistakes. Their code contains good-ish abstractions and other “clean code” features because they intuitively know what they are doing. The code they just wrote to get something done & shipped is probably somewhat “clean” by most standards.
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.
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.
Same with “premature optimization”. Many things can be fixed later. Some can’t or won’t, and those typically get ignored or relabeled as “good design” when it is still ultimately minmaxing cpu time versus developer brain cells.
> communicating those ideas to less experienced developers is where the problem comes in and all the prescriptive dogma arises.
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.
> 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
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.
>The more code I've written, the less I care about code quality.
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
> The more code I've written, the less I care about code quality.
..
- 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
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.
I always felt bad for doing things "the pragmatic way", but your comment made me realize that maybe I'm just not experienced enough to embrace the fact that no code is pretty from the start. Thanks!
Yeah sure! One realization which helped me with this: how many people will be affected by how your code is written, vs what your code does? Given that, where should you put most of your effort?
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.
Yeah I think that there's a bias for programmers to think about artistry, poetry, and idealism in the form of code since that's what we're dealing with all day. But in reality it matters a lot less than we would like to think.
IMO whenever survival (and money, contractual relationships) occur, we will or must go to a different kind of idealism which maximizing the value/effort ratio.
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>
I still care, but the things I fret about have shifted substantially.
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.
I hope in real life you have enough discipline to practice things which people like me would call "code quality". Because taken literally it would lead to all those code bases I have had to maintain - barely a useful test in sight, an empty README.md, no javadocs, classes mixing multiple responsibilities, hundreds of LoCs in one class. Somehow all those things correlate and people who show a modicum of care in one area have others in a reasonable shape too.
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.
This is why I'm like "it works, and has worked for months(years) leave it alone and do better next time" to all the young engineers who like to sling around terms like bit rot (bits do not rot) and "technical debt". Technical debt is when the code becomes hard to comprehend or update, not just because of age or you know a new fancy dancy language or DSL of their own design we should be using. Also get off my lawn lol
> >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.
>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.
Maybe that is clue they/we are on the right track ? Coming to the similar conclusion from different angles ? :)
I've got about a decade more of coding over you and I can confirm the same. The only time you see "clean code" is trivial examples. Get a project that needs to do something complex, and suddenly it becomes not so clean. That's because the real world is messy and complex and solutions are almost always the same.
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.
>The only time you see "clean code" is trivial examples. Get a project that needs to do something complex, and suddenly it becomes not so clean. That's because the real world is messy and complex and solutions are almost always the same.
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 !!
What I see is that the code bases that don’t get cleaner over time either go away, or calcify and the maintainers spend most of their energy on talking people out of changing anything.
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.
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.
> 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.
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.
My biggest problem with code reviews is that they are so often without value. Code review should be a chance for teams to learn from each other, catch issues, and align on coding practices and strategies. In my experience, a lot of the time code reviews are either just a task - where people try to find superficial "mistakes" just to tick a box, or worse a chance for certain colleagues to power trip and try to get other team members to submit for it's own sake.
> ... Code review should be a chance for teams to learn from each other, catch issues, and align on coding practices and strategies.
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.
> where people try to find superficial "mistakes" just to tick a box
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.
Yea true, and not just the code-signature, also the problem or jira-tickets the code is addressing. I.e If i'm reviewing a backend-api ticket I KNOW it's prob NOT the new Frontend-UX hippie we hired :P
> 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.
> Should one of the metrics not be to be as 'uniform' as possible ?
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.
> It would be cool if code-reviews were done "anonymously"
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.
Current team has very low code review participation, and I have a few fights I’m putting most of my energy into so I have to let other things go.
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 don't know, if coding is art, then the reviews need to be more personal. Anonymous art criticism usually isn't useful. You'll have to filter too much noise to get a signal. If you have an art mentor who knows you and your work, their feedback is much more useful.
An anonymous code review might only be to make sure everything technically works (as securely as possible).
I’ve no experience with anonymous reviews, but have had experience with two approaches that help with the minor comments. One is an agreed upon method of identifying potentially minor comments. We call them ‘nits’, with the understanding that they can be dismissed by the code submitter without any changes. The second is the opposite of anonymous reviews where we meet up and have a ‘live’ review. This makes going over any such bits pretty quick and painless, as it avoids the back and forth that text comments sometimes result in. It has the added benefit of being able to discuss the ‘why’ of things more easily, which is especially useful if the reviewer is less familiar with the code involved.
I do the nitpicking reviews but, when I review someone for the first time, I specifically point out that I leave nitpicking reviews and don't block approval on most of them.
> PS2. Oh and on 'code reviews': It would be cool if code-reviews were done "anonymously"
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.
Not only anonymously, they should be published all at once (yes, the risk of duplicate feedback is totally worth it); someone please submit a GitHub feature request ;)
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.
I think there is another big issue related to Clean Code (and friends) that is rarely discussed.
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.)
Great points. I guess I was following some of these rules unknowingly. In one of the project I organized code as one business functionality per package which may be unforgivable sin in "enterprise java best practices". With little duplication it makes so much simpler to see that all business rules, logic handling in just one package. The absolute common thing like database connection management etc is in some common place.
No code survives first contact with the user. Code is messy because of the impedance mismatch between the (mostly) precise world of computers and the real world. So, we do the best we can knowing that 'correct' is always just over the next hill.
I think I’ve learned more about what makes good code by watching people trying to use code than from anything else. I have been doing this since college, when people would hear me helping my roommate finish an assignment so we could go home, and ask more questions. Some people are just out of their element. Some tools are just garbage even though they were written by clever people. Some tools are just garbage because they were written by clever people.
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.
True - who can say ? Hard to argue with a WHOLE career that got "shit done" and WIDELY used by the world ?
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" ??
When I was young I wrote code so I could understand it. Everyone goes faster when the code looks like how they expect it to look. Some of these 10x people are really good, others have mental models that are alien, and so nobody else can touch their stuff because none of it makes a lick of sense to anybody else. These are 3x devs that make everyone else .3x devs.
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.
From What I understand Bellard's code is fairly "dirty". FFmpeg, as I understand it, was hard to modify, maintain, or integrate with.
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.
Yea sorry, I guess I could have picked my "heros" with more care maybe add in a John Carmack ? :)
>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 :)
Coding is a highly subjective and creative endeavor. "Clean code" is akin to "well written" for writers. Sure, you can analyze and even be able to define some good practices, but because we are always creating something new that has never done before, and the field is infinitely complex, no rules can be set in stone and applied across everything.
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.
Writing is a great metaphor: there aren't any hard-and-fast rules and there's certainly a subjective quality to it... but also, some writing is clearly better and some writing is clearly worse. Maybe there is no such thing as "good" writing because writing can be "good" in different ways or because we can never fully define what "good" means, but that doesn't mean that all writing is equal or that "everything is a tradeoff"—and, like you said, it doesn't mean we need to deeply analyze every piece of writing to draw useful conclusions about it.
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?
> some writing is clearly better and some writing is clearly worse
This is a presumption, and one that I don't share
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
> Writing is a great metaphor: there aren't any hard-and-fast rules and there's certainly a subjective quality to it... but also, some writing is clearly better and some writing is clearly worse. Maybe there is no such thing as "good" writing because writing can be "good" in different ways or because we can never fully define what "good" means
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.
> There are a lot of things we can't know exactly but that are still objective
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?
Writing would be a great metaphore only if you write the code alone and when you declared it finished, you never need to come back and add new things/remove old things or fix bugs in it.
I don't see how that invalidates the metaphor. For any version of code you can evaluate clean-ness. Writing is also not immutable, professional authors make hundreds of versions before publishing.
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.
I'm not giving up on clean code, but at the last 5 years I pursuit separation of pure function and unpure function more than clean code. It makes easier to debug / unit tests.
> 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".
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?!
I have no experience with SonarQube. I guess it's like a linter that's slightly higher level? In that case I'd guess the appeal is that it's reducing the cost of often repeated analysis that is usually done manually, not that the tool itself would make your code great to read.
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.
Like writing, code gets better with mulling over most parts (for a long time) and rewriting a lot after insights were properly formed. When you start something, you are exploring; how can you expect that that code will be anywhere close to an ideal situation that time? People say refactoring but in my opinion that is not radical enough; you will be able to redo problems you encountered by maybe completely re-architecting the solution. This luxury is not really a thing many people can afford ofcourse; we just make stuff work well enough to get paid. And it shows.
an important thing to realize with this analogy is the difference in purpose between code and writing. Writing is meant to be read whereas code is meant to specify a program. For people who read code like writing this makes sense but when the focus is more on the resulting program the concerns change a lot.
The Uncle Bob Martin definition of "clean code" from his book "Clean Code: A Handbook of Agile Software Craftsmanship" is a set of rules that absolutely are not at odds with one another. If you follow them you will end up with code that's really nice to read and easier to maintain, and, most importantly, that you can confidently change.
> If you follow them you will end up with code that's really nice to read and easier to maintain, and, most importantly, that you can confidently change.
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)
> 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)
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?
This is where Python's "consenting adults" idea comes in. Why the hell should someone change a vector "mid-flight" - that is a huge code smell and should be dealt with in the review / design / discussion / pub.
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.
In most situations, no you can not trust fellow devs to do the right thing and if you push for those reviews then you're the asshole and lose the office politics game. So yeah, I'd rather guardrail people into good behavior.
IME I write about 10-20 as many data records as "classes". I do use "private" from time to time to protect invariants. But if the default was switched to public, my code on balance would be a lot shorter and readable. I'm not saying that "private" shouldn't exist. I'm saying it's a poor default.
In c++ a class declared with the keyword 'class' is private by default, and a class declared with the keyword 'struct' is public by default. So if you want public by default just write 'struct' (somehow this is not commonly known).
I think the “somehow” is not too hard to guess… it is easy to think that “class” is the only way to declare a class, especially since “struct” is taken from C. I’m sure very few intro C++ explanations ever mention “struct” as a way to declare a class.
There's quite a lot about C++ which wouldn't be covered in a simple tutorial, but the meaning of class/struct is a pretty basic part of the language, hardly obscure.
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.
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?
> 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
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.
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.
These are the points that I've come to despise from Clean Code. After I read that book I wrote code that tried to follow all those rules. It didn't seem bad at the time, but returning to code written that way has always sucked hard. You have to jump around far too much with the 7 line function rule. And over-DRY code can actually be harder to change since repetition is often incidental. In that case a change in one place will actually affect more than its supposed to.
Could you provide an example of how you would restructure some nested ifs to jumps? I can kind of picture it but can also imagine doing it badly and being in pain.
These are minor inconveniences relative to the massive benefits all these techniques bring to your codebase. "Go to implementation" shortcut is a thing and solves the first 2.
"One assert per test" -> just test one "concept" per test, not literarily 1 assert statement per test. IE :
I agree, these rules make the code extremely verbose and over abstracted and scattered. The resulting code base will be several times the size of a straight forward solution, and this has a very big negative impact on maintainability.
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.
>the default case is that you want everything public
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've partially responded to that already[1][2]. Let me add to that, that when you group together data, a lot of invariants are preserved by the logic you already wrote for the types you compose your types of. Most basic grouping of objects is a struct. It's a product type. Often what I want really is the product of all the states of the component types, without eliminating any combinations. And in the case of sum types: I practically always want all the states. All the invariants are already handled in the types being summed. Not for transfer/storage, but because it logically makes sense. Another thing is that "make invalid state unrepresentable" is a good guideline, but it can lead you into a corner, where you write a lot of code just to make sure that your type system proves that a certain error is not representable by a type, when this error is trivial, unlikely and would take seconds to debug at runtime (maybe would even be so obvious, as to not need any debugging at all). Just because a certain invariant is true in a domain, doesn't mean that it needs to be encoded in the type system. We need to choose what's the most productive use of our time as developers. Those are trade-offs.
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.
Private - or visibility scoping generally - is absolutely essential when you program in an environment with multiple teams.
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.
Because most things in healthy programs aren't APIs, but just plain data: records, sum types and composites thereof (arrays, stacks, queues, whatever).
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.
In simple cases many of the suggestions in the clean code book make things more complicated.
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.
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.
This is "logically" one assert, on two lines. So I would welcome it
In 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.
Your interpretation is a saner way to write tests, but I saw no indication that's what he meant. If it was what he meant, then he should have made it clearer. It's not like he couldn't have added an extra couple pages with an explanation and examples.
Also if you look at the code samples he provides they often don't follow his own rules. (At least that was the case when I read the book, don't know if it's been revised since)
IMO it's generally good advice to be wary of rolling too many disjoint use cases into one abstraction, and I think that's the rationale behind recommending polymorphism, but the ergonomics of actually writing OO code make it impractical advice to try to follow as a rule. Inversion-of-control is often a more practical way to solve the same problem, in my experience
Unfortunately, Rust doesn't. Rust only has "struct"-s, no classes, and they're private by default. Enums require the "pub" keyword once before a variant name, and once inside tuple associated with the variant. So often it's two "pub" keywords per line in an enum definition. So after writing code for a while, I then, upon compilation, need to fix up tens of "pub" occurrences. Pretty annoying. Wasted time, and the end result is hideous.
I can hit F12 or Ctrl-Click to navigate to definitions and I get a back button to return to where I was across those navigation operations. Yet it still sucks to bounce through all those files. And it's usually not just a matter of drilling down through the calls, but also moving back up to find out how a dependency of a class that I've ended up in is initialized.
>If you follow them you will end up with code that's really nice to read and easier to maintain, and, most importantly, that you can confidently change.
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.
This is my experience as well. It takes a long time to grok what the program does and how it does it because of all the layers of indirection, even when the program is fairly simple. While this style might make small changes easy if you understand the code, the inertia to larger changes because of the layers of indirection is immense.
Wow that's on-point, agree that in many past projects "inertia to larger changes" is exactly how I would describe so-called Clean Code.
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.
>If you follow them you will end up with code that's really nice to read and easier to maintain, and, most importantly, that you can confidently change.
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.
Was just about to comment with this link. A useful counterpoint whenever Clean Code comes up.
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.
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.
Seems garbage advice to me. Now you've polluted your production code, likely forever. I don't see a situation where using an issue tracker + good commits - e.g. 1 commit for the 1 PR linked to the ticket that removes the feature - won't result in a superior situation from a maintenance and traceability perspective.
Yes, I agree with you. Another thing is how do you know which of the removed features will be asked to be put back? You can't really know.
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?
One solution might be to create a separate branch with the feature still available. With time, you kind of get the gut feeling of what could be useful later and what could not.
I thought the best practice for "keeping stuff around just in case" was an attic/ directory in the source repo. That way you're still benefiting from version control, but it's also clear that the code is totally untested and might bitrot at any time.
Sure, in a normal world. But with 2 devs in team, handling a clinical information system in very rapidly changing environment (new features), there is no time to document things in this way.
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.
In my experience you absolutely SHOULD document every request and removal in e.g. an issue tracker. It's very valuable information.
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.
Yes, sometimes (not often) commented out code can be defended. The point of adding why it is commented out is very well taken. Then you can remove it if the reason for it still being there is not longer applicable. And commented out code tends to go stale quite quickly so if it really is not needed anymore it should be removed.
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....
I have started to prefix my actual comments with another character after the comment character, e.g., #: in Python. Then I know which comments are “real,” and which are just garbage code that can safely be ignored/deleted.
> If you follow them you will end up with code that's really nice to read and easier to maintain, and, most importantly, that you can confidently change
...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.
On the internet I have very rarely encountered "Clean Code" as code following explicitly the rules listed by Bob Martin in his books. People use it like the OP says - "code that feels good to me in the moment, without being able to explain why".
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'm all for the following the spirit of things, but I also want to take a minute to disagree with the "without being able to explain why" part.
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'.
Yeah I agree with almost all of these. IMO the most important is “keep it simple”. That is always always my coding North Star and it’s served me well over the many years I’ve been programming.
+1 when I write code I always think about how my reviewers would perceive it before sending it over and I refactor based on that. You should aim to write code that's easy for others to follow.
Yeah, polymorphism is in no way simple in lots of languages/frameworks. It's actually quite complex in many common languages/frameworks. This is absolutely contradictory in my opinion, but I welcome disagreement :)
And then there's a language like Scala where switch/case is so naturally integrated that it actually becomes the cleaner solution and you basically have to use it.
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.
I see polymorphism vs if/... more as a tradeoff. For example:
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.
I agree with you, especially in instances where that decision logic is unrelated to the data that is actually being used. In your example you'd have to pass "thing" everywhere just for that if-statement, but it might not be used for anything else.
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.
They aren't. This is why pattern matching and typeclasses (to use the Haskell words, though I know other words are in use as well) have become increasingly popular features in languages.
Polymorphism is more complex than a switch/case in a single instance. Across a large codebase using polymorphism instead of hundreds of different switch/cases is simpler, especially when you need to change something. Encapsulating what a class should do within the class instead of in an external code block is much simpler when you're working on something big.
Polymorphism is a switch, but it is the switch over object kind, not action. In fact if there are only few subclasses compilers for some languages where a global analysis is possible implement polymorphic calls as switches over the object tag.
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.
- 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
Exactly my experience with polymorphism. It starts out looking super clean but then the business requirements shift and each has its own edge cases that turn it into a giant mess, often ending up as a mix of both polymorphism and convoluted if statements.
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.
Make the loop operation something that's passed into the method. Or maybe replace the flag with polymorphism if there's some logical value that it really belongs on.
The second one is impractical because the number of classes you'd need would increase exponentially with each added flag. The first one clashes with
> 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?
> the number of classes you'd need would increase exponentially with each added flag.
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.
If your code is complex you can't ever be sure that you're testing the right things, or that there isn't a codepath that your tests miss. Tests are very useful, but they're not a replacement for simple code.
Bob himself has mentioned that many of the rules are contradictory to one another. I can't remember what examples he gave. I think it was in his video series.
Clean code is code that does what you expect it to do without many surprises. It is simple, not clever. Effortless to follow. Each part handles one idea at a time, at the same abstraction level. Doesn't force you to mentally juggle many balls at the same time.
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.
An entirely subjective measure. I want to solve problems, not read code. I can read your code and understand it, but then still have to solve it to understand what problem you are solving.
In my work history, I've never come across code like this, especially "effortless to follow". All the codebases I've worked with have been head scratch causing balls of mud.
Am I unlucky or is what you are describing the rare exception?
There definitely are codebases that are easy to follow.
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.
Most corporate codebases I've encountered are, to put it mildly, unpleasant. In most cases, you've got dozens of people with varying skill and style maintaining the stuff over a decade or more. New business requirements are patched on top, without ever considering the whole architecture.
The cleanest codebases are those where the project has a focused goal and is maintained by a few developers with a low churn rate.
Same even for code base my only job was to cleanup, it would be clean 1 week then split back into mess, sometimes by my own doing. The problem is always the same: there is money for result and result is time-sensitive, no money for form and form takes time. If I have to sacrifice a bit of form to reach a result on time, I'm afraid I'll do it rather than fire a colleague because we can't pay him and do beautiful code :s
Most large software projects have some bad code in it. If it’s all bad, then there is probably an architectural problem, poor code review practices, and/or corners are being cut to meet deadlines.
>but that lofty height of code described is an extreme rarity.
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.
> 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.
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..
> 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.
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.
What you can easily follow though is largely a function of your programming background. An experienced Haskell programmer will have no issue following all kinds of “.” and “$” usages, but one who is not experienced with Haskell, even if they learn what the syntax means, will have trouble following it.
I think how clearly you write and comment can matter just as much. I'm proud to say I've had situations where people with near zero programing experience were able to make some quick changes to get programs I'd written in perl working again following sudden changes. I think the more you know the easier it gets, but a reasonably capable end user with little to no experience in programing can surprise you if you take the time to try keeping things as easy to follow as possible. I do it because I can't trust myself to remember what I was doing even with projects completed only a few months ago.
> Perhaps every team need to have a "definition of clean code" for themselves.
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.
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"
After suffering in the coding world for a bit now, the only clean code I need is the code I can easily change or use. Code formatting, style, naming, but even architecture and all forms of best practice cargo culting I do not care at all, if it does not make things easier.
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.
The fact that everyone can come up with his own definition of what "clean" is supposed to mean regarding code, tells us something very important about it:
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.
But there is agreed-upon tasty food, so there is some kind of intrinsic, shared meaning of the word, which is also true of code.
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.
Yes we do, but "this tastes terrible" still doesn't tell me what that means without additional context. It could be terrible because its spoiled. It could be terrible because the person doesn't like that food.
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".
Strong disagree. It's defined meaning in the context of code is a subjective experience of the coder of "clean", which carries elements of "satisfying", among other things.
Maybe: It has no intrinsic, defined meaning in the context of the computing machine.
> is a subjective experience of the coder of "clean"
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.
I've taken to looking at the first derivative of code quality rather than the zeroth. I don't necessarily sit and worry about the absolute, current quality of my code; rather, I strive to ensure that every time I do something, I'm generally improving the quality of the code rather than degrading it.
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 my 5th role where I write code, and I have dealt with so many different approaches and definitions of "best practice" that at this point I just roll over to whatever the generally agreed approach is at my current org and mimic it as best as possible.
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.
> 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.
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.
Similarly, the existence of so many subjective and unarticulated views on "code correctness" has left me in a place where the only certainty is: there is no certainty.
If your best practices are so great, make a sound business case for them. Uncle Bob did no such thing.
Theres definitely clean code, theres just no rule book for how to define it or course you can teach for how to write it. Some people are just better at the art of writing simple software. You definitely know it when you see it.
The author isn't arguing against the existence of "good" code or "clean" code but rather advocating for us to try to articulate what is good (or bad) about code because "clean" is vague and encompasses too many things.
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.
I agree. The article (well the little I read of its unreadably thin font) doesn't even really argue that there's no such thing as clean code, just that it's not very well defined.
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.
I think many of the qualities mentioned have nothing to do with “clean” code (like performance).
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.
So many developers say "clean code" when they mean "familiar code" and "familiar coding patterns". One person's clean code is another's heap of spaghetti. Idioms (like Duff's device, assignments in conditionals, function chaining) can be clean code if you apply it appropriately and your developers recognize it. Strict class hierarchies can be dirty code if you need to move gigabits in real time with 10 µs latency.
Kids don't like broccoli because they haven't tasted broccoli.
I really like "Clean Code", it's because other people/programmers could easily navigate the code, while not knowing everything that happens in the code.
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 agree - "clean" or "good" are the same type of keywords like "professional" or "specialist".
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.
And yet, everyone will agree:
void dothing1(Args..){...}
//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.
"Clean code" is just a buzzword that people use in interviews, on blogposts aimed at employers, and to make programmers believe they are good programmers, since they already bought the koolaid.
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.
The quote from this article that really hit home for me, was this:
"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?
Since others are reflecting on the merits of the article in ways I agree with, here's some articles I've collected from recent discussions on "clean code". Some even try to ditch the term or argue against it, others give context to the meaning. Would love to see others if anyone has any!
I like to measure things. Like token count, line count, variable count, method parameter count, indentation level and grokking time.
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.
Sometimes, (maybe the majority of times) a five liner is clearer than a one liner. I think line count is a terrible metric to use.
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.
The title is click-bait but the conclusion makes sense. It's just unfortunate that most people have such different ideas about what 'clean code' means. I guess people will ascribe that label to any code which they produce. Everyone (regardless of actual competence) wants to think of themselves as a good developer who writes clean code.
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.
I once joined a new company and got stuck in the middle of a PR from hell.
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.
From the list at the link, I would argue that the following are mostly orthogonal of, and certainly not requirements from, clean code:
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.
> Hopefully I can convince you that you don’t really need clean code, you need _____ code. It’s up to you to fill in that blank with words that describe what your project requires.
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 post showed me why my code is praised an understandable but I have the absolute worst time when it comes to testing it!
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…
Well, not lending itself to testing really is a big problem. But one does not need to sacrifice much in terms of understandability to make code testable. For instance, one does not always need an interface. You could also just inherit from the thing that you want to mock. Another problem that might be going on here is that you might be trying to test on a level that is too low. I think it is in many cases not a good idea to test individual classes and methods. Often it is better to test some small number of classes working together. The problem with testing one single class or method is that you are likely testing implementation details that are very much subject to change and also that at some point one is testing trivial things like 'is the standard library of my language still capable of adding items to a container'. When testing at a bit of a higher level one can write tests that are about properties of the software that are actually valued by the customer. These are much more likely to be stable. If one tests at a level that is a bit higher, one also needs to mock fewer things so the interface thing is also less likely to be much of a problem.
That makes sense a lot of sense. I guess the thing is that golang and their are stdlib testing lends itself to unit testing and not so much to integration 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 don't know, is it more precise to say words like "encapsulated", "testable", "mockable" and "reusable"? Aren't these all essentially the same thing? Suppose you have a class that is technically testable, because you can control all of its inputs, but it has ten thousand methods. Is it really testable if it's so poorly encapsulated?
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.
Disagree. I feel like I have seen this play out a number of times in my career:
- 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 think your mistaking technical debt for code quality.
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.
I partially agree that there is a cyclical aspect to code rewrites and cruft accumulation. I still think a lot of time the perception that code is poorly written gets applied to complexity which is not yet understood rather than objectively poorly written code.
> There’s no such thing as clean code.
> ‘Clean’ isn’t a measure of anything useful. Code can’t be clean simply because ‘clean’ doesn’t describe anything about code.
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.
> 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.
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"?
"Easy to refactor", AFAIK, can be approximated via LoC within functions and classes; dimensionality of functions; cyclomatic complexity; etc. Those are the metrics; you can produce reasonable and grounded numerical measurements of them.
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.
I'm glad he brings up the much-maligned Singleton pattern as an object for debate - or beauty being in the eye of the beholder. "Clean" to me involves a range of classes and singletons (or better, all static code) in as close an approximation to use as they're a actually used in the final code. If something happens once only in your code, it's dumb to make it a class instance. If it happens twice but you know you will never need it again, same. If it happens repeatedly, refactor.
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.
Preach it. I feel like you took the words right out of my mouth. I left my last job because I couldn’t keep sitting and listening to the bullshit about how everything had to be built the same, citing “uncle Bob” every.single.day… coerced into long, drawn out PRs that oscillated between “I don’t like how much you’re changing, can you break it up?” to “what’s the reason for this change?” — lol you literally asked me to break it up. I dunno wish them the best, but god that place was a drag. I knew it was a red flag when I logged into the rabbitmq cluster and immediately notices there was no TLS and when I tried to talk about it I got waved off and asked what was assigned to me, 3 months later I open a 2 line PR and the shadow architect goes “wait, we’re not using TLS?” “Nope” “did you mention this before?” “Yep” SMH
"Clean code" means about the same as "well-written code". Both have meaning. But the issue is that of course everybody and their uncle can agree that "clean" is "good".
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. :-)
> Clean’ isn’t a measure of anything useful. Code can’t be clean simply because ‘clean’ doesn’t describe anything about code.
"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.
Just because "clean" connects to a lot of concepts doesn't mean "clean" code is meaningless, any more than clean dishes. A plate smeared with tar is not clean, no matter if the tar is sterile, and a plate which has been sprayed with salmonella isn't clean, even if you can't see the contaminant. These are different dimensions of "clean", but still very much meaningful.
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 needs to be able to do what you need it to do. So anything that makes that job harder, could be considered not "clean". That might include...
- 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.
> Code needs to be able to do what you need it to do. So anything that makes that job harder, could be considered not "clean".
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.
Hmm, i don't think they're directly interchangeable. I think of clean as specifically what impacts your ability to write new code. If the code performs horribly, doesn't scale, has lots of security holes, that could still be considered "clean", if you are easily able to implement all the changes you want.
> But these traits are in some ways at odds with each other. The most simple code is probably not the most testable. All those interfaces and injected dependencies make for convenient testing, but have a cost in terms of simplicity.
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.
20 short functions definitely sound as though they should be explicit. Named, documented, testable. 1 or 2 you could get away with being implicit. 20 requires a lot of understanding as to what's going on!
Do you test every line of code you write separately? Probably not. You test a function that has 5 lines of code.
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.
This is the wrong way to think about it, though. A function encapsulates some behaviour, regardless of how short or long it is. You don't test each line (directly); you test each function's mode of operation. So a function with one if statement in it potentially needs two (happy path) tests.
I don't think we disagree. It's just that a lambda doesn't change this. If the lambda itself also contains a branch, then yeah, you should probably test the outer function with at least 2 inputs.
I agree, except I think that a lamdba is an arbitrary line to draw for that as well. Why not stop at the main function and give it a load of inputs? A five line lamdba looks a lot like a named function, just harder to test, reuse, and debug in a stack trace.
> 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.
Well, I more meant that if they're "short" as opposed to "20 character one-liners" then it sounds as though they should be tested, whether or not they're defined using lambda syntax.
So here's some TypeScript code I just made up, with a lot of lambdas. It's somewhat typical of code I write all the time.
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:
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 "=>".
I appreciate the large amount of effort you went to to get your point across; that helps with actual discussion, which is awesome.
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.
Not that I am taking a stance here, but you didn't really engage with the GP saying that having functions instead of lambdas allows one to name, test and document them: in fact, they might not be called f1 to f20, in fact they might have docstrings (I guess one could comment lambdas, but see it rarely) and you couldn't test lambdas unless you assign them, i.e. give them a name i.e. turn them into functions in all but name.
* 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.
There is no such thing as "clean code", I think its a verb, not a noun. Same problem as being virtuous, there is no such state. But you can attempt to do things well, make moral choices, etc.
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.
> If you’re hacking away on your own then you can do what you want
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.
In any case, developers should get out of their nerd bubbles a little more. YES there are metrics that exist outside our field, but no, we gotta follow these outdated gurus who havent coded in a while, fetish-size complexity, come from a background incomprehensible to you and still stuck in old ways.
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).
Wow, this post has triggered some emotion from me. I find it incredibly traumatic working with “engineers” that have this point of view. The pain individuals like this inflict on others is real! What they are really saying is that they don’t care about their fellow developers, their productivity nor happiness.
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.
I don't see how you can read the article and come away with this opinion. The author's not saying that you shouldn't make quality code, he's saying that no one agrees on the definition of clean so you should use more descriptive language.
As one of those very harmful devs you're referring to, I think you have it backwards. From my perspective, anyone who lionizes linting and arbitrary practices as "best" is actively harmful to whatever mission my team may have. Readability matters; maintainability matters; functionality matters more. The longer you talk about proper tab spacing, the less time you're talking about algorithm optimization.
If you care so much about the look of the code, you should have gone into marketing.
“Readability matters; maintainability matters; functionality matters more. The longer you talk about proper tab spacing, the less time you're talking about algorithm optimization.”
Totally agree, these things matter most. Maybe I missed the point of the article if you think that’s what I’m arguing against.
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".
Actually, what people say about code, and how you interpret what they say, depends a lot on what you know about the person in question. I know lots of programmers who can list lots of precise reasons why they think a piece of code is "good" or "clean", but who still seem to lack "good instincts".
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 loathe the “clean code” discussions. It’s highly subjective and unmeasurable. I want along and wrote some code which was highly test driven, because why not, and at code review this guy said that it wasn’t readable, eg clean enough, and not testable. I explained that the code was derived with TDD and he couldn’t believe it. Another colleague saw it and agreed with me. But the point here is that all of these reviews and clean code debates are just tiresome and silly.
This reminds me when learning to write papers in college my professor would help proof my paper. I Had clarity in my thoughts but it didn’t translate to paper. We would evaluate the paper line by like. I would often use the phrase “X is better than Y”. He would stop me and ask, “why is X better?”, and I would give a more precise meaning.
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.
Wow! Great article, and I love to see so many people agreeing with it.
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!
Language is often about being able to take shortcuts, to infer meaning, to talk about things in the abstract an everyone understand what you mean.
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.
Sadly there seems to be nothing so attractive to a new(ish) programmer than a shiny silver bullet wrapped neatly into some popular or fashionable term, or better yet a three letter acronym.
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?
There is clean code, but I wouldn't say so generally. Each team should decide form themselves what constitutes clean code as a set of guidelines with documented motivations. Every now and then or as deemed necessary update and evolve the guide. As the codebase matures (in good and bad ways) so should the guiding principles change in weights.
Casey lives in a bubble of his own, where he's the sole developer of every project he starts and he gets to make all the rules for himself. He doesn't really have customers, stakeholders, deadlines, he doesn't work on distributed enterprise software, etc. He makes indie tools and games.
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.
> Casey lives in a bubble of his own, where he's the sole developer of every project he starts and he gets to make all the rules for himself. He doesn't really have customers, stakeholders, deadlines, he doesn't work on distributed enterprise software, etc. He makes indie tools and games.
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.
It was never intended as a definitive _thing_. I don't think anyone believes it has an exact definition. Just like good writing. There is no canonical good. Shakespeare and Hemingway are both "good" but absurdly different. But still, "clean" is a useful abstract concept for us to aim for in each context we encounter. At the end of the day, we need some kind of way of talking about "desirable" or "good" code. And I feel like 'clean' is as good as any other word.
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:
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.
There is a purist version of clean code. One not written. Anyone witness seeing code for the sake of writing code? Playing also on the word "clean" here. I also feel it is just as important to take out the trash and clean out undesired code.
Wow the font and color of the text sucks to read on that blog. I think maybe he has been stuck in C# or Java land to long. I do agree with him though that we need to provide more details on what makes it clean to you if you are saying that.
"What Is Clean Code?
There are probably as many definitions as there are programmers."
Robert C. Martin - Clean Code A Handbook of Agile Software Craftsmanship - p. 7
It also depends a lot on application, e.g. UI code will always look different from ETL code. ETL code is much easier to test, than, say, embedded code.
> Readable, Understandable, Simple, Performant, Safe; But these traits are in some ways at odds with each other.
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.
This is a classic mistake that relatively junior people make. There's a body of research out there that goes back more than half a century that gives you plenty of heuristics, metrics, and other ways to classify code as good at bad in all sorts of interesting ways. Most engineers have barely any awareness of this and instead absorb vague notions of "good" and "bad" things from others over time.
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.
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. :)