Hacker News new | past | comments | ask | show | jobs | submit login

"Clean code" needs a re-brand. It seems like constantly, over my 25+ year career, I'm seeing never-ending push back against design patterns, abstractions, de-duplication etc.

There are always the same reasons:

- Abstractions make code more complex

- We don't have time to write clean code

- It's all just your opinions and preferences

The purpose of clean code is to make code SIMPLER, and easier to maintain as requirements change. The value of software is precisely its ability to change over time. Otherwise we could stick to fixed circuits which are FAR easier and cheaper to implement and maintain.

Therefore, if you did not achieve these goals with your refactor, and your boss can make a persuasive argument that your refactor made it HARDER to maintain as requirements change into the future ... then your refactored code wasn't "clean", now was it?

As for not discussing the refactor with the original developer, that's outside the scope of clean code. Has nothing to do with whether or not clean code is good or bad, or whether your changes were "cleaner" or not. That's a process and etiquette discussion. Just because you did a dick move and went cowboy doesn't say anything about whether or not clean code is valuable.




I think the pushback is against certain recipes that seem too absolutist. "Clean Code" is the title of a famous book that defines clean (among other things) as:

"No Duplication

Duplication is the primary enemy of a well-designed system. It represents additional work, additional risk, and additional unnecessary complexity."

So according to this definition, removing duplication is synomymous with simplification, which is simply incorrect.

Removing duplication is the introduction of a dependency. If this dependency is a good model of the problem then this deduplication is a good abstraction and may also be a simplification. Otherwise it's just compression in the guise of abstraction.

[Edit] Actually, this quote is a reference to Kent Beck’s Simple Design that appears in Clean Code.


Actually, when writing highly optimized code, cut-and-paste duplication is not unusual. We may also eschew methods, for static FP-like functions (with big argument lists), in order to do things like keep an executable and its working space, inside a lower-level cache.

But also, we could optimize code that doesn't need to be optimized. For example, we may spend a bunch of time refactoring a Swift GUI controller struct, that saves 0.025 seconds of UI delay, avoiding doing the same for the C++ engine code, that might save 30 seconds.

I find "hard and fast rules" to be problematic, but they are kind of necessary, when most of the staff is fairly inexperienced. Being able to scale the solution is something that really needs experience. Not just "experience," but the right kind of experience. If we have ten years' experience with hammers, then all our problems are nails, and we will hit them, just right.

I tend to write code to be maintained by myself. It is frequently far-from-simple code, and I sometimes have to spend time, head-scratching, to figure out what I was thinking, when I wrote the code, but good formatting and documentation[0] help, there. Most folks would probably call my code "over-engineered," which I have come to realize is a euphemism for "code I don't understand." I find that I usually am grateful for the design decisions that I made, early on, as they often afford comprehensive fixes, pivots, and extension, down the road.

[0] https://littlegreenviper.com/miscellany/leaving-a-legacy/


In my view, the path to mastery is to first learn the rules, follow them and then learn when to break them and then transcend all of that:

https://en.m.wikipedia.org/wiki/Shuhari

TBH, I don't know if I will ever reach the transcendence stage, but my two cents are (after 20 years of professional programming) that breaking the rules is sometimes desired but not when the person has not even reached the first stage. The example from OP, as explained in the post, seems a clear case of never reaching stage one, following the simple rule of no duplication.

The conclusion in the article is kind of good and bad: it seems the OP has reached stage two but for all the wrong reasons and there is no telling whether they now actually posses the knowledge or that they just discarded one rule to follow another, both of which can be wrong or right based on the situation.


That's not just a view or opinion, it's how learning works. Thanks for the link!

When starting to learn a domain, you lack deep understanding of it, so you cannot make sound decisions without rules.

But rules are always generalizations, they never fully encompass the complexity they hide.

Through experience, you should become able to see the underlying reason behind the rules you've been given. Once full understanding behind a rule is ingrained, the rule can be discarded and you can make your own decisions based on its underlying principles.

Then you become an expert through deep understanding of several aspects of a domain. You are able to craft solutions based off intricate relationships between said aspects.

