You could have just moved the repetitive math into separate functions and called them from the resize functions.
The resize functions themselves only looked repetitive because they were forming an interface, which is why refactoring them away was wrong. But the math they performed is a perfectly valid target for cleaning the code.
Especially since it was a refactoring, it's generally better to handle those in stages if you're working with a team.
There are lot of ways/patterns for solving the same problem... but tearing apart an existing interface being used by others isn't a good start.
The best part about abstraction is that you can modify the underpinnings to be more flexible without hurting the rest. A good step would be to create more flexible math functions underneath, like you're saying, and leave the rest alone.
Afterwards, there can be deeper conversations with the team about how they want the interfaces to communicate to provide an obvious facade but an abstract implementation.
I feel that Clean Code and Uncle Bob is like modern tiktok influencer hustlers. It's feel good whishy-washy crap that has some amount of truth but in the end it ends up being very non-productive
Not to mention UB was all butthurt the other day when people found out a conference he was invited to had fake speakers in its lineup. But hey, I guess it takes one to defend one.
mmm.. this is an interesting take. I have been a zealot of sort in the past (intern/junior time), but now coming to same conclusions about these cargo cults.
> UB was all butthurt the other day when people found out a conference he was invited to had fake speakers in its lineup. But hey, I guess it takes one to defend one.
Firstly, what does this have to do with clean code?
Secondly, “butthurt” is not a word I feel correctly expresses his comments. From what I can tell, he advocated for a skeptical and cautious approach, and wanted due process to take its course instead of mob justice to run with the first impression, as it always does.
Thanks for the link, you scroll down and see the rebuttal by Gergely
> Confirmed speakers at DevTernity 2021, 2022 and 2023 and JDKon 2024 do not exist. There were & are no such engineers at Coinbase or Meta.
> I shared this fact and said no conference should list fake speakers.
There is no "crime" there per se, having a fake speaker in your lineup is a doozy, but it's different from, let's say, saying Bill Gates (with his picture) will be at your conference with no attempt of doing so.
Now I do think the reasons listed by the conference organizer are feeble at best, but (moderately) plausible. I'd never put a placeholder saying so and so are "Engineer at Coinbase" and giving them fake names/pictures
A reasonable rebuttal. But how are his original comments therefore “butthurt”? This was what I was arguing against, remember?
You seem to be making the same misinterpretation of my comment as people in that thread; simply because some comment is not 100% supportive of one side, you think it must therefore be defending the other side. He expressed concern that mob justice was being perpetrated rashly, and people are interpreting that as support for the other side. Similarly, I was arguing against your categorization of his argument as being “butthurt”, and you immediately think that I am supporting the other side of the argument, and therefore present me with rebuttals to that argument, instead of what I actually wrote.
I feel like what you’re describing is bad abstraction. This is the real issue, people designing things off two or three examples, not making sure that what they have is general enough to accommodate future usage.
This is completely right. People read about design patterns or DRY, usually apply them wrong since they're still learning... and then act like it's the source materials fault?
Yup this is the worst part of Clean Code. I've seen real-life bugs caused by this kind of refactoring.
The best part is that the same commit added unit tests so our coverage grew over the magic 80% bar, but of course unit tests don't usually test for race conditions.
Design patterns, for example, really should only be studied once you have quite a bit of experience with complex code bases under your belt. You need to have done battle with some nasty code problems in bigger code bases before you really understand the problems design patterns are trying to solve. Inexperienced developers can't be trusted to apply them because they don't have that judgement. It just looks like a shiny thing.
True for so much of compsci stuff. You won't understand regular languages, automata, grammars etc until you have done a fair bit of pattern grokking yourself.
The problem is that the source material (the Clean Code book in this case) has almost no caveats. And it rarely acknowledge other styles or points of views. For example, when the book discusses possible objections to having "lots of small functions", it simply doubles-down, suggesting also having tons of small classes, and rationalizes it. Experienced developers know that reality is a bit more nuanced than that.
This is actually a big reason for the popularity of the book, showing a "one true way".
And this carried on to followers of the style. Some linters even enforce the style without much regard for the practicality of it.
I don't see how that's a fair characterization of a book that has this quote in the first chapter:
> Many of the recommendations in this book are controversial. You will probably not agree with all of them. You might violently disagree with some of them. That’s fine. We can’t claim final authority. On the other hand, the recommendations in this book are things that we have thought long and hard about. We have learned them through decades of experience and repeated trial and error. So whether you agree or disagree, it would be a shame if you did not see, and respect, our point of view.
It's been a while since I've read Clean Code, but I seem to recall it stated many times that blindly applying the rules of Clean Code without good justification would lead to bad code. The author even provides examples of this. People in this thread are criticising Clean Code principles as if they are meant to be a rigidly enforced dogma. They aren't, and the author never intended them to be so.
I think that quote is a good example of why it is a fair characterization. It uses the authors’ seniority to argue from authority, even explicitly requesting respect.
To a beginner, it reads like ”these are subjective matters so experience is king, and we have more experience than you do”.
"Respecting my experience" does not translate to me as "do everything exactly accordingly to these strict rules". To me it says "consider my opinions before doing something different". Consider. Not follow blindly. I can see how one may interpret it as the first if they read the quote in isolation, but certainly not in the context of the
book. Which, as mentioned before, goes out of its way to state these rules are more like guidelines, and gives examples of where strict adherence causes worse code.
Is it? The book requires being quite familiar with programming already. Perhaps this is me projecting my own experience of when I read it, but I feel like the book is targeted at someone in the late-phase of being a Junior Dev, and/or the early-phase of being an Intermediate Dev.
I personally derived a lot of value from reading the book, and I feel like my skills noticably improved from before to after. So perhaps my own biases are showing, but I believe discounting the book's contents wholesale is a mistake. There is a lot of value to be garnered from reading it. The book doesn't have to be the infallible word of the Software Gods for it to be useful.
That disclaimer filters out anybody that isn't at least on the transition to be a senior dev (with real seniority, not just in inflated title). It takes quite a lot of experience to agree or disagree with a rule, and respect a point of view without automatically applying it.
In fact, since the rules on the book have way deeper impact than they look like, being able to read that book and not getting damaged by it is a good test for seniority.
But, funny thing, if you are mature enough to fit the disclaimer, you've necessarily already seen all that the book talks about and don't need reading it.
Not bad advice when you remember that advice is only ever for people who know absolutely nothing and need encouragement to start. "Only solve for X" gives some constraint so that one can start to focus on what really matters and not be bombarded with so many choices as to derail the entire effort.
With practice, one soon comes to understand why you might also want to solve for Y. Those with experience are going to ignore what everyone else says anyway, so it doesn't matter if it is not true for them.
Indeed, we in this industry are bad at practicing, and that's a problem. Imagine being Taylor Swift and having your guitarist for the night's performance having picked up a guitar for the first time yesterday. That would never fly, yet that's often how we treat software development – and that's how we get to these kinds of places.
This is why I've grown to like discovering patterns based on changes - new features and bugs being fixed.
I wouldn't worry too much about somewhat duplicated, but easily understood code, like the original example.
I would however worry if I have to fix the same issue in 3 places, or add code for a new feature in 3 places. That's when I start wondering if I can push the code I need to change or add into a common place somehow.
In particular, a good approach to this is to abstract the common parts to all those into just some very simple functions. Abstract bottom to top, not top to bottom. There is no need to create a single point where everything happens and deals with the 2^n combinations.
You still reduce repetition, you can still change stuff in a single point, but you retain the ability to make custom changes if needed.
I completely agree. The issue with the "clean" version of the code is that it's completely coupled together. If you touch one thing in the math code, it affects everything. At some point, it becomes easier to keep some duplicated code, point out the similarity using some code structure cues or comments, and over time, highlight any key differences using more comments.
A "clean code" approach to this kind of code doesn't handle evolving requirements well. It always ends up as a mess of helper functions with multiple code paths within depending on where the function is called from. This can sometimes be abstracted: some people might have noticed already the previous sentence is describing OOP polymorphism. But how would anyone know the exact requirements ahead of time? If we're in such an early phase, any attempt to "clean code" will only result in a bad abstraction, or in overengineering. The right approach is to do nothing, note the possible issue, and wait until more is known about the problem domain.
We could call it The Premature Abstraction Anti-pattern :)
Been a victim of this myself a few times myself so I normally do exactly what you've suggested and wait until a clear, and still performant abstraction has emerged from the code and slowly refactor-in the abstraction. But it's always better to do this on a nearly completed codebase, so at the end, where you should naturally be looking to remove lines, not add.
Yes, the problem in the real world is that no one KNOWS they're writing a bad abstraction at the time they're writing it, otherwise they wouldn't write it.
Therefore it's insufficient to say things like "avoid bad abstractions" or "make sure you accommodate future usage". No one can predict the future, generally speaking.
Abstraction should be used with extreme caution, even when you feel that it's probably right. Be an extreme skeptic.
There is no need to accommodate for future usage. Most of the issues resulting from abstractions actually come from people treating abstractions as sacred, trying to adapt abstractions used by some piece of code to fit new use cases, when they should be treating abstractions as disposable instead, swapping them out and creating new ones instead of altering existing ones. If the abstraction that you come up with based on the first few examples is not suitable for new use cases, that's completely fine.
As far as generality is concerned, it's not the number of examples that matters. Once you learn to distinguish whether your duplicated code actually duplicates some piece of knowledge or just happens to resemble another piece of code by happenstance, you stop having to rely on an unreliable magic number as your limit on how many instances of duplication to allow before abstracting something. And that skill probably correlates strongly with the understanding that abstractions are not just macros that make it more convenient to repeat some chunk of code, they actually represent knowledge, and thus their primary use is not to eliminate duplication, but to encode an understanding of a problem domain into the code.
Clean code is bullshit. Don't get me wrong: I like to have beautiful and clean code very much. But code has to be maintainable, performant, reliable, communicative, adaptive and all kind of other things before it has to be clean.
If your clean code hurts those, you made the code worse.
Sometimes you will just have to accept that things take the number of lines they do and having them duplicate in different places is a feature as you (or someone else) might have the reasonable desire to change the individual parts.
Whenever your sense of aesthetics overrides practical concerns you should pause and take a step back.
> Sometimes you will just have to accept that things take the number of lines they do and having them duplicate in different places is a feature as you (or someone else) might have the reasonable desire to change the individual parts.
The trick is always to figure out whether you are in that case or whether you are in the case where someone will want to change the shared logic everywhere at once. In my experience, the latter is vastly more common, and this is what DRY optimizes for.
Part of the reason I enjoy languages like Haskell is because of the abstraction maximalism enshrined at the language level, rather than the coding level.
We all agree on what an Applicative is supposed to do. The challenge shifts right in the idea-to-code pipeline to finding the right Applicative for the problem. Most OO design patterns have natural, well defined equivalents in such an environment, but the difference is this way helps minimize the amount of project specific abstractions you have to deal with.
The downside as a beginner is of course a greater upfront cost to learning the language, and quite frequently opening up another Haskeller's work and realizing there are like 12 compiler extensions you've never heard of that you now need to grok. But even there I would argue that's a net win: you can carry those compiler extensions with you to all projects in the future. (The downside as an expert is realizing you have to get a PhD to get a sense for how much RAM your cats will gobble up!)
oh, oh my. The golang opposite where you don't have language extensions and should be able to grok everything about the language in your head all at once... yeah I'm going with "simpler language".
I know, I know. It's very silly of me not to pick up Rust when I've already got all of its forbearers in my bones. Someday, once I've finished studying everything else I need.
Rust has GAT's which have the same expressivity as HKT's. But many Haskell patterns turn out to be unidiomatic in Rust due to extra overhead. (For instance Rust has multiple function-like traits that access their environment differently. There's no equivalent to this in Haskell of course; GC and default boxing choices for Haskell types obviate the issue, but this is not zero cost.)
As a fellow haskeller, I agree on that the language has very good abstraction capabilities.
On the specific example in the blog post though I can only say that there are so many times I've been down the clean code path and it's more often not another person, but the future you, into whose foot you are shooting. Abstraction is not beneficial when you need to study it later to understand it again.
> Abstraction is not beneficial when you need to study it later to understand it again.
This is where a good study routine comes in. I study Haskell like I study all things of any long term importance, using an Anki deck. It takes me longer to start using a new abstraction, but once I have it it's pretty much there for life, due to my daily commitment of half an hour or so to my reviews. It makes far less sense, of course, to do this for a one-off abstraction I had to apply to a specific project, so this naturally tilted me over the years to work more and more in loss like Haskell.
(N.B., I do not state this as a prescription. I think this is well above and beyond what most people expect of themselves professionally, and that is okay. Everything in life comes with tradeoffs.)
This is really interesting to me. What exactly do you write on the anki cards? I cannot really image learning a haskell abstraction by mere memorization. How can you learn it without actually applying it to a real problem. Often times the hard part is to know when to use which abstraction. How do you learn this using flashcards?
What you detail is pretty much how you would recognize it. You write out a brief summary of the high level problem, and then ask yourself "which abstraction would you reach for first, absent any other information?"
The latter part is often implied, but it's best to make it explicit. You're trying to build up expert intuition, which means allowing for the fractal nature of software development to mean you might be in the 20% of times where this won't work for you.
We know what Applicative and Monad (forgetting the burrito) do because the have mathematical LAWS. If a piece of code uses Applicative or Monad you know it’s behaviour due to the Applicative/Monad mathematical laws.
No OO design pattern has a law, they are a loosely defined concept/pattern which means they are open to personal interpretation/customisation but no concrete law to say exactly what it is and its expected behaviour/output.
> If a piece of code uses Applicative or Monad you know it’s behaviour due to the Applicative/Monad mathematical laws.
Well, we actually only know what it is supposed to do. The laws are not enforced, so it is perfectly possible to implement a type class that violates the laws of the type class.
Several test frameworks implement Law Testing which will test your instances for lawfulness as long as you can generate instances of your data structures. E.g. https://hackage.haskell.org/package/hedgehog-classes . So you only need to be able to generate instances and you get comprehensive tests for free, so that's nice too.
It is also the case that implementations of these very abstract classes typically only also have very few implementations that make any kind of sense type-wise and that the 'obvious' one is correct.
(Just to preempt: Yes, tests aren't proof, but such is life without a full proof system in the language. In practice, I've found property-based testing sufficient.)
Oh, I know and I'm not arguing against anything you've said. I just wanted to express - for people who don't know Haskell - that while the laws exist, they are not (as of now ;) enforced or checked by the type checker.
Your observation reminds me of Philip Wadler's Strange Loop talk [1], where he states that some languages are discovered, whereas most languages are merely constructed.
I think he would have agreed to Haskell (or at least, languages based around lambda calculus) is more in the first category, whereas Java, C++, Smalltalk, Javascrip (ie. OO languages) are in the second. Computational fundamentals versus engineering problems.
But Monads are like what you compose. The equivalent in OO would be the semicolon. You still need business logic that follows messy life / human rules
no matter what paradigm you use.
I think these are two wildly opposed world. What the industry calls OO is just a way to try to avoid large business failure. Other paradigms were more about computation, the logical basis is way way deeper. These people like to own all computational layers very precisely from parsing, to compiling, to algorithmic analysis. Enterprise/Application coding is not about that, it's how to bridge the customer / IT gap with something not too horrendous and still adaptable enough to handle market chaos.
The OP would have done better to use more OOP principles, unless eschewed by the team in general--then I'd probably work elsewhere. The place where OOP has the most natural fit is implementation of GUI widgets and their editors. I've written a few myself.
The 'rule' that unifies OOP's other rules is tell-don't-ask. So for the example at hand we have the handles which are shared in a known way and I presume shape-specific control points (e.g. circle may have center, radius). Then a change of a box handle could call a updateControlPoints(changedHandle). There would also be updateBoxHandles(changedControlPoint). There may be other clean splits, but that's how I'd do it.
> We all agree on what an Applicative is supposed to do.
Maybe. But not on which parsing library to use (parsec? megaparsec? attoparsec? regex-applicative?), on whether to use Data.Text.Strict or Data.Text.Lazy, on how point-free the code should be, on whether to use "." or ">>>" or any other combinators, on which language extensions to use...
I like Haskell, but it suffers from similar problems as any other language: people can't really agree on the best way to do things
- actually write a story relevant to the lesson learned, and not make up some fable that takes 1/3 of the article before even starting to make a point
- not have any sort of popups asking me to subscribe or login
- not have any sort of annoying javascript animation that gets in the way of reading the article
I found that the best use case for classes is dependency injection. Have a constructor, inject your dependencies, then use them in one or more functions. However, you can do the same with functions:
function new_foo(dependency) {
return function(bar) {
// Do something here
}
}
Everybody has different taste and opinions what "clean code" is supposed to look like (best example of this is "for-loops" vs .map()" - IMHO nested loops are usually more readable than a function chain which does the same, but other people have the opposite opinion).
IMHO the core problem of the situation described in the article is that the guy simply rewrote a piece of code without before talking to the author/maintainer/owner of that piece code.
How is this positive?
Respect the limitations imposed by a framework or structure instead of thinking how that could be improved?
Conciseness is a value if it helps the communication and understanding, but it’s not an absolute value.
And that is true for code, as the post explains quite well.
You're wrong, your initial corrections were correct. Your boss will now have to spend 4x as much money to modify every class that ever relied on Ovals and Rectangles if/when he realizes there's another way he might want to modify them.
You didn't catch the end where they explicitly say
> Secondly, nothing is free. My code traded the ability to change requirements for reduced duplication, and it was not a good trade. For example, we later needed many special cases and behaviors for different handles on different shapes. My abstraction would have to become several times more convoluted to afford that, whereas with the original “messy” version such changes stayed easy as cake.
Clean code is easy to understand and to change. Ideally, any given file or method only deals with one concept.
Messy code usually results from someone merging concepts to avoid duplication. After that happens, differences in the concepts cause the code to become much harder to reason about.
I've seen this particular problem happen a million times. The worst code I ever stepped into was completely DRY. When, where, and why something happened wasn't something the original authors could answer. That's because everytime they found code duplication, they'd abstract. And everytime that didn't work, that ended up adding little "if (this instanceof Foo) special case;" to get over the most recent bug.
I've written code for profit for about 25 years, although I started learning to code in Perl and BASIC when I was 8 years old, so, 35 years total. I have a high school degree. I dropped out of art school. I'm self-taught. I've worked on some larger teams but generally in a role where I was responsible for a final module or piece of code from start to finish. Most of my career has been working alone, planning and building full stack custom software for midsized businesses and startups that need particular functionality in their workflows that they can't find off the shelf. Much of it is designing the business logic. Most of my codebase now is TS, NodeJS and PHP, (and SQL) although up until 2016 or so I did a huge amount of work, including games, in Actionscript 3.
> I was expecting this to be a rant against Uncle Bob for his weird comments around the fake women speakers at the tech conference.
It would have been very unfortunate if it were. If an article, entitled "Goodbye, Clean Code", had been written in response to Uncle Bob's twitter life, what it would have demonstrated would be that the author thinks that people with bad opinions have nothing of value to teach about their area of expertise. Too common a position these days.
If someone is interested in further contemplation around this, I highly recommend the book "A philosophy of software design" by John Ousterhout. In my opinion, it gives good reasoning why clean code is sometimes praised and sometimes criticised like it is here. I highly agree with Ousterhout that software complexity is mostly caused by obscurity and (cognitive) dependencies within abstractions. With that in mind its easier to see why Dan failed to improve the code here.
> Firstly, I didn’t talk to the person who wrote it.
This might be an issue in itself sometimes. People identifying with the code they wrote. When you are part of the team, the code belongs to everyone. Everyone should be able to change it if it makes sense and all quality gates are passing.
> When you are part of the team, the code belongs to everyone. Everyone should be able to change it if it makes sense and all quality gates are passing.
Yes, but actually no. Context is important here. If that code had been written six months ago or even a couple of weeks ago? Have at it. But, the OP states "My colleague has just checked in the code that they’ve been writing all week." then "It was already late at night (I got carried away)."
If spent all week on something, working out the all the details, checked it in at 5pm, and then came in the next morning to discover that someone stayed up rewriting it rather than waiting till the next morning to talk to me about it first, I'd be absolutely livid.
sure everyone should be able to change the code, but somebody is immediately refactoring new code to align with their own opinions and making it worse in the process is not ok. i would immediately revert the commit if somebody did that to me (and then tell them why of course)
well obviously both, but it just isn't a very good point. by making changes to the shared code in that specific way, you are undermining and insulting the developer who is supposed to be part of your team. you can't just pretend that the team aren't humans and that social conventions and etiquette don't apply.
Context is very important. If this happens overnight and out of nowhere/excess pro-activity, it's definitely an issue.
But the reality is usually more nuanced. Even saying that because of this particular situation clean code is bad, it's an exageration.
Why is everyone stating what he should have done instead? This is a perfectly legitimate lesson.
In finance, we are constantly dealing with products which are somewhat alike but not quite. Equity options vs Fx options ? Sure, they're options, they have a strike, we price them using some version of Black Scholes, etc. But oh boy they are so different.
The temptation for new joiners to factorize a lot of behaviors is huge, and many times we are in the shoes of the OP's boss, telling the younger devs to remain calm and keep things separate.
It's not about saying goodbye to clean code, it's about actually maintaining a clean code by not overabstracting.
> It's not about saying goodbye to clean code, it's about actually maintaining a clean code by not overabstracting.
But the title says so! :p
I get the whole point of this post, but it can easily be read as an excuse to actually messy (as opposed to "non-clean") code. The author doesn't actively distinguish them, so others do.
> Why is everyone stating what he should have done instead? ... In finance, we are constantly dealing with products which are somewhat alike but not quite.
Yes, but we are talking about geometry. What the (usual) handles do are affine transformations (translation, rotation, scaling and mirroring), if the scaling is uniform, these are similarity transformations. And are guaranteed to work for any shape.
Which leads to the real problem: you can't (well, shouldn't) abstract something, if you don't have enough knowledge about that - especially what new possibilities may show up in the future. For example if somebody thinks that the only possible ovals are circles and ellipses, they will be quite surprised when the first "real" oval shows up.
What goes back into finance, because rewriting a Black Scholes predictor every time one instrument needs it would be insane. And yet, it's what this code seems to do.
It's funny that you phrase this as such, because this exact kind of simple geometry is my go-to example for why this stuff is not as simple. Let's look at a square. Everyone knows a square is a rectangle, right? So of course a square should extend from a rectangle.
But math doesn't deal with mutability all that much. It's interested in the visible properties and constraints. A square is a rectangle because it meets all the constraints and has all the properties of a rectangle. But that is no longer true if the shapes are mutable. A rectangle might appropriately have `setWidth` and `setHeight` operations. A square cannot implement these operations and still obey Liskov substitution, without the ability to downgrade its type to a rectangle. In OOP you might correct this by making the square immutable and the `setWidth` and `setHeight` operations would return a rectangle instead of a square.
To bring this specifically to this use case: Yes, it's a defined mathematical operation to scale a shape such as a square on the x-axis. But, if you do, that square is now a rectangle. This may be important to the implementation, it may not. But it's certainly a relevant technical concern.
No, a square is a rhombus and a kite, same as a rectangle is a kite, so both should be kites. And both are also trapezoids, so they should be trapezoids too. And paralellograms. And of course all of these are quadrilaterals. A square is also a
The idea of geometric objects in a hierarchy leads to either incorrectness or problems in the implementation (most of the time both ;). Just don't do that.
Agreed, it's quite telling that everyone in here feels like they know better than his team mates and him (at the time of writing the article), with no other context than some pseudocode.
DRY has value but abstractions come at a cost. It's always a tradeoff.
I think I went into the same trap as well quite often. And then I exactly ended up with the problems as they are described, i.e. adding further special cases becomes really complicated and convoluted in my abstraction.
But I really find this difficult. How to decide what abstraction is ok and which is not? Since I did this mistake a few times, I now sometimes tend to try to think a lot about this, which slows me down a lot, and in the end, I still not really sure whether I made the right decisions.
I think it's so specific to a given codebase that it's hard to give advice that generalizes well.
I have the same feelings as you here, I often feel a little lost trying to decide how much duplication is appropriate in a given case.
I try to not overthink it in the difficult cases. There are many other places in code where it's more obvious, because you can see the history of how people worked with the code, what was the frequency of changes and edge cases added to it etc.
Often it's also just about the readability of the code, I tend to go by the "how confusing it will be for the next person if I defuplicate this code in a smart way" metric.
The changing part is not time consuming, is having to fix the same bug over and over because you didn’t realize this same function was copy-pasted in 7 different files. It really gets worse when you realize this a year after the first bugfix, still dealing with a stupid bug you fixed a year prior, if it wasn’t for copy-pasted code.
I’m also against DRYing up unnecessarily, but if the code is a 100% match in intention and/ord behavior, then it’s going to be deleted.
just to challenge that, why would you not realise the function is copy pasted in this scenario?
I mean, you'd see that the function is inlined in the place that you discover the bug, so you already know that changing that function's code isn't going to fix similar bugs anywhere else right?
How do you know where the function has been copy pasted? How do you know whether it has been copy pasted in the first place?
The bug may be latent, hard to find, or express itself very differently in different places. It is in fact, a different bug if it originates from code in a different place, even if the code is largely identical.
The example in the linked blog is easy to detect, but often duplicate code exists across files, often unrelated. Just today I deleted several React components that literally only varied by name and were intentionally copy-pasted. I only realized because I the one I edited was imported by the same file as others.
I definitely remember having to debug an old colleague's code who did things like turn and scale 2D shapes. The code was written like the original version in the article, except even more dirty. 4 entirely different code paths for turning things depending on the angle. Several dozen thousand lines of code where almost all of the logic was repeated 4 or two times with minor (and difficult to find) differences. Additionally, different functions which needed to do the same intermediate calculations didn't use intermediate functions, the same code was just copied.
Listening to some people you'd think this code was super easy to understand and modify. It wasn't. Because it was missing abstractions.
Turns out, code has bugs. Not only finding bugs was excruciatingly painful due to every function having 10 mutating variables on average. Actually fixing bugs was an exercise in futility because it was impossible to tell whether the same buggy logic wasn't copy pasted somewhere else in the code. And when you found one, you had to understand all of the specifics of that other place to make sure you didn't break anything.
Abstractions make things easier to understand by allowing the reader to care only about small parts of the logic at a time. The best abstraction is when you read the name of something and decide not to look into it because the information you need is already clear from the name and usage. What repetitive, imperative golang-style "simple" code lacks is the ability for the reader to not read it. My understanding of code is at a high level of abstraction, preferably as high as is relevant to my task. I'd much rather read code that says sum filter predicate than decipher a for loop and then having to figure out that it does indeed do a sum over a filter. Going down this low in abstraction is only useful for intro level programming students.
Then again, golang is clearly aimed at being not a low level programming language, but a language for programmers at a low level of programming.
The code from the article is at some point going to need to be abstracted probably. Unless it is 'done'. As they add much more to it and they will be in the exact situation you are talking about. That is a tough call sometimes when to just bash it out and move on or DRY it up. I personally would toss a comment in there for saying exactly as much. That is going to be a future mess (but not yet).
That whole article though I see as a total communication breakdown. Between the two devs and the manager. That the other dev just invoked 'management chain button' is not a good sign for a good working env. Also the new version did not ssem much better than the prev version. The trust of the developers is broken and the manager did not help and the dev who wrote the blog post now will be shy of doing things.
The problem with these kinds of articles is that they disprove an idea but leave you without any concrete guidance on what you should do instead. Yes, abstractions could become obsolete because we didn't foresee the future (rightfully so), but what would you do instead? At any point in time, you have a limited amount of information and you should come up with the least stupid code that you can. Overdoing abstractions almost always leads to this terrible result, but you can't just call it "goodbye clean code", this is more of a marketing post than an actual useful informative one.
The long answer is a bit complicated, but the short answer is you should duplicate code.
It is far less work to dedupe WET and abstract later once you truly understand the correct abstraction than it is to try to untangle the wrong abstraction later.
Or as Sandi Metz would say, "Duplication is far cheaper than the wrong abstraction."
Does every post need to propose a new One True Way?
Letting go of the Old Way is just as valuable without having a preferred replacement.
It's similar to the bell curve meme about any niche/skill. A beginner only focuses on the fundamentals, a "mid-level" focuses on all the best tools & methodologies (to the point of overcomplicating things), and the master only focuses on the fundamentals.
I don't care about clean code. I care about readable code, easy to change code, efficient code. Of course, I applaud the virtues of OOP, SOLID, design patterns if I sense an interviewer is an Uncle Bob disciple. But I try to steer clear of Uncle Bob disciples, as I don't like living in the kingdom of nouns. [0]
> My boss invited me for a one-on-one chat where they politely asked me to revert my change. I was aghast. The old code was a mess, and mine was clean!
Sounds like a weird thing, if not toxic, of the boss to do. Is the boss a developer? Then why are they the boss and not an equal? Are they in a managing position? Then why do they get involved with the developer's work? Don't they trust their own employee?
I could imagine it maybe, if they were a business of 10 people. Or perhaps the title "boss" is wrong and they meant senior and they themselves being junior dev or so. Otherwise it is really weird and the boss should get out of their face and let them do their job.
Even if they can it doesn't mean they should. People managers should manage people and let engineers engineer. Even if they are former engineers themselves
I've written this pattern 10s of times now. There's a lot of ways to get it wrong according to my normal[0] but going and doing something weird in the "boring" interaction handler code usually burns me. I'd argue for "cleaner" code here.
Every code base has it's tradeoffs, though. But I don't think the conclusion should be "goodbye, clean code". Clean code is a goal for maintainable code; code bases you expect to last >1 year. If your goal is to iterate fast then you shouldn't be looking for maintainable code. And that's perfectly fine! Just don't be surprised when your <1 month code lives >1 year and you're sad.
[0]: My "normal" is to abstract as much of the math as possible to Math-only objects (working with "Box", "Rect", etc type math classes) which makes rigorous unit testing easy. Writing all the math _once_ in an update handler and call from start/move/end/whatever depending on the context and how much info you have. It's the easy to draw all the graphics based on your math objects that are now up to date, however that needs to be done.
If the author had written thorough unit tests first, they could have then engaged in fearless refactoring, they would have been orders of magnitude less likely to have broken any functionality, and what's more, nobody else would have cared about or minded the change in code.
Write whatever dirty abomination you want. Then unit test the crap out of it. Then refactor it into the pristine shining epitome of every pattern fanboy. As long as it works, before and after, nobody will berate you for it.
> As long as it works, before and after, nobody will berate you for it.
This is not the reality in most places. People care about aesthetics, and about practical issues like how maintainable the code is, even if the code is thoroughly tested.
336 comments
[ 3.2 ms ] story [ 304 ms ] threadThe resize functions themselves only looked repetitive because they were forming an interface, which is why refactoring them away was wrong. But the math they performed is a perfectly valid target for cleaning the code.
The end result is of course a huge bag of small, pure functions, but they're easy to test and refactor so it's never an issue.
There are lot of ways/patterns for solving the same problem... but tearing apart an existing interface being used by others isn't a good start.
The best part about abstraction is that you can modify the underpinnings to be more flexible without hurting the rest. A good step would be to create more flexible math functions underneath, like you're saying, and leave the rest alone.
Afterwards, there can be deeper conversations with the team about how they want the interfaces to communicate to provide an obvious facade but an abstract implementation.
I feel that Clean Code and Uncle Bob is like modern tiktok influencer hustlers. It's feel good whishy-washy crap that has some amount of truth but in the end it ends up being very non-productive
Not to mention UB was all butthurt the other day when people found out a conference he was invited to had fake speakers in its lineup. But hey, I guess it takes one to defend one.
Firstly, what does this have to do with clean code?
Secondly, “butthurt” is not a word I feel correctly expresses his comments. From what I can tell, he advocated for a skeptical and cautious approach, and wanted due process to take its course instead of mob justice to run with the first impression, as it always does.
<https://x.com/unclebobmartin/status/1729465103781695982>
> Confirmed speakers at DevTernity 2021, 2022 and 2023 and JDKon 2024 do not exist. There were & are no such engineers at Coinbase or Meta.
> I shared this fact and said no conference should list fake speakers.
There is no "crime" there per se, having a fake speaker in your lineup is a doozy, but it's different from, let's say, saying Bill Gates (with his picture) will be at your conference with no attempt of doing so.
Now I do think the reasons listed by the conference organizer are feeble at best, but (moderately) plausible. I'd never put a placeholder saying so and so are "Engineer at Coinbase" and giving them fake names/pictures
You seem to be making the same misinterpretation of my comment as people in that thread; simply because some comment is not 100% supportive of one side, you think it must therefore be defending the other side. He expressed concern that mob justice was being perpetrated rashly, and people are interpreting that as support for the other side. Similarly, I was arguing against your categorization of his argument as being “butthurt”, and you immediately think that I am supporting the other side of the argument, and therefore present me with rebuttals to that argument, instead of what I actually wrote.
Also have u seen refactors in clean code?
Guy refactors thread safe code by introducing static variables which make code not thread safe, but hey! At least it is a little bit shorter!
The best part is that the same commit added unit tests so our coverage grew over the magic 80% bar, but of course unit tests don't usually test for race conditions.
This is actually a big reason for the popularity of the book, showing a "one true way".
And this carried on to followers of the style. Some linters even enforce the style without much regard for the practicality of it.
That's Clean Code.
> Many of the recommendations in this book are controversial. You will probably not agree with all of them. You might violently disagree with some of them. That’s fine. We can’t claim final authority. On the other hand, the recommendations in this book are things that we have thought long and hard about. We have learned them through decades of experience and repeated trial and error. So whether you agree or disagree, it would be a shame if you did not see, and respect, our point of view.
It's been a while since I've read Clean Code, but I seem to recall it stated many times that blindly applying the rules of Clean Code without good justification would lead to bad code. The author even provides examples of this. People in this thread are criticising Clean Code principles as if they are meant to be a rigidly enforced dogma. They aren't, and the author never intended them to be so.
To a beginner, it reads like ”these are subjective matters so experience is king, and we have more experience than you do”.
And yet, that's clearly their target audience.
I personally derived a lot of value from reading the book, and I feel like my skills noticably improved from before to after. So perhaps my own biases are showing, but I believe discounting the book's contents wholesale is a mistake. There is a lot of value to be garnered from reading it. The book doesn't have to be the infallible word of the Software Gods for it to be useful.
That disclaimer filters out anybody that isn't at least on the transition to be a senior dev (with real seniority, not just in inflated title). It takes quite a lot of experience to agree or disagree with a rule, and respect a point of view without automatically applying it.
In fact, since the rules on the book have way deeper impact than they look like, being able to read that book and not getting damaged by it is a good test for seniority.
But, funny thing, if you are mature enough to fit the disclaimer, you've necessarily already seen all that the book talks about and don't need reading it.
With practice, one soon comes to understand why you might also want to solve for Y. Those with experience are going to ignore what everyone else says anyway, so it doesn't matter if it is not true for them.
Indeed, we in this industry are bad at practicing, and that's a problem. Imagine being Taylor Swift and having your guitarist for the night's performance having picked up a guitar for the first time yesterday. That would never fly, yet that's often how we treat software development – and that's how we get to these kinds of places.
I wouldn't worry too much about somewhat duplicated, but easily understood code, like the original example.
I would however worry if I have to fix the same issue in 3 places, or add code for a new feature in 3 places. That's when I start wondering if I can push the code I need to change or add into a common place somehow.
You still reduce repetition, you can still change stuff in a single point, but you retain the ability to make custom changes if needed.
Amen. With the caveat that this is a guideline, not a rule.
A "clean code" approach to this kind of code doesn't handle evolving requirements well. It always ends up as a mess of helper functions with multiple code paths within depending on where the function is called from. This can sometimes be abstracted: some people might have noticed already the previous sentence is describing OOP polymorphism. But how would anyone know the exact requirements ahead of time? If we're in such an early phase, any attempt to "clean code" will only result in a bad abstraction, or in overengineering. The right approach is to do nothing, note the possible issue, and wait until more is known about the problem domain.
Been a victim of this myself a few times myself so I normally do exactly what you've suggested and wait until a clear, and still performant abstraction has emerged from the code and slowly refactor-in the abstraction. But it's always better to do this on a nearly completed codebase, so at the end, where you should naturally be looking to remove lines, not add.
Therefore it's insufficient to say things like "avoid bad abstractions" or "make sure you accommodate future usage". No one can predict the future, generally speaking.
Abstraction should be used with extreme caution, even when you feel that it's probably right. Be an extreme skeptic.
As far as generality is concerned, it's not the number of examples that matters. Once you learn to distinguish whether your duplicated code actually duplicates some piece of knowledge or just happens to resemble another piece of code by happenstance, you stop having to rely on an unreliable magic number as your limit on how many instances of duplication to allow before abstracting something. And that skill probably correlates strongly with the understanding that abstractions are not just macros that make it more convenient to repeat some chunk of code, they actually represent knowledge, and thus their primary use is not to eliminate duplication, but to encode an understanding of a problem domain into the code.
If your clean code hurts those, you made the code worse.
Sometimes you will just have to accept that things take the number of lines they do and having them duplicate in different places is a feature as you (or someone else) might have the reasonable desire to change the individual parts.
Whenever your sense of aesthetics overrides practical concerns you should pause and take a step back.
The trick is always to figure out whether you are in that case or whether you are in the case where someone will want to change the shared logic everywhere at once. In my experience, the latter is vastly more common, and this is what DRY optimizes for.
https://news.ycombinator.com/item?id=29239861 212comments
https://news.ycombinator.com/item?id=22022466 575 comments
Goodbye, Clean Code (2020) - https://news.ycombinator.com/item?id=38366222 - Nov 2023 (1 comment)
Goodbye, Clean Code (2020) - https://news.ycombinator.com/item?id=29239861 - Nov 2021 (212 comments)
Goodbye, Clean Code - https://news.ycombinator.com/item?id=22022466 - Jan 2020 (575 comments)
We all agree on what an Applicative is supposed to do. The challenge shifts right in the idea-to-code pipeline to finding the right Applicative for the problem. Most OO design patterns have natural, well defined equivalents in such an environment, but the difference is this way helps minimize the amount of project specific abstractions you have to deal with.
The downside as a beginner is of course a greater upfront cost to learning the language, and quite frequently opening up another Haskeller's work and realizing there are like 12 compiler extensions you've never heard of that you now need to grok. But even there I would argue that's a net win: you can carry those compiler extensions with you to all projects in the future. (The downside as an expert is realizing you have to get a PhD to get a sense for how much RAM your cats will gobble up!)
There's also a lot to like about Haskell. Complaining about something which you don't understand isn't a good look on anyone, though.
How do you feel about the increase in project-specific abstractions in Go? Or do you disagree with that framing?
Go one step further with affine types and use Rust. You get haskell type system with predictable performance.
No. When trying to use Rust's traits like Haskell you learn really fast why higher kinded types are needed.
On the specific example in the blog post though I can only say that there are so many times I've been down the clean code path and it's more often not another person, but the future you, into whose foot you are shooting. Abstraction is not beneficial when you need to study it later to understand it again.
This is where a good study routine comes in. I study Haskell like I study all things of any long term importance, using an Anki deck. It takes me longer to start using a new abstraction, but once I have it it's pretty much there for life, due to my daily commitment of half an hour or so to my reviews. It makes far less sense, of course, to do this for a one-off abstraction I had to apply to a specific project, so this naturally tilted me over the years to work more and more in loss like Haskell.
(N.B., I do not state this as a prescription. I think this is well above and beyond what most people expect of themselves professionally, and that is okay. Everything in life comes with tradeoffs.)
The latter part is often implied, but it's best to make it explicit. You're trying to build up expert intuition, which means allowing for the fractal nature of software development to mean you might be in the 20% of times where this won't work for you.
No OO design pattern has a law, they are a loosely defined concept/pattern which means they are open to personal interpretation/customisation but no concrete law to say exactly what it is and its expected behaviour/output.
Well, we actually only know what it is supposed to do. The laws are not enforced, so it is perfectly possible to implement a type class that violates the laws of the type class.
It is also the case that implementations of these very abstract classes typically only also have very few implementations that make any kind of sense type-wise and that the 'obvious' one is correct.
(Just to preempt: Yes, tests aren't proof, but such is life without a full proof system in the language. In practice, I've found property-based testing sufficient.)
I think he would have agreed to Haskell (or at least, languages based around lambda calculus) is more in the first category, whereas Java, C++, Smalltalk, Javascrip (ie. OO languages) are in the second. Computational fundamentals versus engineering problems.
[1] https://thestrangeloop.com/2015/propositions-as-types.html
> law to say exactly what it is and its expected behaviour/output. What does Monad laws say about expected _behaviors_ of some Monad?
The 'rule' that unifies OOP's other rules is tell-don't-ask. So for the example at hand we have the handles which are shared in a known way and I presume shape-specific control points (e.g. circle may have center, radius). Then a change of a box handle could call a updateControlPoints(changedHandle). There would also be updateBoxHandles(changedControlPoint). There may be other clean splits, but that's how I'd do it.
Maybe. But not on which parsing library to use (parsec? megaparsec? attoparsec? regex-applicative?), on whether to use Data.Text.Strict or Data.Text.Lazy, on how point-free the code should be, on whether to use "." or ">>>" or any other combinators, on which language extensions to use...
I like Haskell, but it suffers from similar problems as any other language: people can't really agree on the best way to do things
- actually write a story relevant to the lesson learned, and not make up some fable that takes 1/3 of the article before even starting to make a point - not have any sort of popups asking me to subscribe or login - not have any sort of annoying javascript animation that gets in the way of reading the article
it's rare enough to be noted.
> Use the platform, use JavaScript !!
IMHO the core problem of the situation described in the article is that the guy simply rewrote a piece of code without before talking to the author/maintainer/owner of that piece code.
Paragraphs for his publications had to be short due to the limitations of magazine layouts.
“For the bulk of your work, a paragraph can only be one or two perfectly written sentences” he insisted.
I feel like there’s a parallel from that to writing functions.
Magazine readers, and programmers both read high density text rather quickly.
We should make it easier for future readers to quickly understand your point/function.
We should write short, correct functions.
Only add abstraction when you absolutely have to and write more documentation in the case that you do.
Edit: missed the length of the function comment, ignore my diatribe
You get a PR to review and you rewrite the code without talking about it? Yes, that's a besserwisser.
there is a talk by James Power [0] during which it he speaks about whether the repetition is intention or coincidental.
[0] https://www.youtube.com/watch?v=-z2eqLwVmzw
> Secondly, nothing is free. My code traded the ability to change requirements for reduced duplication, and it was not a good trade. For example, we later needed many special cases and behaviors for different handles on different shapes. My abstraction would have to become several times more convoluted to afford that, whereas with the original “messy” version such changes stayed easy as cake.
Clean code is easy to understand and to change. Ideally, any given file or method only deals with one concept.
Messy code usually results from someone merging concepts to avoid duplication. After that happens, differences in the concepts cause the code to become much harder to reason about.
I've seen this particular problem happen a million times. The worst code I ever stepped into was completely DRY. When, where, and why something happened wasn't something the original authors could answer. That's because everytime they found code duplication, they'd abstract. And everytime that didn't work, that ended up adding little "if (this instanceof Foo) special case;" to get over the most recent bug.
Very different, but also informative :)
It would have been very unfortunate if it were. If an article, entitled "Goodbye, Clean Code", had been written in response to Uncle Bob's twitter life, what it would have demonstrated would be that the author thinks that people with bad opinions have nothing of value to teach about their area of expertise. Too common a position these days.
This might be an issue in itself sometimes. People identifying with the code they wrote. When you are part of the team, the code belongs to everyone. Everyone should be able to change it if it makes sense and all quality gates are passing.
Yes, but actually no. Context is important here. If that code had been written six months ago or even a couple of weeks ago? Have at it. But, the OP states "My colleague has just checked in the code that they’ve been writing all week." then "It was already late at night (I got carried away)."
If spent all week on something, working out the all the details, checked it in at 5pm, and then came in the next morning to discover that someone stayed up rewriting it rather than waiting till the next morning to talk to me about it first, I'd be absolutely livid.
In finance, we are constantly dealing with products which are somewhat alike but not quite. Equity options vs Fx options ? Sure, they're options, they have a strike, we price them using some version of Black Scholes, etc. But oh boy they are so different.
The temptation for new joiners to factorize a lot of behaviors is huge, and many times we are in the shoes of the OP's boss, telling the younger devs to remain calm and keep things separate.
It's not about saying goodbye to clean code, it's about actually maintaining a clean code by not overabstracting.
But the title says so! :p
I get the whole point of this post, but it can easily be read as an excuse to actually messy (as opposed to "non-clean") code. The author doesn't actively distinguish them, so others do.
Yes, but we are talking about geometry. What the (usual) handles do are affine transformations (translation, rotation, scaling and mirroring), if the scaling is uniform, these are similarity transformations. And are guaranteed to work for any shape. Which leads to the real problem: you can't (well, shouldn't) abstract something, if you don't have enough knowledge about that - especially what new possibilities may show up in the future. For example if somebody thinks that the only possible ovals are circles and ellipses, they will be quite surprised when the first "real" oval shows up.
But math doesn't deal with mutability all that much. It's interested in the visible properties and constraints. A square is a rectangle because it meets all the constraints and has all the properties of a rectangle. But that is no longer true if the shapes are mutable. A rectangle might appropriately have `setWidth` and `setHeight` operations. A square cannot implement these operations and still obey Liskov substitution, without the ability to downgrade its type to a rectangle. In OOP you might correct this by making the square immutable and the `setWidth` and `setHeight` operations would return a rectangle instead of a square.
To bring this specifically to this use case: Yes, it's a defined mathematical operation to scale a shape such as a square on the x-axis. But, if you do, that square is now a rectangle. This may be important to the implementation, it may not. But it's certainly a relevant technical concern.
DRY has value but abstractions come at a cost. It's always a tradeoff.
But I really find this difficult. How to decide what abstraction is ok and which is not? Since I did this mistake a few times, I now sometimes tend to try to think a lot about this, which slows me down a lot, and in the end, I still not really sure whether I made the right decisions.
Any suggestions how to decide such things?
See also John Carmack about code duplication: http://number-none.com/blow/blog/programming/2014/09/26/carm...
I have read much advice on this topic, but still, I think I often struggle to make the right abstractions.
I have the same feelings as you here, I often feel a little lost trying to decide how much duplication is appropriate in a given case.
I try to not overthink it in the difficult cases. There are many other places in code where it's more obvious, because you can see the history of how people worked with the code, what was the frequency of changes and edge cases added to it etc.
Often it's also just about the readability of the code, I tend to go by the "how confusing it will be for the next person if I defuplicate this code in a smart way" metric.
I can remember lots of times where debugging an abstraction was very time consuming, though.
I’m also against DRYing up unnecessarily, but if the code is a 100% match in intention and/ord behavior, then it’s going to be deleted.
I mean, you'd see that the function is inlined in the place that you discover the bug, so you already know that changing that function's code isn't going to fix similar bugs anywhere else right?
The bug may be latent, hard to find, or express itself very differently in different places. It is in fact, a different bug if it originates from code in a different place, even if the code is largely identical.
I definitely remember having to debug an old colleague's code who did things like turn and scale 2D shapes. The code was written like the original version in the article, except even more dirty. 4 entirely different code paths for turning things depending on the angle. Several dozen thousand lines of code where almost all of the logic was repeated 4 or two times with minor (and difficult to find) differences. Additionally, different functions which needed to do the same intermediate calculations didn't use intermediate functions, the same code was just copied.
Listening to some people you'd think this code was super easy to understand and modify. It wasn't. Because it was missing abstractions.
Turns out, code has bugs. Not only finding bugs was excruciatingly painful due to every function having 10 mutating variables on average. Actually fixing bugs was an exercise in futility because it was impossible to tell whether the same buggy logic wasn't copy pasted somewhere else in the code. And when you found one, you had to understand all of the specifics of that other place to make sure you didn't break anything.
Abstractions make things easier to understand by allowing the reader to care only about small parts of the logic at a time. The best abstraction is when you read the name of something and decide not to look into it because the information you need is already clear from the name and usage. What repetitive, imperative golang-style "simple" code lacks is the ability for the reader to not read it. My understanding of code is at a high level of abstraction, preferably as high as is relevant to my task. I'd much rather read code that says sum filter predicate than decipher a for loop and then having to figure out that it does indeed do a sum over a filter. Going down this low in abstraction is only useful for intro level programming students.
Then again, golang is clearly aimed at being not a low level programming language, but a language for programmers at a low level of programming.
That whole article though I see as a total communication breakdown. Between the two devs and the manager. That the other dev just invoked 'management chain button' is not a good sign for a good working env. Also the new version did not ssem much better than the prev version. The trust of the developers is broken and the manager did not help and the dev who wrote the blog post now will be shy of doing things.
I think maybe what you mean is it doesn't give you any guidance on when to do that, other than "sometimes".
That's just a matter of taste and experience. It depends on the specific code and circumstances.
It is far less work to dedupe WET and abstract later once you truly understand the correct abstraction than it is to try to untangle the wrong abstraction later.
Or as Sandi Metz would say, "Duplication is far cheaper than the wrong abstraction."
Letting go of the Old Way is just as valuable without having a preferred replacement.
It's similar to the bell curve meme about any niche/skill. A beginner only focuses on the fundamentals, a "mid-level" focuses on all the best tools & methodologies (to the point of overcomplicating things), and the master only focuses on the fundamentals.
[0] https://steve-yegge.blogspot.com/2006/03/execution-in-kingdo...
Sounds like a weird thing, if not toxic, of the boss to do. Is the boss a developer? Then why are they the boss and not an equal? Are they in a managing position? Then why do they get involved with the developer's work? Don't they trust their own employee?
I could imagine it maybe, if they were a business of 10 people. Or perhaps the title "boss" is wrong and they meant senior and they themselves being junior dev or so. Otherwise it is really weird and the boss should get out of their face and let them do their job.
Every code base has it's tradeoffs, though. But I don't think the conclusion should be "goodbye, clean code". Clean code is a goal for maintainable code; code bases you expect to last >1 year. If your goal is to iterate fast then you shouldn't be looking for maintainable code. And that's perfectly fine! Just don't be surprised when your <1 month code lives >1 year and you're sad.
[0]: My "normal" is to abstract as much of the math as possible to Math-only objects (working with "Box", "Rect", etc type math classes) which makes rigorous unit testing easy. Writing all the math _once_ in an update handler and call from start/move/end/whatever depending on the context and how much info you have. It's the easy to draw all the graphics based on your math objects that are now up to date, however that needs to be done.
If the author had written thorough unit tests first, they could have then engaged in fearless refactoring, they would have been orders of magnitude less likely to have broken any functionality, and what's more, nobody else would have cared about or minded the change in code.
Write whatever dirty abomination you want. Then unit test the crap out of it. Then refactor it into the pristine shining epitome of every pattern fanboy. As long as it works, before and after, nobody will berate you for it.
This is not the reality in most places. People care about aesthetics, and about practical issues like how maintainable the code is, even if the code is thoroughly tested.
Also the point here is about cognitive cost of working on the refactored code, not broken functionalities.