Especially if there is a very common refactoring bug and you get a pepperoni pizza instead of a pizza with one of the most natural toppings like pineapple.
But I tend to agree with the author. Sometimes verbosity is the lesser evil. No suggestion should become a dogma and whoever played some games of code golf knows that short code doesn't mean code that is easy to read. Extreme example of course. But I believe many start to optimize in this way just as a way to reduce the line count.
Still, there is still room for some kind of factories or function templates (not in the c++ sense). I think a user is allowed to repeat himself but then again a user is just another arbitrary layer again. But if such helpers are to be implemented, I tend to like it in a place where the user is invoking said helpers and not on the level below that if that makes sense.
> ...Especially if there is a very common refactoring bug and you get a pepperoni pizza instead of a pizza with one of the most natural toppings like pineapple.
Exactly! The DRY'ed example in the first section should read rather:
> "Had we started out with two pizza types that have different crust/sauce/cheese, we never would have made this refactor. Instead of our code being architected around the concept of how pizzas are made in the abstract, its architecture is tightly coupled to the specific needs of these two pizzas that we happened to be dealing with. The chance that we will be putting this code back the way it was is extremely high."
Maybe.
But maybe you're falling prey to the other programmer trap - catering for conceivable situations that are just never going to happen, and making your codebase unnecessarily accommodating as a result. This is another great source of complexity, and quite often the source of unnecessary abstractions (which add to cognitive load) too.
In my experience it's better to cope with half-and-half pizza toppings when they arise, rather than coding as if they're already needed. Because when they are needed, you'll probably find the requirement is actually to put them on a 3-tier wedding cake, or a car.
People should reconsider writing articles like these. It's just a list of (3!) criticisms. You can still have your click-baity title, but why not write about when and how to use XYZ principle instead?
What forced me most to use DRY in inappropriate ways is typing out blocks of the same code again and again. Then I realized that and began to maintain and use easily expandable snippets with fillable placeholders. It turned out that my mind had no objections against repetitive code at all, and the clarity of it has only increased, due to the lack of context switches and parametric entanglement.
We do this because, unlike developers, CPUs can’t learn anything. No reusable abstractions are possible without extra instructions (and cycles) that tell the CPU exactly when and how to reuse them, or new microcode firmware that ordinary users aren’t empowered to write.
Yes, e.g. when I need to add a new line/block of code, I just search for a pattern and edit there. When refactoring demands heavy structural cross-module changes, I just don’t do it honestly. What’s dead is dead, but I may do a “guided” side by side rewrite.
I don’t touch snippets unless there is a good reason to do that. They are my general templates, not per-project tools.
In most of my code, the need for refactoring was mostly a consequence of building a too rigid high-tech structure which with time turned out to not fit the job anyway. Figured out I can avoid it by not building it, and antiDRY also plays a role in it (albeit mostly psychological).
I have taken what you might consider low-DRY approach also so it's interesting to see what mechanisms people use to manage reuse without introducing complexity into the system itself.
I think taking another look at project agnostic generators is something worth doing too. It has the benefits of automating some of the duplication without the rigidity of deep abstractions and dependencies. I am still exploring that though.
I mean, if you have repetitive pieces of code that can be parametrized, why not parametrize them directly in the code instead of using a tool to produce the same code over and over?
E.g. I rarely abstract http calls or endpoints, because APIs tend to have nuances and to account for them I have to add more parameters than is worth. So I have a full-blown snippet from which I delete irrelevant parts.
Another example is little utilities like a promise as a semaphore (by lifting resolve() to the scope). It could go as a utility function without parameters, but then I’d have to maintain that utility module across projects, which makes it extremely fragile, and force other people to deal with it.
> Instead of our code being architected around the concept of how pizzas are made in the abstract, its architecture is tightly coupled to the specific needs of these two pizzas that we happened to be dealing with. The chance that we will be putting this code back the way it was is extremely high.
Mistake 1: Switch from DRY to premature optimization.
> You might think that legit reasonable developers but would not actually do something like this and would instead go back to the existing invocations and modify them to get a nice solution, but I've seen this happen all over the place.
Mistake 2: Assumption of incompetence to support your argument.
> . As soon as we start the thought process of thinking how to avoid a copy paste and refactor instead, we are losing the complexity battle.
Mistake 3: Strawman argument. DRY does NOT lead to over-complicating things.
Overcomplicating things leads to overcomplicating things.
Now, i wasted 5 minutes, so you can waste some more to reply to this comment, instead of completely ignoring this dumb random blog post.
I don't read coding opinion articles like OP but I like to check out comments.
> DRY does NOT lead to over-complicating things.
That is not true. I dive around foreign code bases a lot and dry-ness is actually a significant complicating factor in understanding code, because you're jumping around a lot (as in physically to different files or just a few screens away in the same file). As in, inherently every time it's used, not just in situations where it's used in a complicated way.
This sounds dumb but it just simply is much harder to keep context about what's going on around if you can't refer back to it because it's on the same screen or one short mouse scroll above or below your current screen.
That obviously doesn't mean you should leave copy pasted versions of the same code over your code base. But it's important to consider that refactorization of that code into something common that gets called from multiple places as something that you don't get for free, but that is an active trade off which you usually have to apply to prevent bugs (changing one code location and not the other) or simple code bloat. In practice this is very relevant when you suspect something might be repeated in the future, but you're not sure. Imo: Just don't factor it out into anything, leave it there, in place, in the code.
What does `make_pizza()` do? It could be a lot or it could be a little. It could have side-effects or not. Now I have to read another function to understand it, rather than easily skimming the ~four lines of code that I would have to repeat.
I think the article fails to show particularly problematic examples of DRY. E.g. merging two ~similar functions and adding a conditional for the non-shared codepaths. shudders
Yup. But I guess that typically happens in steps. So next DRY-programmer that comes along will add a cheezeFilledCrust boolean to that make_pizza function and so on. Every time it will seem more reasonable to add another boolean, because otherwise you have to remove the make_pizza function, and there would be SO MUCH CODE DUPLICATION.
I’ve seen this again and again in the field and I wholeheartedly agree with the sentiment in the OP. IMHO different code paths should only share code if there is good reason to believe that the code will be identical forever.
Now the next genius turns up and says that make pizza is at it's core always a n-step domain process.
So now you've dumped it down to an interface with a default implementation which calls the create_dough, add_toppings, bake_pizza interfaces in order, each of which are either passed in callbacks or discovered through reflection.
We can even sprinkle in some custom DSL to "abstract away" common step like putting the product into the oven correctly!
Jr's will never understand when why and what is effectively excecuted at runtime. Honestly, at this point I enjoy working with this kind of code. It's always such a high entertainment value and I get paid by the hour, so whatever
Some languages handle massive parameter lists better than other (ex with defaults). There are also design patterns for this type of problem (ex a PizzaBuilder).
The strength of the reaction made me realize just how widespread and intractable the "wrong abstraction" problem is. I started asking questions and came to see the following pattern:
1. Programmer A sees duplication.
2. Programmer A extracts duplication and gives it a name.
This creates a new abstraction. It could be a new method, or perhaps even a new class.
3. Programmer A replaces the duplication with the new abstraction.
Ah, the code is perfect. Programmer A trots happily away.
4. Time passes.
5. A new requirement appears for which the current abstraction is almost perfect.
6. Programmer B gets tasked to implement this requirement.
Programmer B feels honor-bound to retain the existing abstraction, but since isn't exactly the same for every case, they alter the code to take a parameter, and then add logic to conditionally do the right thing based on the value of that parameter. What was once a universal abstraction now behaves differently for different cases.
7. Another new requirement arrives.
Programmer X.
Another additional parameter.
Another new conditional.
Loop until code becomes incomprehensible.
8. You appear in the story about here, and your life takes a dramatic turn for the worse.
Existing code exerts a powerful influence. Its very presence argues that it is both correct and necessary. We know that code represents effort expended, and we are very motivated to preserve the value of this effort. And, unfortunately, the sad truth is that the more complicated and incomprehensible the code, i.e. the deeper the investment in creating it, the more we feel pressure to retain it (the "sunk cost fallacy"). It's as if our unconscious tell us "Goodness, that's so confusing, it must have taken ages to get right. Surely it's really, really important. It would be a sin to let all that effort go to waste."
Not really a sunk cost fallacy. Existing code needs to be maintained. Some of it should be deleted since it costs more to maintain. Some of it shouldn’t be deleted since it might bite you in the behind when you realize that all of that code was correct (although gnarly) and now you’ve introduced regressions. And which code is which? Hard to say.
Sunk cost (fallacy) is about making decisions based on things that you have already lost. But you haven’t lost or expended the code—the code is right there, and it’s hard to know if it’s more of an asset or a burden.
> What does `make_pizza()` do? It could be a lot or it could be a little. It could have side-effects or not. Now I have to read another function to understand it, rather than easily skimming the ~four lines of code that I would have to repeat.
This is not a problem of DRY. This is a problem of wrong abstraction and naming. If the function is just four lines, it could easily be named `make_and_cook_pizza`. In the alternative scenario where those four lines are copy pasted all over the place, one is never sure if they are exactly the same or have little tweaks in one instance or the other. Therefore, one has to be careful of the details, which is much harder than navigating to function definition, because in this case you cannot navigate to other instances of the code.
Exactly this. I fixed a problem like this a week ago. I found some duplicated code, factored it out into one place by introducing an abstract base class (Python) and in the process discovered one of the duplicated methods had a logic error leading to returning a slightly smaller integer result.
The code had test coverage, but the test confirmed that it produced the wrong result. I had to fix the test too.
in a sense yes, in a sense no. if you see a function and know its sort of black box properties and its inputs and outputs are well defined, you really don't need to care. however, that applies whether the code is in an external function/module or physically inlined into your code. the sectioning off into separate code is then there to forcefully tell the reader "don't even try to care about the implementation details of this", so in practice your point still applies.
however... real software doesn't work like this. the abstractions that work that way exist for a select few very well understood problems where a consensus has developed long before you're looking at any code.
math libraries would be a typical example. you really don't need to know how two matrices are multiplied if you know the sort of black box properties of a matrix.
but the minute functions, classes, and other ways of abstraction code in a DRY way that you encounter constantly in everyday code, even if they are functionally actually well abstracted (meaning it does an isolated job and its inputs and outputs are well defined), even for simple problems, are typically complex enough that learning their abstract properties can be the same level of difficulty and time investment as learning the implementation itself. on top of practical factors like lack of documentation.
this is also why DRYness as a complicating factor really doesn't factor in once the abstracted code does something so complex that there is no way you could even attempt to understand it in a reasonable amount of time. like implementing a complex algorithm, or simply just doing something that touches too many lines of code. in this case you are left to study the abstract properties of that function or module anyways.
which is much more understandable than eight lines of regex magic where you don't even know what the regex is doing.
The problem of not knowing what it does or whether it has side effects or not is more a problem of naming and documentation than DRY. Even then, it's still better than repeating the code all over, simply because when you read and understand the function once, you don't need to go back. On the other hand, if the code is all over, you need to read it again to recognize it's the same piece of code.
if those 8 lines of regex have been unit tested and the function is commented to describe "what" the code does, it is entirely the point that you don't need to understand how it works
additionally, the function should be stateless and have no side effects ;)
How do you test 8 lines of regex inside a function that does more things? And what's easier, to write and read the function name or copy-paste the lines with the comment (if the comment explaining what that piece does is even written, that is)?
Except that there should be only one way to make a filename from a string. Maybe some options like "allow_spaces" if needed but the point of DRY is not only to share code but to share algorithms.
But that is usually a problem with abstraction, rather than a problem with a method call. If I can trust what make_pizza does, that is much faster to read than any four lines of code.
A functional style certainly helps. I get the pizza in my hand and don’t have to worry that anyone left the oven on.
You can't, unless it's in a standard library or a core dependency used by millions of people.
That's one of the reasons why functional code is generally easier to read. A lambda defined a few lines above whatever you're reading gives you the implementation details right there while still abstracting away duplicate code. It's the best of both worlds. People who's idea of "functional programming" is to import 30 external functions into a file and compose them into an abstract algorithm somewhere other than where they're defined write code that's just as shitty and unreadable as most Java code.
>> You can't, unless it's in a standard library or a core dependency used by millions of people.
You can if you have reasonably competent colleagues. And if you do make some wrong assumptions about what a certain method does, it should be caught by your tests.
I feel that people that insist on reading and understanding all the code, and write code that has to be read fully to be possible to understand what is does, have missed something quite fundamental about software development.
> I think the article fails to show particularly problematic examples of DRY. E.g. merging two ~similar functions and adding a conditional for the non-shared codepaths. shudders
Not a problem of DRY, but bad code structure.
Just keep the two functions and pull the shared code-path out
Not all the time. When the similar code mixes types and the common codepaths are sprinkled multiple times over it you can either have the code there twice, or have an overcomplicated templated common function.
In these cases factorizing may or may not be a good idea.
I think it's just that for every complex topic, any general rule will break down at some point. That doesn't tell you that the rule is bad, but to learn how to tell when you're dealing with such an exception.
Thanks - I like this point. I think it's probably a better illustration of what I'm trying to say in my third point. Devs are biased towards adapting existing shared code so we end up with shared libraries picking up little implementation details from each of their consumers and ultimately becoming very messy.
DRY makes it harder to actually understand the system as a whole in some sense, since it usually means some indirection has been added to the program. However, it avoids the one thing that actually makes me pull hair out: code that looks the same because it was duplicated but is just different enough to trip you up because each area it was used required minor syntax changes that had major implications for the result.
Repetition also makes it harder to understand a system: not only do you have to read more, you also need to remember and compare repeating fragments that may be identical or just similar.
What makes it easier to understand a system is simplicity. I'd argue that DRY, deployed with a right strategic plan, usually does more to simplify things than does copy-paste.
But, as any tool, DRY is but a tool; to be useful it requires some skill.
> This sounds dumb but it just simply is much harder to keep context about what's going on around if you can't refer back to it because it's on the same screen or one short mouse scroll above or below your current screen.
To note, a common effect of not DRYing functions is an increase in local code length.
In many code bases that lived long enough, that means screens and screens of functions inside the module/class files. It is still easier to navigate than between many files, but not by that much in practice (back/forth keyboard shortcuts go a long way to alleviate this type of pain)
> I dive around foreign code bases a lot and dry-ness is actually a significant complicating factor in understanding code, because you're jumping around a lot (as in physically to different files or just a few screens away in the same file).
I can't agree more. Also, "code reuse" makes debugging significantly harder when trying to reverse engineer some code base. The breakpoints or printf:s get triggered by other code paths etc. And you need to traverse stack frames to get a clue what is going on.
Extra bonus points for fancy reflection so that you have no clue what is going on.
I can't disagree more. DRY forces you to create pure reusable code, and split your code into small pieces. When I read such code I need to understand just a few pieces.
You need multiple cases of duplication (repeating yourself) before you can infer a reusable piece of code.
If you make everything as generic and reusable as possible from the beginning, you'll end up with messy code that has way too much options to set for every simple operation.
Indeed. In any practical optimisation problem, which is fundamentally what all engineering is, there's a sweet spot.
You can't just slam the DRYness knob to 11 and expect it to always be better, any more than you can turn a reflow oven up to 900°C and expect it to be better, just because 380°C is better, for the specific PCB in question, than 250°C.
It also doesn't mean you can turn it off entirely, just as if you look at your charred results at 900°C you don't conclude that "heaters considered harmful".
Also, the problem is strongly multivariate and the many variables are not independent so the "right" setting for the DRYness knob is not necessarily the same depending on all sorts of things, technical and not, up to and including "what are we even trying to achieve?"
That's point. However, I think this is more of a "verbose vs elegant argument". Yes, DRY should not be a religion - I will write more code, possibly duplicated, if I deem it necessary for the code to be more readable that way. It's a judgement call, but I think the basic concept of DRY should still stand. If you find yourself cutting and pasting too much, stop, go get a coffee, take a walk, come back, and see how you can do it better.
> This sounds dumb but it just simply is much harder to keep context about what's going on around if you can't refer back to it because it's on the same screen or one short mouse scroll above or below your current screen.
What's funny is that DRY was first popularised in the Pragmatic Programmer[0] book, and "coincidental" duplication is explicitly addressed right there on page 34, "not all code duplication is knowledge duplication... the code is the same but the knowledge is different... that's a coincidence, not a duplication."
> Mistake 1: Switch from DRY to premature optimization.
"Premature optimization" is largely a bogus concept, because the meaning of "optimization" has shifted a lot since the concept was first created.
People now use optimization to mean "sensible design that does not needlessly waste resources".
In this meaning of optimization, "premature optimization" is a bogus concept.
You should absolutely ALWAYS write non-pessimized code by default.
What the original concept referred to is what people now call "micro optimizations". Sure, premature micro optimizations is often a waste of time. But this is irrelevant to the context of this discussion.
> In this meaning of optimization, "premature optimization" is a bogus concept.
The idea is that you can end up optimizing before you know the entire use-case, because software engineering isn't like building bridges or skyscrapers.
I'm a performance geek, but I love code I can easily change rather than code that is fast until some customers have touched it. Mostly out of experience with PMs with selection bias on who they get feedback from ("faster horses" or "wires we can hook phones to").
The first thing to optimize is how fast you can solve a new problem that you didn't think about - or as my guru said "the biggest performance improvement is when code goes from not working to working properly".
The other problem with highly optimized code is that it is often checked-in after all the optimizations, so the evolution of thinking is lost entirely. I'd love to see a working bit + 25 commits to optimize it rather than 1 squashed commit.
Optimized code that works usually doesn't suffer from this commentary so the biggest opponents I have with this are the most skilled people who write code with barely any bugs - I don't bother fighting them much, but the "fun" people with work understand my point even if they write great code first time around.
These two are mostly why I talk to people about not prematurely optimizing things, because I end up "fixing" code written by 15 or more people which has performance issues after integration (or on first contact with customer).
The reasoning behind discouraging premature optimization makes no distinction between "micro optimizations" and any other kind, the purpose of this guidance is to minimize wasting time building unnecessarily complex solutions based on untested performance assumptions.
If you're writing and enterprise app and lean back in your chair and start to think about speeding things up with loop unrolling and avx instruction sets then you're doing the premature optimization thing.
But trying to limit large nested loops is easy fruit that doesn't take much effort to pick.
the typo is "non-pessimized code", which should be "non-optimized code".
I see humor in thinking if my code is pessimistic enough. Have I assumed that the edge cases will happen and worked around them? Do I expect (and handle) crashes, i/o failures, network timeouts, etc?
"code pessimism" could be an interesting metric.
The typo in the other post was "superb owl" which should have been "super bowl". Several people on that thread enjoyed the typo, including a comment from CostalCoder saying "Please, please do not correct that typo!"
I think they used that term on purpose. Non-pessimized in this case is the same as optimized, and I believe it's a reference to this video https://youtu.be/7YpFGkG-u1w
> Assumption of incompetence to support your argument.
Okay, but it kind of is about incompetence. And by “it” I mean everything. Look, we all remember that first time we all realized that adults are just winging it most of the time. Almost nobody knows what they are doing. Half the people who “know” actually know the least.
> DRY does NOT lead to over-complicating things.
Don’t Repeat Yourself is a terrible acronym because what it stands for is exactly the opposite of what people do. Not doing something is avoidance, opting out, like “don’t push your sister” vs “be nice to your sister”.
What most people do is they realize they have already repeated themselves, or someone else, and they rip it out. They deduplicate their code. Avoidance definitely can “lead” somewhere, but deduplication is active, and that can often be headed the wrong way, either directly or obliquely.
The Rule of Three is much clearer on this. You get one. There’s nothing to do when you see you’ve duplicated code - except to check if you’re the first or not.
No really, you're absolutely on point. The post is not worth the time, the case against DRY is too weak.
Sounds like a kid complaining about pushing DRY in a direction that overcomplicated things for him because of himself and instead of improving himself he choosed to attack "an uncomfortable principle".
> Mistake 1: Switch from DRY to premature optimization.
Mistake 1a: Conflating the term "premature optimization" - it doesn't apply here. Premature optimization is about runtime performance, DRY is about optimising maintenance overhead.
Mistake 1b: (good) DRY can't be done early (it's a continuous process throughout project development).
> Mistake 2: Assumption of incompetence to support your argument.
Mistake 2: Assuming you're never working in teams leveraging peers' of varying experience and technical focus.
The presumption of re-usability is absolutely the most common red flag I've seen with DRY: I've seen it with a lot of very senior / experienced devs. You can call them incompetent, but there's plenty of them and we have to work with them. Articles like this help.
> Mistake 3: Strawman argument. DRY does NOT lead to over-complicating things. Overcomplicating things leads to overcomplicating things.
This statement concerns me. DRY very obviously and demonstrably leads to over-complicating things (excessive / ballooning parametrisation is just one of many very simple examples of this). If you can't see this I would have my own concerns about competence...
Thanks for having my back. #3 is an overwhelming real world phenomenon. In fact, I posted my article on reddit and someone wrote back a comment with a huge OOP solution that would mitigate all my problems. Not sure that reader got to point #3.
> Overcomplicating things leads to overcomplicating things.
This would be the most efficient title, subtitle, and entire contents of most posts about programming principles.
However, each reader has to have a similar enough perspective, background, and experience to understand and apply it. In that sense, the trend line measuring the value of commenting about comments about random blog posts indeed indicates wasted time, but hopefully it's a local minima.
My pithy corollary to your helpful tautology is a quote from Tommy Angelo that's stuck with me since my poker days: "The decisions that trouble us most are the ones that matter least."
Decisions are necessarily difficult to make when the expected value of either outcome are similar. We waste an awful lot of time on choices that could have been made just as well with a coin flip.
So there you go world: two quotes that are generally useful about generalities that are locked, loaded, and ready to shoot you in the foot when misapplied.
> Mistake 3: Strawman argument. DRY does NOT lead to over-complicating things. Overcomplicating things leads to overcomplicating things.
Sure, I agree, except DRY is probably the second greatest gateway drug to overcomplicating things to OOP. Actually, they really hand-in-hand since OOP features are often used to DRY things.
DRY can easily go too far because fundamentally it's about centralizing ideas with the premise that different operations can and should share units, even though a "writeSomeFileToDisk" function doesn't necessarily have to do the exact same thing between different higher-level operations. Because so many engineers emphasize "elegance", if a set of functions seem similar enough, they pressure themselves to write code that is shareable, hence more abstract. Abstractions are inherently more complicated and hard to understand, not the other way around. Rather than having very simple "molecules" of code that can be understood on their own, there is instead a much larger molecule of nodes that are connected by abstract dependencies, and those nodes may only have dependencies in common.
DRY should be done sensibly, but teaching DRY is a problem in our industry because we don't teach engineering discipline. We teach principles like DRY and OOP, and even YAGNI as if they are tenets of a religion.
Article tl;dr: Design is hard and can't be boiled down into applying pithy mindlessly.
For what it's worth, I agree with your points and disagree with the various counterpoints that were posted; "optimization" can mean a lot of things, and I for one understand what you mean.
DRY is better for performance (cache efficiency). It’s also less work for the compiler. Those might not be concerns of someone writing pizza CRUD in python.
DRY is not necesarily better for performance. Loop unrolling is extremely un-dry and often provides better performance. DRY can also lead to more branches which can lead to branch predictor misses which can impact performance. For example: the author's update to make_pizza to handle split pizzas introduces a branch where previously the code would have been branchless.
The “branch” looks like an easy cmove target. Moreover, while it might not have a branch in the func itself, you will have to have one somewhere higher in the control flow anyway.
As for loop unrolling, I bet you a loop with unrolled calls to 2 different unDRY functions will be slower (and not only because of the most certainly present extra branches to select for them)
The DRY example is better though. The payload is an object. When you have multiple objects of the same shape you have a class of objects. Menu items could be loaded from a JSON source. Separation of concerns and duplication is removed from the code.
The example discusses a code boundary that is internal to a single atomic "module" - the preparation of a data structure that describes a pizza. Then the author says that bad things will happen if said code boundary is used from other modules.
However, why would an extrenal module developer do that? It is common wisdom to recognize and avoid module-internal utility functions.
Conversely, as long as the presented shortcut is internal to a module (=used only for a specific set of use cases well understood by anyone touching the code), and saves toil, it might actually be justified.
> However, why would an extrenal user do that? Potential external users typically recognize and avoid module-internal utility functions.
External users will go look the implementation of msvc's standard library and reverse engineer windows API to make things faster lol. No internal module function is ever safe.
I agree that no internal module function is safe, but MSVCRT is used by literally millions of developers, some of which have very uncommon functional requirements, such as making their product work on a rare version of Windows. My empirical observation is that most developers are prone to the other extreme of not considering internals when they should.
Personal anecdata. I have been increasingly aware of my own mental patterns during development and I've noticed that often I've been sitting and mulling over refactoring to some sort of universal solution instead of getting on with the work and getting things done. There are instances when I could've finished the task twice as fast if I would've just went ahead and done it with repeating code instead of thinking of clever ways to DRY it.
Therefore for my personal projects I'm now firm believer of quick iterative building. Just get the first iteration done, get it working and save improvements for later. It may create a bit more work for the future me but it decreases the mental load quite significantly. I'll take less mental load with clear objective (refactor this because this) over more mental load with unclear objectives (make universal solutions taking into account things that may or may not happen in the future) any day.
My principles are "be as stupid as possible" write it for someone stupider and comment it for someone even stupider and then maybe you'll have something maintainable.
(Important note: stupid is not incompetent - it's a proxy for clarity, composability and rational structure without becoming formal, rigid, overly orthodox or academic about it)
I can relate to this. I think Casey Muratori coined the maxim "write usable code first". I often mutter that to myself when I'm trying to design some crazy system rather than just solving the problem at hand.
There's no silver bullet. My opinion is that you should weight alternatives without repetition and with repetition and choose the most appropriate one. Also if in the future you feel that this common code is becoming more complex with options and switches, feel free to remove it by inlining, either completely or in a few places. Often it'll allow for better code or it'll allow to find out another way to extract common code.
Basically I like the refactoring approach. You have a set of refactorings. Like extract method / inline method. The point is that every refactoring is two-way. And both ways are useful in different situations.
To support this approach, sane IDE is a must and strictly typed language is preferable You should refactor your code without fear of breaking unrelated code.
What I definitely think is overrated is "if it works - don't touch it" principle. It's lazy and in the end it creates much more work than if one would gradually improve something that works.
IMO, DRY is the second principle, the first one is KISS. It is preferable to repeat ourself if that contributes to simplicity and ease of maintenance. My third principle is that there is only three principles.
"There are two ways of constructing a software design: one way is to make it so simple that there are obviously no deficiencies, and the other is to make it so complicated that there are no obvious deficiencies.”
Not sure why this is on the frontpage. Not only are there a bunch of typos, a bunch of code doesn't actually work the way they said it does. Also gotta love hating on the 10x developer or whatever for saying you are wrong.
EVERYTHING HAS TRADEOFFS. Every single thing has tradeoffs. Obviously you should not write terrible, brittle code. The reason DRY is important is because when you start duplicating code, having 30 different serialization methods littered throughout your code, 5 different ways of calculating the same value, etc etc you see why it really matters. Its a GUIDELINE used to as a general rule. And as guidelines and general rules go -- its useful for juniors and people who don't have the experience to see the best way to write the code.
Its a good default, and like YAGNI, and 100 other programmer acronyms it has its ups and downs. Your pizza example is not "coincidental repetition" -- it is actual repetition -- you just abstracted it in a really poor way to make a strawman.
This. The clean code principles should be considered within the specific context of the situation. They are guidelines that are good to keep in mind, but no more than that.
The article gets this wrong by considering DRY as some kind of dogma and then discovering some situations where it doesn't work well. And then of course some commenters here get it wrong by only looking at situations were it does work well.
It's the same religious discussion again as FP vs OOP, static vs dynamic typing, no code vs full code etc. etc. The real answer to each of these is always 'it depends'.
Yeah, this reads like a junior programmer that got told off for having very repetitive code and they're trying to get the internet to agree that they're in the right and DRY isn't all it's cracked up to be. From the about page it doesn't seem like this is accurate, but that's how it reads.
The problem is that he made his case poorly and I definitely don't agree.
I see DRY as a smell, not a principle. If you see clones (same code in multiple places), then it's likely indicating that there is something that can be factorized. Now the question you should ask yourself before factorization is whether the duplication is coincidental (as the author shows) or if it's because the logic was copy pasted. Most of the time it's the second case, and duplicate code does make maintenance harder and riskier.
One of my pet-peeves is clones in unit-tests. People tend to care less about code quality when it comes to unit-tests, and code gets copy-pasted all over the place. The result is usually an unmaintainable ball of mess, where the most subtle variation in the unit being tested requires you to apply the same change in 15 different places. In this situation, DRY is a very useful indicator that something is going wrong.
Now the opposite of DRY is YSHRY - You Should Have Repeated Yourself. When you start adding 5 boolean parameters to a function to adapt it to all its calls, it's a smell that you thought you should have DRY, whereas YSHRY.
In my case when I was junior I tried to be very smart and try to DRY a lot, but I found that most of the times is better to write "dumb" code and repeat yourself if the complexity is not worth, also as you stated if your function is used in a lot of places for slightly different things is just so easy to break something without noticing and also harder to test.
So I agree with you, as developer you should know when do duplicate code and when DRY, but overall try to maintain your code as simple as possible, that makes also easier to maintain.
Only experience teaches you where to apply DRY and where not to.
Sometimes just because something looks the same or is similar does not mean it’s the same. Applying DRY just because it looks the same can have the unwanted consequence of changing in 1 place changing in another too when that’s not the desired affect. Then you add another parameter and conditional logic just because you don’t want 2 similar looking things.
Out of all the comments on this post, this is the one I agree with the most. I have been bitten by "not enough DRY" as well as "too much DRY".
I think experience as well as the maturity of the code are most relevant when deciding things like this. If you do this too early you will back yourself into a corner or eventually end up with a "god function" (usually the outcome of what OP mentions about conditional logic and more params).
If you wait too late you will inevitably have a giant codebase with lots of duplicates everywhere.
In my experience it's easier to clean up duplicated code than it is to break apart a "master function" that nobody has touched in 3 years out of fear.
> but I would assert that any change that doesn't modify the existing calls of make_pizza or make a totally separate function for split topping pizzas (not DRY) will be some level of bad.
You make a make_pizza function that supports split toppings and you pull the guts out of the original make_pizza function that just calls the first make_pizza function with left_toppings=[toppings], right_toppings=[toppings]. You don't need to ruin your function signature with *args.
The fundamental assertion is that you should be structuring your code such that it is reflective of reality, but reality is really bloody messy. The immediate response to this example is that Pizza is in fact Toast[1] and so you should actually have a make_toast function that handles all forms of toast. This is clearly ridiculous, and if you're building a system to make pizzas and you build your function in a way that extends as far as building nigiri sushi, you're an idiot. You have to take a reasonable judgement of what is the underlying structure that you want to reflect. It's not a coincidence Hawaiian and Pepperoni Pizzas are structure the same.
Except, once you are done, you probably never have to touch that code again, and creating a class does not really take long.
Depending on the given problem, this is sometimes more time consuming, but still gets easier and faster with practice, and the benefit down the road can be tremendous.
Associative arrays are bad even for such simple things imo, because it breaks autocompletion / code inspections, and your functions are then these blackboxes that are hard to understand without looking at the implementation (code). Sometimes this is also evident when it's just you working on the code; try leaving the code for a few months only to return at it, and waste time relearning how to use your own code, because it is not self-documenting, and you can also forget- or misspell an array key. Etc. This is not the case if you define data types as objects instead of using arrays.
I learned this from trial and error myself, and I used to use associative arrays a lot for things – now I find myself using/creating objects more often, and I just love returning to this code later, and have it work without too much crapping around.
There was rarely a point where I haven't regretted using an associative array instead of a class. Not only adds the class semantic meaning, you can also add constraints and methods to it.
> The problem is that these two pizzas just happen to have the same crust, sauce and cheese.
The problem with analogies is that they are often bad. Don't don't specify that you want tomato sauce and regular cheese when you order your pizza because that's the default.
> "Keep it simple, silly", "keep it short and simple", "keep it short and sweet", "keep it simple and straightforward", "keep it small and simple", "keep it simple, soldier", "keep it simple, sailor", or "keep it sweet and simple".
I once worked on a project that was basically a simple My Account application/area for a train ticket retailer.
The backend itself held no data, but whoever built the backend had gone full service layer, with models and adapters to the upstream services that hold the data.
The result was a backend that was a pain in the ass to change, necessitating whole trees of file changes to build features.
So we started inlining everything. We just took it back to the request handlers. We started fetching, mutating and returning the data in the request handlers. Suddenly a change became modifying one function. Every endpoint was unique and didn't depend on anything else. Things became easy.
Halfway through the migration, someone got our effort reviewed by a principal engineer who told me "it wasn't SOLID", and my contract wasn't renewed. It didn't dishearten me.
Software design is meant to make change easier and proudly adding abstractions can be a bad thing.
> So we started inlining everything. We just took it back to the request handlers. We started fetching, mutating and returning the data in the request handlers. Suddenly a change became modifying one function. Every endpoint was unique and didn't depend on anything else. Things became easy.
What is the big benefit you gained from doing that compared to calling, say, a service method call in the controller?
costService.generateCost(newPrice);
Is it really that difficult to go to the service method definition? With inlining at the controller level, in order to unit test generateCost, you'll now have to deal with authentication/authorization/request handling related infrastructure which has nothing to do with cost calculation.
The most complicated code we had was for generating receipts, which used some functions which we kept separate, because it made sense.
Auth was handled by a middleware. And then once we'd stripped out all the layers, 95% of the handlers looked like roughly like this:
result = fetch(...);
/* Maybe more fetches, maps or filters */
response.send(result);
They didn't really _do_ anything. It was all just small tweaks to data someone else owned. The biggest challenge were upstream endpoints changing on us, making sure we were logging and passing things like correlationIds consistently. Moving to fat handlers, we unified those by having those things already set up and passed into the handlers. The focus was on devex, so a junior could easily modify/create an endpoint and not have to think about how to get it right. We made the pit of success as easy to fall into as possible by breaking the rules that weren't serving the project very well.
It was a glorified proxy layer. There were benefits in treating it as such, rather than deluding ourselves into thinking we needed services, repositories, models and such. Just transform data from someone else's endpoints and focus on the frontend.
500 comments
[ 3.2 ms ] story [ 267 ms ] threadBut I tend to agree with the author. Sometimes verbosity is the lesser evil. No suggestion should become a dogma and whoever played some games of code golf knows that short code doesn't mean code that is easy to read. Extreme example of course. But I believe many start to optimize in this way just as a way to reduce the line count.
Still, there is still room for some kind of factories or function templates (not in the c++ sense). I think a user is allowed to repeat himself but then again a user is just another arbitrary layer again. But if such helpers are to be implemented, I tend to like it in a place where the user is invoking said helpers and not on the level below that if that makes sense.
Exactly! The DRY'ed example in the first section should read rather:
This demonstrates the omnipresent dangers of untested copy-paste."Copy-paste, copy-paste, Will Robinson!!"
Be prepared for laughter
Maybe.
But maybe you're falling prey to the other programmer trap - catering for conceivable situations that are just never going to happen, and making your codebase unnecessarily accommodating as a result. This is another great source of complexity, and quite often the source of unnecessary abstractions (which add to cognitive load) too.
In my experience it's better to cope with half-and-half pizza toppings when they arise, rather than coding as if they're already needed. Because when they are needed, you'll probably find the requirement is actually to put them on a 3-tier wedding cake, or a car.
In all fairness, the best programming principle is: just like with software licenses, know what to use and when.
I don’t touch snippets unless there is a good reason to do that. They are my general templates, not per-project tools.
In most of my code, the need for refactoring was mostly a consequence of building a too rigid high-tech structure which with time turned out to not fit the job anyway. Figured out I can avoid it by not building it, and antiDRY also plays a role in it (albeit mostly psychological).
I think taking another look at project agnostic generators is something worth doing too. It has the benefits of automating some of the duplication without the rigidity of deep abstractions and dependencies. I am still exploring that though.
due to context switches and parametric entanglement
Another example is little utilities like a promise as a semaphore (by lifting resolve() to the scope). It could go as a utility function without parameters, but then I’d have to maintain that utility module across projects, which makes it extremely fragile, and force other people to deal with it.
Mistake 1: Switch from DRY to premature optimization.
> You might think that legit reasonable developers but would not actually do something like this and would instead go back to the existing invocations and modify them to get a nice solution, but I've seen this happen all over the place.
Mistake 2: Assumption of incompetence to support your argument.
> . As soon as we start the thought process of thinking how to avoid a copy paste and refactor instead, we are losing the complexity battle.
Mistake 3: Strawman argument. DRY does NOT lead to over-complicating things. Overcomplicating things leads to overcomplicating things.
Now, i wasted 5 minutes, so you can waste some more to reply to this comment, instead of completely ignoring this dumb random blog post.
> DRY does NOT lead to over-complicating things.
That is not true. I dive around foreign code bases a lot and dry-ness is actually a significant complicating factor in understanding code, because you're jumping around a lot (as in physically to different files or just a few screens away in the same file). As in, inherently every time it's used, not just in situations where it's used in a complicated way.
This sounds dumb but it just simply is much harder to keep context about what's going on around if you can't refer back to it because it's on the same screen or one short mouse scroll above or below your current screen.
That obviously doesn't mean you should leave copy pasted versions of the same code over your code base. But it's important to consider that refactorization of that code into something common that gets called from multiple places as something that you don't get for free, but that is an active trade off which you usually have to apply to prevent bugs (changing one code location and not the other) or simple code bloat. In practice this is very relevant when you suspect something might be repeated in the future, but you're not sure. Imo: Just don't factor it out into anything, leave it there, in place, in the code.
`make_pizza(["pepperoni"])`
What does `make_pizza()` do? It could be a lot or it could be a little. It could have side-effects or not. Now I have to read another function to understand it, rather than easily skimming the ~four lines of code that I would have to repeat.
I think the article fails to show particularly problematic examples of DRY. E.g. merging two ~similar functions and adding a conditional for the non-shared codepaths. shudders
I’ve seen this again and again in the field and I wholeheartedly agree with the sentiment in the OP. IMHO different code paths should only share code if there is good reason to believe that the code will be identical forever.
So now you've dumped it down to an interface with a default implementation which calls the create_dough, add_toppings, bake_pizza interfaces in order, each of which are either passed in callbacks or discovered through reflection.
We can even sprinkle in some custom DSL to "abstract away" common step like putting the product into the oven correctly!
Jr's will never understand when why and what is effectively excecuted at runtime. Honestly, at this point I enjoy working with this kind of code. It's always such a high entertainment value and I get paid by the hour, so whatever
https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
Quote follows:
----
The strength of the reaction made me realize just how widespread and intractable the "wrong abstraction" problem is. I started asking questions and came to see the following pattern:
1. Programmer A sees duplication.
2. Programmer A extracts duplication and gives it a name. This creates a new abstraction. It could be a new method, or perhaps even a new class.
3. Programmer A replaces the duplication with the new abstraction. Ah, the code is perfect. Programmer A trots happily away.
4. Time passes.
5. A new requirement appears for which the current abstraction is almost perfect.
6. Programmer B gets tasked to implement this requirement. Programmer B feels honor-bound to retain the existing abstraction, but since isn't exactly the same for every case, they alter the code to take a parameter, and then add logic to conditionally do the right thing based on the value of that parameter. What was once a universal abstraction now behaves differently for different cases.
7. Another new requirement arrives. Programmer X. Another additional parameter. Another new conditional. Loop until code becomes incomprehensible.
8. You appear in the story about here, and your life takes a dramatic turn for the worse.
Existing code exerts a powerful influence. Its very presence argues that it is both correct and necessary. We know that code represents effort expended, and we are very motivated to preserve the value of this effort. And, unfortunately, the sad truth is that the more complicated and incomprehensible the code, i.e. the deeper the investment in creating it, the more we feel pressure to retain it (the "sunk cost fallacy"). It's as if our unconscious tell us "Goodness, that's so confusing, it must have taken ages to get right. Surely it's really, really important. It would be a sin to let all that effort go to waste."
Sunk cost (fallacy) is about making decisions based on things that you have already lost. But you haven’t lost or expended the code—the code is right there, and it’s hard to know if it’s more of an asset or a burden.
This is not a problem of DRY. This is a problem of wrong abstraction and naming. If the function is just four lines, it could easily be named `make_and_cook_pizza`. In the alternative scenario where those four lines are copy pasted all over the place, one is never sure if they are exactly the same or have little tweaks in one instance or the other. Therefore, one has to be careful of the details, which is much harder than navigating to function definition, because in this case you cannot navigate to other instances of the code.
The code had test coverage, but the test confirmed that it produced the wrong result. I had to fix the test too.
however... real software doesn't work like this. the abstractions that work that way exist for a select few very well understood problems where a consensus has developed long before you're looking at any code.
math libraries would be a typical example. you really don't need to know how two matrices are multiplied if you know the sort of black box properties of a matrix.
but the minute functions, classes, and other ways of abstraction code in a DRY way that you encounter constantly in everyday code, even if they are functionally actually well abstracted (meaning it does an isolated job and its inputs and outputs are well defined), even for simple problems, are typically complex enough that learning their abstract properties can be the same level of difficulty and time investment as learning the implementation itself. on top of practical factors like lack of documentation.
this is also why DRYness as a complicating factor really doesn't factor in once the abstracted code does something so complex that there is no way you could even attempt to understand it in a reasonable amount of time. like implementing a complex algorithm, or simply just doing something that touches too many lines of code. in this case you are left to study the abstract properties of that function or module anyways.
The problem of not knowing what it does or whether it has side effects or not is more a problem of naming and documentation than DRY. Even then, it's still better than repeating the code all over, simply because when you read and understand the function once, you don't need to go back. On the other hand, if the code is all over, you need to read it again to recognize it's the same piece of code.
additionally, the function should be stateless and have no side effects ;)
``` def make_string_filename(s, style="new"): # 2 lines of shared magic if style == "old" # 2 lines of original magic elif style == "new": # different 2 lines of magic ```
When you get here, two totally separate `make_string_filenames()`, each private to the area of code they're relevant to, would be better.
- make_string_filename_style1
- make_string_filename_style2
- make_string_filename
Then make_string_filename consists of logic to use the right style.
Or one function and a Sum type to be called like:
Given sum type:A functional style certainly helps. I get the pizza in my hand and don’t have to worry that anyone left the oven on.
You can't, unless it's in a standard library or a core dependency used by millions of people.
That's one of the reasons why functional code is generally easier to read. A lambda defined a few lines above whatever you're reading gives you the implementation details right there while still abstracting away duplicate code. It's the best of both worlds. People who's idea of "functional programming" is to import 30 external functions into a file and compose them into an abstract algorithm somewhere other than where they're defined write code that's just as shitty and unreadable as most Java code.
>> You can't, unless it's in a standard library or a core dependency used by millions of people.
You can if you have reasonably competent colleagues. And if you do make some wrong assumptions about what a certain method does, it should be caught by your tests.
I feel that people that insist on reading and understanding all the code, and write code that has to be read fully to be possible to understand what is does, have missed something quite fundamental about software development.
Not a problem of DRY, but bad code structure.
Just keep the two functions and pull the shared code-path out
In these cases factorizing may or may not be a good idea.
`makePizza :: PizzaType -> [Toping] -> IO (Pizza)`
Seems to carry all that information by just accepting a PizzaType symbol and a list of toppings, `IO` communicating the side effect.
What makes it easier to understand a system is simplicity. I'd argue that DRY, deployed with a right strategic plan, usually does more to simplify things than does copy-paste.
But, as any tool, DRY is but a tool; to be useful it requires some skill.
To note, a common effect of not DRYing functions is an increase in local code length.
In many code bases that lived long enough, that means screens and screens of functions inside the module/class files. It is still easier to navigate than between many files, but not by that much in practice (back/forth keyboard shortcuts go a long way to alleviate this type of pain)
I can't agree more. Also, "code reuse" makes debugging significantly harder when trying to reverse engineer some code base. The breakpoints or printf:s get triggered by other code paths etc. And you need to traverse stack frames to get a clue what is going on.
Extra bonus points for fancy reflection so that you have no clue what is going on.
DRY only hits when you indeed repeat something.
If you predict potential reuse, which you don't certainly know, it's premature optimization.
Increasing the distance between inputs and outputs increases complexity.
Reusable code isn't all that reausable when nobody understands it or things are so fragmented people can't figure out how to operate the code base.
This isn't a rule. It's a moderation thing.
If you make everything as generic and reusable as possible from the beginning, you'll end up with messy code that has way too much options to set for every simple operation.
Abstractions have non zero complexity costs.
And
Repeated code has non zero complexity costs
Why is this a hard concept?
It doesn't make dry any less valid.
Generally you can invoke both reasons to do something but the underlying reasoning is always complexity.
You can't just slam the DRYness knob to 11 and expect it to always be better, any more than you can turn a reflow oven up to 900°C and expect it to be better, just because 380°C is better, for the specific PCB in question, than 250°C.
It also doesn't mean you can turn it off entirely, just as if you look at your charred results at 900°C you don't conclude that "heaters considered harmful".
Also, the problem is strongly multivariate and the many variables are not independent so the "right" setting for the DRYness knob is not necessarily the same depending on all sorts of things, technical and not, up to and including "what are we even trying to achieve?"
Are you still using a VT100?
[0] https://www.amazon.co.uk/Pragmatic-Programmer-journey-master...
"Premature optimization" is largely a bogus concept, because the meaning of "optimization" has shifted a lot since the concept was first created.
People now use optimization to mean "sensible design that does not needlessly waste resources".
In this meaning of optimization, "premature optimization" is a bogus concept.
You should absolutely ALWAYS write non-pessimized code by default.
What the original concept referred to is what people now call "micro optimizations". Sure, premature micro optimizations is often a waste of time. But this is irrelevant to the context of this discussion.
The idea is that you can end up optimizing before you know the entire use-case, because software engineering isn't like building bridges or skyscrapers.
I'm a performance geek, but I love code I can easily change rather than code that is fast until some customers have touched it. Mostly out of experience with PMs with selection bias on who they get feedback from ("faster horses" or "wires we can hook phones to").
The first thing to optimize is how fast you can solve a new problem that you didn't think about - or as my guru said "the biggest performance improvement is when code goes from not working to working properly".
The other problem with highly optimized code is that it is often checked-in after all the optimizations, so the evolution of thinking is lost entirely. I'd love to see a working bit + 25 commits to optimize it rather than 1 squashed commit.
Optimized code that works usually doesn't suffer from this commentary so the biggest opponents I have with this are the most skilled people who write code with barely any bugs - I don't bother fighting them much, but the "fun" people with work understand my point even if they write great code first time around.
These two are mostly why I talk to people about not prematurely optimizing things, because I end up "fixing" code written by 15 or more people which has performance issues after integration (or on first contact with customer).
That's the whole point of my comment.
The word "optimization" as currently used confabulates two separate concepts:
- Non-pessimization (new meaning of "optimization")
- Micro-optimization (original meaning of "optimization")
You're talking about micro optimized code, and I'm talking about simple non-pessimized code.
At the low levels you really have no idea where the performance bottle necks are without profiling and getting actual numbers to work with.
At the high level you pretty much have a clear idea of what roughly the system is supposed to do and what performance characteristics you want.
If you're writing and enterprise app and lean back in your chair and start to think about speeding things up with loop unrolling and avx instruction sets then you're doing the premature optimization thing.
But trying to limit large nested loops is easy fruit that doesn't take much effort to pick.
Some days I come here just for the typos. :)
Today I've seen two good ones, number zero was
"Costco had to stop returns on TVs because people were “renting” them for free for the superb owl."
I see humor in thinking if my code is pessimistic enough. Have I assumed that the edge cases will happen and worked around them? Do I expect (and handle) crashes, i/o failures, network timeouts, etc?
"code pessimism" could be an interesting metric.
The typo in the other post was "superb owl" which should have been "super bowl". Several people on that thread enjoyed the typo, including a comment from CostalCoder saying "Please, please do not correct that typo!"
https://news.ycombinator.com/item?id=31999048
Okay, but it kind of is about incompetence. And by “it” I mean everything. Look, we all remember that first time we all realized that adults are just winging it most of the time. Almost nobody knows what they are doing. Half the people who “know” actually know the least.
> DRY does NOT lead to over-complicating things.
Don’t Repeat Yourself is a terrible acronym because what it stands for is exactly the opposite of what people do. Not doing something is avoidance, opting out, like “don’t push your sister” vs “be nice to your sister”.
What most people do is they realize they have already repeated themselves, or someone else, and they rip it out. They deduplicate their code. Avoidance definitely can “lead” somewhere, but deduplication is active, and that can often be headed the wrong way, either directly or obliquely.
The Rule of Three is much clearer on this. You get one. There’s nothing to do when you see you’ve duplicated code - except to check if you’re the first or not.
No really, you're absolutely on point. The post is not worth the time, the case against DRY is too weak.
Sounds like a kid complaining about pushing DRY in a direction that overcomplicated things for him because of himself and instead of improving himself he choosed to attack "an uncomfortable principle".
Though note that DRY can itself be premature optimisation of the codebase.
Mistake 1a: Conflating the term "premature optimization" - it doesn't apply here. Premature optimization is about runtime performance, DRY is about optimising maintenance overhead.
Mistake 1b: (good) DRY can't be done early (it's a continuous process throughout project development).
> Mistake 2: Assumption of incompetence to support your argument.
Mistake 2: Assuming you're never working in teams leveraging peers' of varying experience and technical focus.
The presumption of re-usability is absolutely the most common red flag I've seen with DRY: I've seen it with a lot of very senior / experienced devs. You can call them incompetent, but there's plenty of them and we have to work with them. Articles like this help.
> Mistake 3: Strawman argument. DRY does NOT lead to over-complicating things. Overcomplicating things leads to overcomplicating things.
This statement concerns me. DRY very obviously and demonstrably leads to over-complicating things (excessive / ballooning parametrisation is just one of many very simple examples of this). If you can't see this I would have my own concerns about competence...
Fallacy: False Dichotomy and No True Scotsman.
"Things are either DRY or premature optimization and can't be both"
"No TRUE application of DRY would ever be a premature optimization"
This would be the most efficient title, subtitle, and entire contents of most posts about programming principles.
However, each reader has to have a similar enough perspective, background, and experience to understand and apply it. In that sense, the trend line measuring the value of commenting about comments about random blog posts indeed indicates wasted time, but hopefully it's a local minima.
My pithy corollary to your helpful tautology is a quote from Tommy Angelo that's stuck with me since my poker days: "The decisions that trouble us most are the ones that matter least."
Decisions are necessarily difficult to make when the expected value of either outcome are similar. We waste an awful lot of time on choices that could have been made just as well with a coin flip.
So there you go world: two quotes that are generally useful about generalities that are locked, loaded, and ready to shoot you in the foot when misapplied.
Edit: formatting improvement.
Sure, I agree, except DRY is probably the second greatest gateway drug to overcomplicating things to OOP. Actually, they really hand-in-hand since OOP features are often used to DRY things.
DRY can easily go too far because fundamentally it's about centralizing ideas with the premise that different operations can and should share units, even though a "writeSomeFileToDisk" function doesn't necessarily have to do the exact same thing between different higher-level operations. Because so many engineers emphasize "elegance", if a set of functions seem similar enough, they pressure themselves to write code that is shareable, hence more abstract. Abstractions are inherently more complicated and hard to understand, not the other way around. Rather than having very simple "molecules" of code that can be understood on their own, there is instead a much larger molecule of nodes that are connected by abstract dependencies, and those nodes may only have dependencies in common.
DRY should be done sensibly, but teaching DRY is a problem in our industry because we don't teach engineering discipline. We teach principles like DRY and OOP, and even YAGNI as if they are tenets of a religion.
For what it's worth, I agree with your points and disagree with the various counterpoints that were posted; "optimization" can mean a lot of things, and I for one understand what you mean.
As for loop unrolling, I bet you a loop with unrolled calls to 2 different unDRY functions will be slower (and not only because of the most certainly present extra branches to select for them)
The example discusses a code boundary that is internal to a single atomic "module" - the preparation of a data structure that describes a pizza. Then the author says that bad things will happen if said code boundary is used from other modules.
However, why would an extrenal module developer do that? It is common wisdom to recognize and avoid module-internal utility functions.
Conversely, as long as the presented shortcut is internal to a module (=used only for a specific set of use cases well understood by anyone touching the code), and saves toil, it might actually be justified.
External users will go look the implementation of msvc's standard library and reverse engineer windows API to make things faster lol. No internal module function is ever safe.
Therefore for my personal projects I'm now firm believer of quick iterative building. Just get the first iteration done, get it working and save improvements for later. It may create a bit more work for the future me but it decreases the mental load quite significantly. I'll take less mental load with clear objective (refactor this because this) over more mental load with unclear objectives (make universal solutions taking into account things that may or may not happen in the future) any day.
(Important note: stupid is not incompetent - it's a proxy for clarity, composability and rational structure without becoming formal, rigid, overly orthodox or academic about it)
Generally the less code, the cleaner the conceptual execution. I always strive to remove and reduce conceptually deceits
Here's some code I wrote earlier, probably a good example
https://github.com/kristopolous/music-explorer/blob/master/w...
It's self contained, not very big, not trying to be fancy, as direct as possible.
It's worth noting a few things:
Some things are repeated when there's no reasonable way to refactor it in a way that simplify things.
No framework. No view/model/controller/provider/orm separation. It's not doing much and it does it fine
Stuff is composed but intentionally not abstracted
Here's a frontend
https://github.com/kristopolous/music-explorer/blob/master/w...
Again, no react or angular or other framework. Just direct modern code.
As far as what it looks like, it's a music player frontend to some sprawling project Example https://9ol.es/pl/
Basically I like the refactoring approach. You have a set of refactorings. Like extract method / inline method. The point is that every refactoring is two-way. And both ways are useful in different situations.
To support this approach, sane IDE is a must and strictly typed language is preferable You should refactor your code without fear of breaking unrelated code.
What I definitely think is overrated is "if it works - don't touch it" principle. It's lazy and in the end it creates much more work than if one would gradually improve something that works.
Simplicity is hard.
EVERYTHING HAS TRADEOFFS. Every single thing has tradeoffs. Obviously you should not write terrible, brittle code. The reason DRY is important is because when you start duplicating code, having 30 different serialization methods littered throughout your code, 5 different ways of calculating the same value, etc etc you see why it really matters. Its a GUIDELINE used to as a general rule. And as guidelines and general rules go -- its useful for juniors and people who don't have the experience to see the best way to write the code.
Its a good default, and like YAGNI, and 100 other programmer acronyms it has its ups and downs. Your pizza example is not "coincidental repetition" -- it is actual repetition -- you just abstracted it in a really poor way to make a strawman.
The article gets this wrong by considering DRY as some kind of dogma and then discovering some situations where it doesn't work well. And then of course some commenters here get it wrong by only looking at situations were it does work well. It's the same religious discussion again as FP vs OOP, static vs dynamic typing, no code vs full code etc. etc. The real answer to each of these is always 'it depends'.
How great dev life could be if everyone saw it like that.
The problem is that he made his case poorly and I definitely don't agree.
This should be the lede, IMHO.
Bullshit. What's the tradeoff on using `gets` vs any other function?
Nothing. Absolutely nothing. `gets` is wrong 100% of the time period.
If you're wrong its not a trade and a lot of things in this article and about dry is wrong
Not enough people are flagging the post.
One of my pet-peeves is clones in unit-tests. People tend to care less about code quality when it comes to unit-tests, and code gets copy-pasted all over the place. The result is usually an unmaintainable ball of mess, where the most subtle variation in the unit being tested requires you to apply the same change in 15 different places. In this situation, DRY is a very useful indicator that something is going wrong.
Now the opposite of DRY is YSHRY - You Should Have Repeated Yourself. When you start adding 5 boolean parameters to a function to adapt it to all its calls, it's a smell that you thought you should have DRY, whereas YSHRY.
So I agree with you, as developer you should know when do duplicate code and when DRY, but overall try to maintain your code as simple as possible, that makes also easier to maintain.
DRY / single source of truth offers a certain protection against such bugs.
Sometimes just because something looks the same or is similar does not mean it’s the same. Applying DRY just because it looks the same can have the unwanted consequence of changing in 1 place changing in another too when that’s not the desired affect. Then you add another parameter and conditional logic just because you don’t want 2 similar looking things.
I think experience as well as the maturity of the code are most relevant when deciding things like this. If you do this too early you will back yourself into a corner or eventually end up with a "god function" (usually the outcome of what OP mentions about conditional logic and more params).
If you wait too late you will inevitably have a giant codebase with lots of duplicates everywhere.
In my experience it's easier to clean up duplicated code than it is to break apart a "master function" that nobody has touched in 3 years out of fear.
You make a make_pizza function that supports split toppings and you pull the guts out of the original make_pizza function that just calls the first make_pizza function with left_toppings=[toppings], right_toppings=[toppings]. You don't need to ruin your function signature with *args.
The fundamental assertion is that you should be structuring your code such that it is reflective of reality, but reality is really bloody messy. The immediate response to this example is that Pizza is in fact Toast[1] and so you should actually have a make_toast function that handles all forms of toast. This is clearly ridiculous, and if you're building a system to make pizzas and you build your function in a way that extends as far as building nigiri sushi, you're an idiot. You have to take a reasonable judgement of what is the underlying structure that you want to reflect. It's not a coincidence Hawaiian and Pepperoni Pizzas are structure the same.
[1]:https://cuberule.com/
Depending on the given problem, this is sometimes more time consuming, but still gets easier and faster with practice, and the benefit down the road can be tremendous.
Associative arrays are bad even for such simple things imo, because it breaks autocompletion / code inspections, and your functions are then these blackboxes that are hard to understand without looking at the implementation (code). Sometimes this is also evident when it's just you working on the code; try leaving the code for a few months only to return at it, and waste time relearning how to use your own code, because it is not self-documenting, and you can also forget- or misspell an array key. Etc. This is not the case if you define data types as objects instead of using arrays.
I learned this from trial and error myself, and I used to use associative arrays a lot for things – now I find myself using/creating objects more often, and I just love returning to this code later, and have it work without too much crapping around.
The problem with analogies is that they are often bad. Don't don't specify that you want tomato sauce and regular cheese when you order your pizza because that's the default.
> "Keep it simple, silly", "keep it short and simple", "keep it short and sweet", "keep it simple and straightforward", "keep it small and simple", "keep it simple, soldier", "keep it simple, sailor", or "keep it sweet and simple".
It's true that often, complexity is praised...
The backend itself held no data, but whoever built the backend had gone full service layer, with models and adapters to the upstream services that hold the data.
The result was a backend that was a pain in the ass to change, necessitating whole trees of file changes to build features.
So we started inlining everything. We just took it back to the request handlers. We started fetching, mutating and returning the data in the request handlers. Suddenly a change became modifying one function. Every endpoint was unique and didn't depend on anything else. Things became easy.
Halfway through the migration, someone got our effort reviewed by a principal engineer who told me "it wasn't SOLID", and my contract wasn't renewed. It didn't dishearten me.
Software design is meant to make change easier and proudly adding abstractions can be a bad thing.
What is the big benefit you gained from doing that compared to calling, say, a service method call in the controller?
Is it really that difficult to go to the service method definition? With inlining at the controller level, in order to unit test generateCost, you'll now have to deal with authentication/authorization/request handling related infrastructure which has nothing to do with cost calculation.Auth was handled by a middleware. And then once we'd stripped out all the layers, 95% of the handlers looked like roughly like this:
result = fetch(...);
/* Maybe more fetches, maps or filters */
response.send(result);
They didn't really _do_ anything. It was all just small tweaks to data someone else owned. The biggest challenge were upstream endpoints changing on us, making sure we were logging and passing things like correlationIds consistently. Moving to fat handlers, we unified those by having those things already set up and passed into the handlers. The focus was on devex, so a junior could easily modify/create an endpoint and not have to think about how to get it right. We made the pit of success as easy to fall into as possible by breaking the rules that weren't serving the project very well.
It was a glorified proxy layer. There were benefits in treating it as such, rather than deluding ourselves into thinking we needed services, repositories, models and such. Just transform data from someone else's endpoints and focus on the frontend.