Let's take `goto` as an example. The rule you will commonly see is "don't use it". But if you're experienced enough you know that's unnecessarily constraining, e.g. there's nothing wrong with a `goto` in C for the purpose of simplifying error handling within a function. It only becomes an issue when used liberally to jump across large swathes of code. But your advice to juniors should still just be "don't use it", otherwise your advice will have to be "use `goto` only when appropriate", which is meaningless.


I agree.

Also, that "unnecessary optimization" thing, can be a form of "bikeshedding."

Maybe the app is too slow, and the junior engineer knows Swift, so they spend a bunch of time, doing fairly worthless UI optimization, when they should have just gone to their manager, and said "The profiler shore do spend a mite of time in the engine. Maybe you should ask C++ Bob, if he can figure out how to speed it up."


I love this part: "It represents additional work, additional risk, and additional unnecessary complexity", because it could be "refactored" into "additional work, risk, and complexity". I assume it hasn't been, because (in the author's opinion) it communicates the intended meaning better - which might be the case with code, too. "Well-designed" is subjective.


Good design has objective and subjective elements. Or... it might be more accurate to say that it is entirely objective, but some/many elements are context-sensitive.

For example, a style of writing that is difficult to follow but rewarding to parse for the dedicated and skilled reader may be considered good. It is good at being an enjoyable reading puzzle. But from an accessibility standpoint, it's not a clear presentation of information, so it's not good.

Mostly we call things that are increasingly accessible well designed. But we're using a specific criterion of accessibility. It's a great criterion and it's one we should generally prioritize. But it's not the only facet of design.

In code, we generally could categorize high quality design as accessibility. Most engineers probably think of themselves as not really needing accessibility features (although how many are undiagnosed neurodivergent?), but writing code that is easy to read and parse and follow is an accessibility feature and an aspect of good design.


I'm not really sure I know what you mean by "compression in the guise of abstraction". Re-usable code is a great way to isolate a discrete piece of logic / functionality to test and use in a repeatable manner. A sharable module is the simplest and often smallest form of abstraction.


Reusing a function C in functions A and B makes A and B dependent on C. If the definition of C changes, the definitions of A and B also change.

So this is more than reusing some lines of code. It's a statement that you want A and B to change automatically whenever C changes.

If this dependency is introduced purely out of a desire to reuse the lines of code that make up C, then I'm calling it compression. In my view, this is a bad and misleading form of abstraction if you can call it that at all.


> Reusing a function C in functions A and B makes A and B dependent on C. If the definition of C changes, the definitions of A and B also change.

To pile onto this example, in some cases the mastermind behind these blind deduplication changes doesn't notice that the somewhat similar code blocks reside in entirely different modules. Refactoring these code blocks into a shared function ends up introducing a build time dependency where previously there was none, and as a result at best your project takes longer to build because independent modules are now directly dependent or at worsr you just introduced cyclic dependencies.


Unrelated modules often superficially look like each other at a point in time, I think that’s what the parent is referring to. An inexperienced developer will see this and think “I need to remove this duplication”. But then as the modules diverge from each other over time you end up with a giant complex interface that would be better off as 2 separate modules.

So deduplicating unnecessarily compresses the code without adding anything to code quality.


Unrelated modules often superficially look like each other at a point in time, I think that’s what the parent is referring to.

Yes, exactly.


> Removing duplication is the introduction of a dependency. If this dependency is a good model of the problem then this deduplication is a good abstraction and may also be a simplification. Otherwise it's just compression in the guise of abstraction.

I think you're referring to coupling. Deduplicating code ends up coupling together code paths that are entirely unrelated, which ends up increasing the complexity of an implementation and increase the cognitive load required to interpret it.

This problem is further compounded when duplicate code is extracted to abstract and concrete classes instantiated by some factory, because some mastermind had to add a conditional to deduplicate code and they read somewhere that conditionals are for chumps and strategy patterns are cleaner.

Everyone parrots the "Don't Repeat Yourself" (DRY) rule of thumb and mindlessly claim duplicate code is bad, but those who endure the problems introduced by the DRY principle ended up coining the Write Everything Twice (WET) rule of thumb to mitigate those problems for good reasons. I lost count of all the shit-tier technical debt I had to endure because some mastermind saw two code blocks resembling the same shape and decided to extract a factory with a state patter turning two code blocks into 5 classes. Brilliant work don't repeating yourself. It just required 3 times the code and 5 times the unit tests. Brilliant tradeoff.


> saw two code blocks resembling the same shape

Yeah, this is the crux of it. What exactly is duplicated code? Humans are pattern matching machines, we see rabbits in the clouds. Squint at any 4 lines of code and something might look duplicated.

On the other hand, code bases that do have true duplication (100s of lines that are duplicated, large blocks of code that are exactly duplicated 16 different times), multiple places & ways to interact with database at differing layers - that's all not fun either.

It is a balance & trade-off, it goes bad at either extreme. Further, there is a level of experience and knowledge that needs to be had to know what exactly is a "duplicate block of code" (say something that pulls the same data out of database and does the same transform on it, is 20 lines long and is in 2, 3 or more places) vs things that just look similar (rabbits in the clouds, they are all rabbits).


> Deduplicating code ends up coupling together code paths that are entirely unrelated, which ends up increasing the complexity

Code paths that may be unrelated. If they are related, then deduplicating is most definitely a good idea. If they're trying to do the same thing, it makes sense that they call the same function to do it. If they do completely different things that currently happen to involve some of the same lines of code, but they could become different in the future, then deduplication makes no sense.


"Clean code" can mean two things: (1) someone's subjective feeling that a certain piece of code is nice, and (2) following the (majority of) the guidelines in the book Clean Code and related.

What people have started feeling, myself included, is that (2) and (1) are fundamentally different in many places. That is, that following some of the guidelines of Clean Code the book produces code that is very much not clean.

So sure, everyone agrees that clean code is better, almost by definition. There are indeed valid (usually business/time) reasons to go for code you know is messy, but those are all external - in an ideal world, everyone wants to write clean code. But, increasingly, people don't feel that Clean Code is a good way to achieve that. In fact, many of the patterns in Clean Code have ended up being considered sources of messy code by many people - over-abstraction and de-duplication perhaps chief amongst them.


After numerous failed attempts at reading Clean Code, I'd say (1) and (2) are very much at odds.


One thing I learned in industry is that the best cleanly written code does not survive contact with product. Sometimes the technical debt is encoded in the product requirements themselves, and no amount of refactoring or best practices can fix it, only mitigate the pain.


Sure. We see that all the time. Error handling and security are often afterthoughts, for example. We engineers tend to get "commissioned" to build quick proof of concepts and once the business sees their vision come to reality they don't want it to be thrown out and redone with architectural consideration, they want to rush it to market as the "MVP" as quick as humanly possible. This is a reality of our industry that we need to manage and find ways to cope with.

When it comes to certain architectural decisions, there are things that are very hard to change later on, and sometimes you don't know how the product will need to scale, or what challenges the engineering team will be faced with a year, two years, 5 years down the line. This is where we need to make our best guesses and we can't always get it right.

When it comes to the rest, making the code as simple and easy to change is the best we've got.

I was recently reminded by someone I look up to and admire that not everyone has the "gift" of being able to think "top down." This came up because I am often frustrated by the claim that "we don't have time to write this cleanly."

It's a frustrating claim to me, because I don't find that writing short, single responsibility code that is well labelled takes any time at all. I also spent time learning how to work TDD so I can write unit tests as part of the development and design process rather than having to go back and write tests after the fact.

But a lot of developers build "bottom up", where they write really long functions or methods, or dump everything into a single file and then think about separation of concerns, and layers and isolated "components" later on and thus need to spend a lot of lengthy time refactoring after the fact.

That initial "bottom up" process is the "getting it to work" part of the process, which is fun for most devs. But if they then need to refactor after it is tedious and laborious and not fun at all. And I think this is probably where the vast majority of the push-back and friction amongst devs really comes from if we look at it from a human point of view.

If you enjoy the process of DESIGNING software, and thinking top down, you won't find refactoring to be a chore or time consuming. You will understand how a well chosen abstraction can SIMPLIFY rather than complicate. But if you like to solve problems primarily, and to build things you haven't built before, and by "building" it means getting something that "works" up and running quickly ... then you don't want to spend your time reading books on design patterns, or refactoring your PR because the Principal told you it's not up to standards.


Top down and bottom up have little to do with using long functions and not separating code in multiple files. In top down you start with a high-level overview of the system and gradually break it down into smaller and more detailed components as it provides a clear and structured view of the system. In bottom up you start with the individual components or modules and gradually combine them to create larger subsystems or the complete system. Each approach is suitable for a type of problem. I personally prefer to start off a POC as bottom up then once the general shape of the system emerges a rewrite top bottom is a good idea. I've seen lots of systems that started as top bottom and the architecture was atrocious because it's final shape wasn't know at the beginning, it was great on paper though. There's no silver bullet, there are multiple ways to solve problems and they all have their pros and cons. The takeaway is part is that applying methodology dogmatically is not a good idea.


> I don't find that writing short, single responsibility code that is well labelled takes any time at all.

Writing it takes no time; designing it does.

> That initial "bottom up" process is the "getting it to work" part of the process, which is fun for most devs.

It's not about having fun: it's about getting the functionality to the first users, and then, finally, you can get some slack and have fun and refactor your code for future changes. But functionality is what sells: clean code is invisible to the users and thus to the business.

Clean code is for us developers, and only for us. We have the right to be egoist and work for our own sake from time to time, and a working functionality is what will let the business close an eye over it for some time.


> It's not about having fun: it's about getting the functionality to the first users, and then, finally, you can get some slack and have fun and refactor your code for future changes.

You're speaking to business motivation, which I intentionally did not touch upon. Everything you said is valid.

What I'm speaking about is developer push-back against "clean code." And when a developer is advocating for the business, then it is business push-back.

Business push-back is valid because a) their business needs are valid and b) it is very difficult to correlate code quality with business value. There is a correlation, because developer velocity and ability to grow the feature-set and fix defects quickly and efficiently benefits the business as it reduces cost over time and benefits users. But that benefit is long-term and not immediately visible by end users. The business doesn't always want to pay for this up front which is their prerogative, and then we circle back to what you said about developers doing it in order to make our own lives easier because we know the day is going to come when the business gets frustrated that velocity was extremely high at the beginning of the project life-cycle but has ground to a halt.

In any event, developer push back is real, and is not tied to business values but to personal developer values. I see code comments (which IMO are smells and point to smells) that say things like "// no point in extracting this anonymous function as it is only a few lines long" ... meanwhile doing so would have taken no extra time at all and it is buried in the middle of a 300 line Promise chain or rjxs stream etc.


To see how clean code affects users you need to look beyond its direct, first order effects. Clean code is less likely to be buggy, is more malleable and performs better. That's how clean code benefits users, they get to enjoy faster software with fewer bugs and more features.


> you need to look beyond its direct, first order effects.

Sure, I agree! But too many developers, in order to look beyond the first order effects, have a tragic tendency to forget about looking at the first order effects. And that is a big problem!


What a giant pile of “no true Scotsman”

The purpose of clean code and the results of practicing it, it turns out, are vastly different. The reason for that is because people are generally incapable of predicting any future state of their code even if they have the requirements in front of them.

Clean code necessitates that dreadful question of “what if”, which is a horrific Pandora’s box of never getting your shit out the door.

It is nearly always better to code for what you know, and to have a person experienced in the domain to otherwise guide the architecture.


I never made the argument of "no true developer." I pointed to specific reasons cited when push-back is given, and then explained why I disagree with those points.

> Clean code necessitates that dreadful question of “what if”,

Nope. Not even a little. "Clean code" can be as simple as keeping your moving parts isolated and loosely coupled so that they are easier to debug, to reason about and to reuse in new contexts if needed.

What you're talking about, the "what if" game ... is what Martin Fowler termed "Speculative Generality" in his book "Refactoring" when cataloguing code smells and sources of complexity. In other words, the exact opposite of what I'm talking about.


Sorry, but your entire post is a “no true Scotsman”.

>if the code is not maintainable, then it’s not real clean code”

If this isn’t a word for word prime example of “no true Scotsman”, I don’t know what is.


If you don't mind, I'd like to propose an alternative list:

- Most of our abstractions are bad, even those we think are good. The good ones are largely already in our libraries (although even that isn't always the case unfortunately).

- Bad abstractions are a time sink.

- Writing code is also about communicating with other humans (on the team), so what they think about communication matters a lot more than what I think


> Bad abstractions are a time sink

Yes, but contrary to the general perception, I fend them to be far less of a time sink than duplicated code. It takes almost no time at all to bypass or rework a bad abstraction, but code duplication necessitates either a literal multiplication of your efforts wherever you need to add features to duplicated code, or to refactor the duplication into a non-duplicated form, which also takes a lot longer than dealing with a bad abstraction. Abstractions are cheap and disposable, even if they're bad. Duplication is expensive and hard to get rid of.


Abstractions are not cheap and disposable. If 5 places use one abstraction, it takes at least (1) 5x the effort to dispose of them compared to changing one of those places.

(1) At least 5, because sometimes its worse. Example: lets say you are using a bad ORM which in addition doesn't expose its connection pool in any reusable way (most don't). You want to introduce a good ORM, but you can't because duplicating the size of the connection pool is too expensive. Additionally, because the ORM triggers events when objects are created and modified, and database state relies on that, you cannot use the other ORM for object creation and modification unless you replace them all at once. So you would have to refactor your code to introduce your own data access layer that has implementations for both ORMs, then switch over all at once.

In contrast, introducing deduplication later is somewhat easier. You can build an abstraction and test it with 1 or 2 of the existing parts of the code, then migrate the rest incrementally instead of all-at-once. It will also prevent some bad (expensive) aspects of the design due to the requirement that it must be incrementally adoptable (must be able to have a reusable connection pool, demand a more reliable change events system etc) and as a corollary it will also be incrementally removable.


"somewhat" easier is debatable.

The bad situations from the costs of duplication are often far worse. If those code paths had slightly different needs, they would diverge. It often feels like 90% of my career has been to take on massive undertakings to learn am entire system to ultimately pick these out after the fact & replace it with something reasonable.

I don't have data to support any absolute claims of what is better vs not. However, every project, team, company, industry that I have worked on has been filling my time with the above scenario. Not sure what universal truth could be pulled from that (if any)


> Otherwise we could stick to fixed circuits which are FAR easier and cheaper to implement and maintain.

While this is not the key point of your post, this in particular is also not the case. With even a little complexity, it quickly becomes much easier and cheaper to implement functionality in Software on a generic CPU than in fixed circuits.

For example, we often integrate simple CPU in products that otherwise are fixed circuits, just to perform single functions as they would be too resource intensive and hard to verify. The CPU then becomes a slave to the surrounding circuit.


People never think about complexity as a tool, but it's as much of a tool as the lowly assumption. If anything, they're two sides of the same coin.

You can make things simple by making assumptions, you can enable cool things by making things complex.

the thing about assumptions is that they're dangerous. easy example: assuming the timezone in a db column vastly simplifies things but if you're ever wrong things can go terribly wrong.

Which is why some of the most dangerous developers on your team are the ones who either don't _respect_ assumptions or don't even realize they're building in assumptions.

The flip side is true for complexity. You can absolutely harness complexity to great effect, but if you have developers who don't realize they're building in complexity or don't respect the cost of that complexity, you have dangerous developers.


Yup, Clean Code is to make the life of $NEXT_PERSON easier.

The value put on this varies from company to company.

Some require it, others will balk at not releasing a feature because you want to tidy the code up from being prototype quality.


It's a good idea to remember that $NEXT_PERSON could quite possibly you a year from now, when you've probably forgotten what you were on about previously. There are selfish reasons to clean code as well!


I think the broader issue is that there is little valuable empirical evidence that the stuff people say is clean code actually makes things simpler. Likewise I find it unlikely that there is one way to make things simpler and its the way associated with the book. It's all vibes.


I like to keep in mind a companion rule along with DRY (don't repeat yourself): Don't make the reader repeat themself, i.e. re-read your code multiple times after you've cleverly repackaged it.


Philosophy of Software Design is a much better book discussing these ideas than clean code. Part of the problem is uncle Bob's book is now tied up with the idea of clean code, but his book sucks.




Consider applying for YC's W25 batch! Applications are open till Nov 12.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: