I am the industry for over 10 years now. Whenever I have to work with a project where someone used DRY consciously, I know I am in for a world of pain. Consolidating code is easy, pulling it apart is a lot harder.
Good way to go at it is to isolate the functionality that is used many times and to pull it aside in its own function (or similar). That's just good code practice and also makes it easy to refactor and modify as needed.
It’s not about being used many times, but about the necessity to evolve in the same direction. When that happens, it usually manifests as toil for the team. Consolidating code means to change the structure of the code so that only one piece needs to be modified in the future. That can take many forms, but it usually involves creating a new shareable component.
Shareable components are more effort to maintain, so just creating them because they consolidate code is not always a good idea. You really want to have positive ROI here and you only get that if you actually reduce maintenance burden. For raw code duplication that doesn’t have a maintenance issue on it‘s own, the bar is a lot higher than most people think.
Well, this morning I just fixed a case where somebody had used btoa to base64 encode something in Javascript and used methods from Buffer somewhere else because they'd been intimidated away from using btoa. (Ok, it is dirty to use UTF-8 codepoints if it is byte values, you can write btoa("Á") but btoa("中") is a crash.)
It would have been OK if they'd used the right methods on Buffer but they didn't.
These encoding/decoding methods are a very good example of code that should be centralized, not least so you can write tests for them. (It is a favorable case for testing because the inputs and outputs are well defined and there are no questions of whether execution is done like you might encounter testing a React component) It is so easy to screw this kind of thing up in a gross way or an a subtle way (I'm pretty sure btoa's weirdness doesn't affect my application because codepoints > 255 never show up... I think)
There's the meme that you should wait until something used 3 times before you copy it but here is a case where two repetitions were too many and it had a clear impact on customers.
Raw code duplication is always a maintenance issue when centralising it when you notice the duplication (instead of keeping copy-pasting it) costs nothing.
I basically agree, but doesn't this just mean, if I'm consolidating non-DRY code, that I'm now the one using DRY consciously, and the next dev will be cursed with all of my newly introduced DRY abstractions?
> Whenever I have to work with a project where someone used DRY []consciously[], I know I am in for a world of pain.
Huh. When you put it that way, that's actually a good point. In my experience, competent programming will try to consolidate repeated code, and then cite "because DRY" if asked why, but I can't think of any case where I or anyone else competent started with "needs more DRY" as the original motivation (as opposed to "this is a incomprehensibly verbose mess" or the like).
Conversely, starting with "don't repeat yourself [and don't let anything else repeat itself]" as a design goal does seem to correlate well with cases where someone temporarily (newbie) or permanently (moron/ideologue) incompentent followed that design principle off a cliff.
"Consolidating code is easy, pulling it apart is a lot harder."
My experience is the opposite. The less code, the better. I just spent a week on refactoring UI automation test code where they had copied the same 30 lines of code into almost 100 places. Every time with an ID changed and some slightly different formatting. It took me a few days to figure out that these sections do the same thing so I decided to introduce a function with ID as parameter. It was a lot of work to identify all sections and then to make sure they are really equivalent.
Saved us 3000 lines of code and now we can be sure that timeouts and other stuff is handled correctly everywhere. An we can respond to changes quickly.
that's DRY to me. Don't copy/paste code. Introduce functions. Ideally in the simplest way. When you have functions, you declare the same behavior everywhere.
> In the industry code that isn't DRY is a much bigger problem than code that is too DRY.
As with anything dogmatic, it truly depends. There are times when the abstraction cost isn't worth it for a few semi-duplicate implementations you want to combine into a single every-edge-case function/method.
There's a certain psychological attraction to messy and confused situations which people are just too comfortable with but it explains why things like GraphQL (didn't have a definition for how it worked for years because "Facebook is going to return whatever it wants to return") inevitably win out over SPARQL (which has a well-defined algebra).
One of my biggest gripes (related to the post) is the data structure
which is of course an academic example but that I've seen in many kind of e-business application. Nobody ever seems to think of it until later but two obvious requirements are: (1) query to see what state a user was in at a given time, (2) show the history of a given user. The code to do that in the above is highly complex and will change every time a new state gets added. The customer also has experiences like "we had a student who took two leaves of absence" or "some students apply, get rejected, apply again, then get accepted" When you find data designs like this you also tend to find some of the records are corrupted and when you are recovering the history of users there will be some you'll never get right.
If you think before you code you might settle on this design
create table history (
student_id integer primary key,
status integer not null,
begin_date datetime not null,
end_date datetime
)
which solves the above problems and many others in most situations. (For one thing the obvious queries are trivial and event complex queries about times and events can be written with the better schema.) I can't decide if the thing I hate the most about being a programmer is having to clean up messes like the above or having to argue with other developers about why the first example is wrong.
If "No code" is to really be revolutionary it's going to have to have built-in ontologies so that programmers get correct data structures for situations like the above that show up everyday in everyday bizaps where there is a clear right answer but it is usually ignored.
It is easy to write a query for the first that gets a list of students names and the dates they applied. That query is harder for the second one. On the other hand figuring out what state a user was in at time t could be a very hard problem with the first table.
My experience with the first is that you find corrupted data records, one cause of that will be that people will cut and paste the SQL queries so maybe 10% of the time they wind up updating the wrong date. Systems like that also seem to have problems with data entry mistakes.
The biggest advantage of #2 is ontological and not operational, which is that in a business process an item is usually in exactly one state out of a certain set of possible states. Turns out that this invariant influences the set of reasonable requirements that people could write, the subconscious expectations of what users expect, needs to be implicitly followed by an application, etc.
Granted some of the dates I listed up there don't quite correspond to a state change, for instance the system needs to keep track of when a student started an application and when the last document (transcripts, letters, etc.) has been received. With 5 documents you would have 32 possible states of received or not and that's unreasonable, particularly considering that a student with just one letter and a very strong application in every other way might get accepted despite that. It's fair to say the student can have an "open application" and a "complete application". Similarly you could say the construction of an airplane or a nuclear power plant can be defined by several major phases but that these systems have many parts installed so if the left engine is installed but the right engine is not installed these are properties of the left and right engine as opposed to the plane.
Same. From what I've seen, most code is written with abstractions and DRY as a high priority rather than writing code that is performant and doesn't take jumping between 5 different files to make sense of it.
"In the industry code that isn't DRY is a much bigger problem than code that is too DRY."
which industry is that?
in general programming, absolute nope
not-DRY code can be weaseled out with a good ide
badly abstracted code, not so much
in fact in a way, DRY is the responsibility of the IDE not the programmer - an advanced IDE would be able to sync all the disparate code segments, and even DRY them if necessary
but when I read DRYed code, the abstraction better be a complete and meaningful summary, like 'make a sandwich', and without many parameters (and no special cases), or else I'd rather read the actual code
i understand the impulse to try to factorize everything but it just doesn't work beyond a certain point in the real world; it's too difficult to read, and there's always an 'oh, can you just' requirement that upends the entire abstract tower.
You didn’t provide any evidence for this, you just stated your coding preference. Which is usually the case in these discussions. Some anecdotes, and then people making grand claims based on personal preference. Obviously, some programmers have thought the opposite and have their own anecdotes.
the comment I replied to was merely a strong opinion
same same
i don't believe there is much evidence, certainly nothing conclusive, in this debate
but factorizing code concentrates the logic
that can be an advantage, to a certain degree, but it also reduces resilience, by specializing the code, and can reduce readability by forcing lookups of nested abstractions
Abstraction too early is usually a mistake, no one is smart enough to predict all the possible edge cases. Repeated code allows someone to go in there and add an edge case easily. Its a more fool proof way of programming
Having specialized in project rescue, touring all over "the industry", you can't possibly make that generalization.
For every purported best practice, there are teams/orgs that painted themselves into a corner by getting carried away and others that really would have benefited from applying it more than they did.
In the case of DRY, it's an especially accessible best practice for inexperienced developers and the project leads many of them become. Many many teams do get carried away, mistaking "these two blocks of code have the same characters in the same sequence" with "these two delicate blocks of code are doing the same thing and will likely continue to do so"
Having advice articles floating around on both sides of practices like this helps developers and teams find the guidance that will get them from where they are to where they need to be.
In Zion National Park, there's a hike called Angel's Landing. For part of the hike, you go along this ridge, where on one side you have a cliff of 500 feet straight down, and on the other side, you have a cliff of 1000 feet straight down. And in places, the ridge is only a couple of feet wide.
Best practices can be like that. "Here's something to avoid!" "OK, I'll back far away from that." Yeah, but there's another cliff behind you, of the opposite error that is also waiting to wreck your code base.
Listen to best practices. Don't apply them dogmatically, or without good judgment.
DRY should honestly just be restated as "Avoid multiple source of truth", two pieces of code that look similar but have two separate goals are not something that falls under DRY unless you can pull out some common logic that actually is the same goal and can become the single source of truth for that goal.
Not from my experience. Unnecessarily duplicated code, even when there are small differences which are likely accidental, is usually much easier to fix than too DRY code. Pulling apart false sharing can be really hard.
The number of person-hours wasted on over-engineered products that never even made it to release could have: solved the halting problem, delivered AGI v2.0, made C memory-safe without compromising backward-compatibility, or made it easy to adjust mouse pointer speed on Linux.
Because you’re unlikely to write a good abstraction until you need it more than twice.
And if you only need the code twice, you very likely wasted time writing the abstraction because copying updates between the two locations is not hard.
This is a rule of thumb, I’m not trying to tell anyone how to do their job.
The rule of three[1] also comes to mind and is a hard learned lesson.
My brain has a tendency to desire refactoring when I see two similar functions, I want to refactor--it's almost always a bad idea. More often than not, I later find out that the premature refactoring would've forced me to split the functions again.
DRY when it's a wielded as a premature optimization (like all other premature optimization) prevents working code that is tailored to solving a problem from shipping quickly.
Let’s say you have a business rule that you can never have more than 5 widgets. You can make this assumption in multiple places, even with totally different code, and that’s damaging DRY when the rule changes to allowing 6. On the other hand, having a bit of duplicated HTML can help, as they may only be the same by accident.
In thise case, '5 widgets max' would be a parameter that should be defined as a global constant instead of having a hard-coded 5s all over the place or, worse, pieces of code copy-pasted 5 times... That's a standard good coding practice.
Generality can really hurt performance. Duplicating specialized code to handle different cases can really help optimize specific code hot spots for certain data patterns or use cases.
I'd love examples where DRY can really hurt performance. Typically what matters most in terms of performance is the algorithm used, and that won't change.
More importantly, cleverer people than me said "premature optimization is the root of all evil"
IMO it hurts developer productivity more than performance, because it introduces indirection and potentially unhelpful abstractions that can obscure what is actually going on and make it harder to understand the code.
In raw performance this could manifest as issues with data duplication bloating structures and resulting in cache misses, generic structures expressed in JSON being slower then a purpose-built struct, chasing pointers because of functions buried in polymorphic hierarchies. But I doubt that any of this would really matter in 99% of applications.
Premature optimization is about not making a micro-implementation change (e.g. `++i` vs `i++`) for the sake of percieved performance. You should always measure to identify slow points in expected workloads, profile to identify the actual slow areas, make high-level changes (data structure, algorithm) first, then make more targetted optimizations if needed.
In some cases it makes sense, like writing SIMD/etc. specific assembly for compression/decompression or video/audio codecs, but more often than not the readable version is just as good -- especially when compilers can do the optimizations for you.
A lot of times I've found performance increases have come from not duplicating work -- e.g. not fetching the same data each time within a loop if it is fixed.
Not really. Knuth was talking about putting effort to make a non-critical portion of the software more optimized. He's saying put effort into the smaller parts where performance is critical and don't worry about the rest. It's not about `++i` vs. `i++` (which is semantically different but otherwise in modern compilers not an optimization anyways but I digress).
The optimizations he was talking about were things like writing in assembly or hand-unrolling loops. It was assumed that you’ve already picked an performant algorithm / architecture and are writing in a performant low level language like C.
Also, your digression about modern compilers is irrelevant to the context of the quote, since Knuth talked about premature optimization at a time when compilers were much simpler than today.
That was my point, though. Don't worry about minor possible changes to the code where the performance doesn't matter. For example, if the ++i/i++ is only ever executed at most 10 times in a loop, is on an integer (where the compiler can elide the semantic difference) and the body of the loop is 100x slower than that.
If you measure the code's performance and see the ++i/i++ is consuming a lot of the CPU time then by all means change it, but 99% of the time don't worry about it. Even better, create a benchmark to test the code performance and choose the best variant.
That's not my interpretation. If you're profiling and benchmarking you're already engaging in (premature) optimization. This process you're describing of finding out whether `i++` is taking a lot of CPU time and then changing it is exactly what Knuth is saying not to worry about for 97% of your code. Knuth is saying it doesn't matter if `i++` is slow if it's in a non-performance critical part of your code. Any large piece of software has many parts where it doesn't matter for any practical purpose how fast they run and certainly one loop in that piece of software doesn't matter. For example, the software I'm working on these days has some fast C code and then a pile of slow Python code. In your analogy all the Python code is known to be much slower than the C code, we don't need a profiler or benchmarks to tell that, but it also doesn't matter because the core performant functionality is in that C code.
Knuth says forget about small efficiencies in 97% of your code. Indeed, the `i++` optimization isn't apt to make more than a small difference, even with the most naive compiler, but other decisions could lead to larger chasms. It seems he is still in favour of optimizing for the big wins across the entire codebase, even if it doesn't really matter in practice.
But it's your life to live. Who cares what someone else thinks?
In the general case, it usually depends on the latency of what you'd DRY your code to vs the latency of keeping the implementation local and specialized.
If you're talking about consolidating some code from one in-process place to another in the same language, you're mostly right: there's only going to be an optimization/performance concern when you have a very specific hotspot -- at which point you can selectively break the rule, following the guidance you quoted. This need for rule-breaking can turn out to be common in high-performance projects like audio, graphics, etc but is probably not what the GP had in mind.
In many environments, though, DRY'ing can mean moving some implementation to some out-of-language/runtime, out-of-process. or even out-of-instance service.
For many workloads, the overhead of making a bridged, IPC, or network call swamps your algorithm choice and this is often apparent immediately during design/development time. It's not premature optimization to say "we'll do a lot better to process these records locally using this contextually tuned approach than we will calling that service way out over there, even if the service can handle large/different loads more efficiently". It's just common sense. This happens a lot in some teams/organizations/projects.
> I'd love examples where DRY can really hurt performance.
A really common example is overhead of polymorphism, although that overhead can vary a lot between stacks. Another is just the effect caused by the common complaint about premature abstraction: proliferation of options/special cases, which add overhead to every case even when they don’t apply.
This quote is often taken out of context, here's the full quote: "Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%."
If you want a specific example look at something that needs to be performant, i.e. in those 3%, let's say OpenSSL's AES implementation for x86, or some optimized LLM code, you'll see the critical performance sections include things that could be reused, but they're not.
Also the point Knuth is making is don't waste time on things that don't matter. Overuse of DRY falls squarely into that camp as well. It takes more work and doesn't really help. I like Go's proverb there of "A little copying is better than a little dependency."
I think it really depends and it's a case where a lot of engineering judgment and taste comes to bear. For example right now I'm maintaining a Jenkins system that has two large and complicated pipelines that are about 90% overlapping but for wretched historical reasons were implemented separately and the implementations have diverged over the years in subtle ways that now make it challenging to re-unify them.
There is no question in my mind that this should always have been built as either a single pipeline with some parameters to cover the two use-cases, or perhaps as a toolbox of reusable components that are then used for the overlapping area. But I expect the mentality at the time the second one was being stood up was that it would be less disruptive to just build the new stuff as a parallel implementation and figure out later how to avoid the duplication.
Hmm. Certainly there's no doubt that there's technical debt ("do it this way for now, we'll clean it up later") here too, but I think there was also a conscious decision to build something parallel rather than generalizing the thing that already existed to accommodate expanding requirements.
I agree for very specific situations, but compilers tend to get better at optimization over time, and it can be better to express plain intent in the code and leave low-level optimization to the compiler, rather than optimizing in code and leaving future hardware/compiler improvements on the table.
The testing pyramid nonsense is probably the worst one though. Instead of trying to find a sensible way to match the test type to the code, they pulled some "one size fits all" shit while advertising that they aren't that bothered about fixing their flaky tests.
I think you're holding some of these to too high of a bar. This is a one-page article intended to be posted in company bathrooms. Of course it's less comprehensive than a longer blog post.
Most seasoned software engineers stopped following google in that respect a long time ago. They are not a tech shop any more; it's just an add business now with lots of SRE work.
also never write tests for code that doesn't exist because you gradually slow down learning to a crawl and you are no longer writing features but tests and mockups that offer nothing to the end user.
It's important to remember that all best practices are not created equal. I'd prioritize readability over DRY. I'd prioritize cohesion over extensibility. When people talk about best practices, they don't talk about how a lot of them are incompatible, or at least at odds with each other. Writing code is about choosing the best practices you want to prioritize as much as it's about avoiding bad practices.
Maintenance is 90% of a project life time. Sometime those "best practices" rigid implemented means the project won't live to see even it's 1st birthday.
Readability doesn't matter much when you have 10,000+ lines of code. You aren't going to read all that code, and new code introduced by other people continuously isn't something you can keep track of, so even if you understand one tiny bit of code, you won't know about the rest. You need a system of code management (documentation, diagram, IDE, tests, etc), to explain in a human-friendly way what the hell is going on. Small chunks of code will be readable enough, and the code management systems will help you understand how it relates to other code.
Maybe but hallucinations become a real problem here. Even with publicly available API's that are just slightly off the beaten path, I've gotten full-on hallucinations that have derailed me and wasted time.
> I think this is where AI could be helpful in explaining and inspecting large codebases, as an assist to a developer.
That's a great point. Everyone lauds the benefits of chatgpt/copilot in generating new code, but I'm starting to learn that the places they might shine is onboarding onto projects and preliminary code reviews. What LlMs excel at is context, and they should excel in activities where context-awareness is key.
Even if you’re not going to read 10.000+ lines, if the few you read are easy to understand you’re still going to have a much better time maintaining the codebase.
> Readability doesn't matter much when you have 10,000+ lines of code. You aren't going to read all that code (...)
You got it entirely backwards. Readability becomes far more important with the size of your project.
When you get a bug report of a feature request, you need to dive into the code and update the relevant bits. With big projects, odds are you will need to change bits of the code you never knew they existed. The only way that's possible is if the code is clear and it's easy to sift through, understand, and follow.
> You need a system of code management (documentation, diagram, IDE, tests, etc), to explain in a human-friendly way what the hell is going on.
That system of code management is the code itself. Any IDE supports searching for references, jump to definitions, see inheritance chains, etc. Readable code is code that is easy to navigate and whose changes are obvious.
> Readability doesn't matter much when you have 10,000+ lines of code. You aren't going to read all that code,
As someone who has read 10,000+ lines in order to track down surprising behavior in other people's code, I can say without a doubt that readability still matters at that scale.
Code management systems can sometimes be helpful, but they are no substitute.
Ravioli code is a real problem though. Saying small chunks are readable is not enough. The blast radius of a five byte change can be fifteen code paths and five million requests per hour.
To be fair, not all lines of code are equal. A project with a state machine, commands, strategy patterns, etc requires an awful lot of repetitive boilerplate.
A number-crunching app or a data processing pipeline packed with spaghetti business logic is far harder ti read.
I agree and would add that one of the goals for technical design or architecture work is to choose the architecture that minimizes the friction between best practices. For example if you architecture makes cohesion decrease readability too much then perhaps there is a better architecture. I see this tradeoff pop up from time to time at my work for example when we deal with features that support multiple "flavors" of the same data model, then we have either a bunch of functions for each providing extensibility or a messy root function that provides cohesion. At the end both best practices can be supported by using an interface (or similar construct depending on the language) in which cohesion is provided by logic that only cares about the interface and extensibility is provided by having the right interface (offload details to the specific implementations)
The best practices are the ones that allow you to do business and where the maintenance work is relatively not too painful considering the budgeted development time.
Your task is to deliver a good product, not necessarily good code.
> The best practices are the ones that allow you to do business and where the maintenance work is relatively not too painful considering the budgeted development time.
the problem is even that in concrete terms can be controversial. everyone wants to minimize maintenance work; not everyone agrees on what kind of code will achieve that.
"Read" isn't quite the right word for code. "Decode" is better. We have to read to decode, but decoding is far less linear than reading narrative text. Being DRY usually makes decoding easier, not harder, because it makes the logic more cohesive. If I know you only fromajulate blivers in one place I don't have to decode elsewhere.
Well, "read" is still the verb we use most often to describe a human interpreting code. Also, many information-dense books are not intended to be read linearly, yet we still say we're "reading" (or "studying") the book.
I was just mulling this over today. DRY = easier-to-decode is probably true if you're working on groking the system at large. If you just want to peak in at something specific quickly, DRY code can be painful.
I wanted to see what compile flags were used by guix when compiling emacs. `guix edit emacs-next` brings up a file with nested definitions on top of the base package. I had to trust my working memory to unnest the definitions and track which compile flags are being added or removed. https://git.savannah.gnu.org/cgit/guix.git/tree/gnu/packages...
It'd be more error prone to have each package using redundant base information, but I would have decoded what I was after a lot faster.
Bazel solves this exact problem (coming from its macrosystem) by allowing you to ask for what I term the "macroexpanded" BUILD definition using `bazel query --output=build //some/pkg/or:target`.
When bazel does this, it also comments the file, macro,and line number the expanded content came from for each block.
This gives us reuse without obscuring the real definition.
I automated this in my emacs to be able to "macroexpand" the current buid file in a new buffer. It saves me a lot of time.
> Being DRY usually makes decoding easier, not harder
"Usually" being the keyword and what the article is all about IMHO. I work in a codebase so DRY that it takes digging through dozens of files to figure out what one constant string will be composed as. It would have been simpler to simply write it out, ain't nobody going to figure out OCM_CON_PACK + OCM_WK_MAN means at a glance.
Sorry, but this doesn't make sense. Why should system critical things be more difficult to understand? Surely you want to reduce room for error, not increase it?
I mean, sure, I guess API urls could be system-critical. But generally, I prefer to grep a codebase for a url pattern and find the controller immediately. Instead, you have to dig through layers of strings composed of other strings and figure it out. Then at the end, you’re probably wrong.
Does it? Every time I see DRY'd code, it usually makes the project it's in more difficult to understand. It's harder to understand where values come from, where values are changed, what parts of the codebase affect what. And that's before trying to figure out where to change something in the right place, because it's often unclear what other parts of the code are coupled to it through all the abstractions.
At a high level, at first glance, the code might look good and it "makes sense". But once you want to understand what's happening and why, you're jumping through five different classes, two dozen methods and you still don't know for sure until you run a test request against the API and see what shows up where in the debugger. And you realize your initial glimpse of understanding was just window dressing and actually nothing makes sense unless you understand every level of the abstractions being used.
It's suddenly a puzzle to understand another software developer instead of software engineering.
An IDE can help a lot. Coming from Perl, everything you said was true. I wanted everything in one file as much as possible, and breaking tasks off into functions just meant I had to jump around to try and rebuild the flow in my head. I spent so much time inside the debugger since reading the code would only go so far.
Now I work in C#, we have a lot of classes with a few functions, a lot of helper functions. Doesn't matter since it's so easy to use the tooling to build a mental picture - let alone refactor it in an instant if that variable name feels a bit off, or we think a function is not used (such things were always a risky exercise in Perl).
We refactored one insurance based project to use generic base classes extensively since all insurance shares some attributes and features - this really helped cut down complexity of changes and overall just reduced code on the screen to sift through. I had a lot of fun doing this, I'm a weirdo who almost likes deleting code more than writing it. Once you hit the lowest level it is a little less intuitive due to being generic but at the higher levels we mostly work at, it's simpler, and rolling out a new product we get a lot of stuff for free. They got a long way copy-pasting the product logic (4 or 5 product lines) but at this point it made sense to revisit, and I sneak a bit more in each time I have a change to do.
One area I find DRY particularly annoying is when people overly abstract Typescript types. Instead of a plain interface with a few properties, you end up with a bunch of mushed together props like { thing: boolean } & Pick<MyOtherObj, 'bar' | 'baz'} & Omit<BaseObj, 'stuff'> instead of a few duplicated but easily readable interfaces:
Am I crazy for almost exclusively just using type and sum types and no generics or interfaces and somehow being able to express everything I need to express?
Hmm, you can do pretty nice things with generics to make some things impossible (or at least fail on compile), but I agree it’s hardly readable. In some cases you need that though.
Fully agree. I think this is something that takes some time/experience to appreciate though. Junior engineers will spend countless hours writing pages of code that align with the “design patterns” or “best practices” of the day when there’s a simpler implementation of the code they’re writing. (I’m not saying this condescendingly—I was once a junior engineer who did that too!)
I agree with this. Easy to change often means good tests too.
I worked in Perl. Yes it has a reputation for being hard to read, but that was not the problem. Our scripting was pretty basic and easy to read. It's the loose typing, the runtime evals, the lack of strict function parameters, no real IDE, “Only perl can parse Perl” - the fact you can load a module from a network share at runtime, import it, and call a function, based on a certain run flag - and so on. Refactoring was always a mine field and there was a lot I wanted to do in my old job but could not justify it due to the risk.
Readability is almost always (almost only because there are some rare exceptions) the most important thing to me, even for low-level systems software. I always ask myself, “If I don’t touch this code for a year and then come back to it, how long will it take me to understand it again? How long will it take someone who’s never been exposed to this code to understand it?”
Luckily, our compilers and interpreters have gotten so good and advanced that, in 95%+ of cases, we need not make premature “optimizations” (or introduce hierarchies of “design patterns”) that sacrifice readability for speed or code size.
They actually have a Fortran example of "optimized" code that's quite difficult to follow, but allegedly faster according to the comments. But they rewrote it to be more readable and ... turns out that's actually faster!
So this already applied even on 197something hardware. Also reminds me about this quote about early development of Unix and C:
"Dennis Ritchie encouraged modularity by telling all and sundry that function calls were really, really cheap in C. Everybody started writing small functions and modularizing. Years later we found out that function calls were still expensive on the PDP-11, and VAX code was often spending 50% of its time in the CALLS instruction. Dennis had lied to us! But it was too late; we were all hooked..."
And Knuth's "premature optimisation is the root of all evil" quote is also decades old by now.
Kind of interesting we've been fighting this battle for over 50 years now :-/
(It should go without saying there are exceptions, and cases where you do need to optimize the shit out of things, after having proven that performance may be an issue. Also at scale "5% faster" can mean "need 5% less servers", which can translate to millions/dollars saved per year – "programmers are more expensive than computers" is another maxim that doesn't always hold true).
> Dennis Ritchie encouraged modularity by telling all and sundry that function calls were really, really cheap in C.
The old salty professor who taught numerical physics at my uni insisted that function calls were slow and that it was better to write everything in main. He gave all his examples in Fortran 77. This was in the 2010s...
In fact he is right. The advantage of writing modular code, however, is that we can test the locations where performance is needed and optimize later. With a big main it becomes very hard to do anything complex.
This is why I liked it when the language I was coding in supported inline expansion: I could keep my code modular but nevertheless avoid the penality of function calls in performance critical functions in the compiled code.
Was he, though? I mean, yeah having to push and pop a call stack does indeed require more work than not having to do that. However, compilers can and do inline and optimize out function calls.
And what's the real performance impact of calling functions a constant number of times outside of the hot path? Is an untestable spaghetti salad of things better than a few hypothetical push and pops?
There's wisdom behind Knuth's remarks on premature optimization.
The one gotcha with optimizing for “readability” is that at least to some extent it’s a metric that is in the eye of the beholder. Over the years I’ve seen far too many wars over readability during code review when really people were arguing about what seemed readable *to them*
This is the reason I refuse to use the word "clean" to describe code anymore. It's completely subjective, and far too many times I've seen two people claim that their preferred way of doing things is better because it's "clean", and the other's way is worse because it's "less clean", no further justification added. It's absolutely pointless.
There are a lot of topics in software development where everyone can agree that X is correct. However, *defining* X gets into subjective arguments. And yep, readability and clean code are both in that category.
DRY often gives you the wrong or a leaky abstraction and creates dependencies between sometimes unrelated pieces of code. It’s got tradeoffs rather than being a silver bullet for improving codebases.
Having 0% DRY is probably bad, having 100% DRY is probably unhinged
Yes. Especially at the beginning when it's critical to ensure that the logic is correct.
You can then go back and DRY it up while making sure your unit tests (you did write those, right?) still pass.
PS: same applies to "fancy" snippets that save you a few lines; write it the "long way" first and then make it fancy once you're sure it runs the way it's supposed to
Agree and would add that software projects also run through different phases in their lifespans with each phase having their own objectives [1].
So while - as you say - best practices can be at odds with each other - dev teams might be following both over time, just prioritizing one in some phase while completely disregarding it during another.
[1] E.g. the UI of the actual product might pivot multiple times at phase 1 because the product has yet to find its niche or core offering. While at a later stage the focus might be on massive scaling, either in numbers of devs or rolling out the product in new jurisdictions. Other phases might be a maintenance one, when an "offshore" team is given ownership or a sundown of an application.
Imo the very best approach is a codebase that's small enough that you can just do chunky refactors every so often rather than building in extensibility as a "thing". Not applicable to all problem spaces (I'd hate to do this for UI code), but for a lot of stuff it works really nicely.
For me this often looks like an external DSL/API that stays relatively constant (but improving), with guts that are always changing.
I think the only hard and fast rule is to DRY the code that will introduce a bug if you change it in one place and not the other. And if it will, at least do a fat comment in both places for posterity.
Whenever I have to have a "mental model" of the code, I know I screwed up.
+1. If I go with the comment option, then I'll sometimes write a comment like "If you change this here, then you must change it everywhere with this tag: UNIQUE-TAG".
This way, the reader can just do a global grep to find all the places to change, and you don't have to list them in each place and keep them in sync.
A comment is a nice addition, but the very least is to ensure that your test suite properly covers cases where changing one and not the other will introduce a problem. This not only ensures that both are changed, but that both are changed in the way they need to be. A comment alone may prompt you to change both (if you ever read it - I bet a lot of developers don't), but you may not notice when you fail to change them in the same way, which is no better than not changing one.
One of the #1 issues I’ve seen with DRY over the years seems to stem from a misunderstanding of what it means.
DRY is not just about code duplication, it’s about information/knowledge duplication, and code happens to be one representation of information.
Hyper focusing on code duplication quickly gets into premature optimization territory, and can result in DRYing things that don’t make sense. Focusing on information duplication leaves some leeway for the code and helps identify which parts of the code actually need DRY.
The difference is important, and later editions of the Pragmatic Programmer call this out specifically. But the concept of DRY often gets a bit twisted in my experience.
I think the best way to understand DRY is by thinking about the practical problem it solves: you don't want footguns in the codebase where you could change something in one place, but forget to change the same thing in other places (or forget to change the complementary logic/data in other components.)
The goal of DRY as a refactoring, is first-and-foremost to obviate such developer errors.
And therefore — if you want to be conservative about applying this "best practice" — then you could do that by just never thinking "DRY" until a developer does in fact trip over some particular duplication in your codebase and causes a problem.
> you don't want footguns in the codebase where you could change something in one place, but forget to change the same thing in other places
This. Ironically the example on TFA is vulnerable to this issue. Each of the deadline setting methods has a copy of the validation ensuring that the date is in the future. If it's discovered that we need to ensure deadlines are set no later than the project deadline (since that wouldn't generally make sense), it's awfully easy to only update one and miss the others, especially after code has been added and these implementations are no longer visually near each other. I'm not saying that this means the code must be DRY'ed, but it is a risk from the beginning of the project, so one that needs to be weighed during initial implementation.
This is why some advice from Sandy Metz really stuck with me.
It is not a problem to /have/ the same code 2, 3 or even 4 times in a code base. In fact, sometimes just straight up copy-paste driven development can be a valid development technique. Initially that statement horrified me, but by now I understand that just straight up copy-pasting some existing code can be one of these techniques that require some discipline to not overdo, but it's legit.
And in quite a few cases, these same pieces of code just start developing in different directions and then they aren't the same code anymore.
However, if you have to /change/ the same code in the same way in multiple places, then you have a problem. If you have to fix the same bug in multiple places in similar or same ways, or have to introduce a feature in multiple places in similar way - then you have a problem.
Once that happens, you should try to extract the common thing into a central thing and fix that central thing once.
It feels weird to work like that at first, but I've found that often it results in simpler code and pretty effective abstractions, because it reacts to the actual change a code base experiences.
The challenge is that if you're not careful, you can end up copy-pasting the same bit of code hundreds of time before realizing it has to be changed.
I once worked in a year-old startup of ~5 developers that found it had written the same line of code (not even copy-pasted, it was only one line of code so the devs had just written it out) 110 times. A bug was then discovered in that line of code, and it had to be fixed in 110 places, with no guarantee that we'd even found all of them. This was a very non-obvious instance of DRY, too, because it was only one line of code and the devs believed it was so simple that it couldn't possibly be wrong. But that's why you sometimes need to be aware of what you're writing even on the token level.
That's why we have principles like "3 strikes and then you refactor". 3 times fixing a bug isn't too onerous; even 4-6 is pretty manageable. Once you get to 20+, there starts to be a strong disincentive to fixing the bug, and even if you want to, you aren't sure you got every instance.
Oh yeah we've had those as well. I kinda feel two things about these at the same time.
At a practical level, these situations sucked. Someone had to search for the common expression, look at each instance, decide to change it to the central place or not. They spent 2-3 days on that. And then you realize that some people were smart and employed DRY - if they needed that one expression 2-3 times, they'd extracted one sub-expression into a variable and suddenly there was no pattern to find those anymore. Those were 2-4 fun weeks for the whole team.
But at the same time, I think people learned an important concept there: To see if you are writing the same code, or if you're referring to the same concept and need the same source of truth, like the GP comment says. I'm pretty happy with that development. Which is also why my described way is just one tool in the toolbox.
Like, one of our code bases is an orchestration system and it defines the name of oidc-clients used in the infrastructure. These need to be the same across the endpoints for the authentication provider, as well as the endpoints consumed by the clients of the oidc provider - the oauth flows won't work otherwise.
And suddenly it clicked for a bunch of the dudes on the team why we should put the pedestrian act of jamming some strings together to get that client-id into some function. That way, we can refer to the concept or naming pattern and ensure the client will be identical across all necessary endpoints, over hoping that a million different string joins all over the place result in the same string.
In such a case, early or eager DRY is the correct choice, because this needs to be defined once and exactly once.
This really makes me think we should be focusing on cost/benefit, risk/reward, pros/cons at all times. If we have a bug in these 5 copies, will it be too hard to fix in all of them? No? What about these 10 copies? If that sounds like its starting to get difficult, maybe now is the time.
> If we have a bug in these 5 copies, will it be too hard to fix in all of them?
Yes, it will be, because copy-pasted code is never the same verbatim. First and foremost, name changes make it almost impossible to identify different copies. Then, there are different tweaks for each copy to make it suitable for the context. I always DRY early, because it's always free to copy-paste later.
I could make the same argument for not using DRY. The DRY-ed code is hard to change, programmers feel honour-bound to keep using it and tweaking it by adding a variety of parameters to more and more cases, and at the end becomes impossible to understand or update, slowing down development.
Now, what probably should've been 3 abstractions is one incredibly convoluted "abstraction" that makes no sense, and its 3x harder than 3 individual abstractions to deduplicate and inline. It further pulls and invites complexity, as its current size is implicit invitation to include additional cases and places.
Furthermore, while without DRY fixing bugs may've been tedious, now with DRY it may be almost impossible due to high risk of breaking a lot of things that depend on that code. (You might be lucky enough to be able to and have written extensive tests with 100% edge case coverage for it - if that's the case then you've postponed the moment of pain somewhat)
Both can be true. It depends on the context whether benefits exceed costs. Decisions should be made based on a specific context and with thinking applied, not generic rules.
It depends on the context. In some context, they might actually jump out. In some context, even if they don't, it might be fine, because the larger modules containing the code already have excellent tests and are solid and stable
This is why you shouldn't write one line of code, ever again. /s
We've all been there though, at some point in our careers. Possibly multiples of times (try changing thousands of "echo" statements to call a logger because it was initially meant to be a simple script that just kept growing).
It sucks but I've also been on the other side, where it was DRY but 20% of the calls to the function now needed different behavior. Finding all of those usages was just as hard.
> We've all been there though, at some point in our careers. Possibly multiples of times (try changing thousands of "echo" statements to call a logger because it was initially meant to be a simple script that just kept growing).
Been there - now unless it is a very simple / throwaway code, I always start with logging setup from the start. It also helps with print based debugging because you can tune the output.
Metz says she adds TODOs and comments that it has been duped. It's one of those things that requires thought, and she even says it's an advanced technique. How many times is too many? I'm not sure, but I can safely say over 100 is WAY too many. Probably 10 is too many. Heck, if you find yourself updating the same code in four different places over and over and over, it's time to abstract. The idea is to let the code sit and let the abstraction reveal itself if there isn't already an OBVIOUS one. As mentioned by the parent poster, you're looking out for these copies to diverge. If four or five copied codepaths haven't diverged after some time, there's a good chance that just from working on it every day you will have realized the proper way to abstract it.
You absolutely do have to be careful. But even so, it's arguable that having to update something in 100 different places is better than updating in one place and having it affect 100 different paths where you only want 99 of them (this is some hyperbole, of course).
How do you monitor all code duplications in the code base? Including ones that have been modified slightly ( such as optimizations, name changes, additional statements in between, etc)
Tests. AFAIC, this isn't something that should be long living. If it is only duplicated in a couple of places and remains unchanged for years, that's probably fine too, because ya... no one is touching it. If one place does need to change and tests still pass that should mean that the other one didn't need to change and you've reaped the benefit from not prematurely abstracting. There are a lot of ways it could play out, though. Often the duplication is very local and obvious. I think a lot of people take "duplication is cheaper than the wrong abstraction" WAY more seriously than its intended. It's actionable way of saying "don't abstract early" as the counter that is usually: "But then I'll have duplication and DRY is the law." Like EVERY piece of programming advice, though, it's not universal.
This happens with SQL a lot where people copy and paste queries all over the place. Especially for reports, there's always the case where some quick and dirty report was thrown together in 20 mins ends up as something managers can't live without.
Making changes quickly get onerous when the query (or slight variation on it) is pasted into multiple places. Nowadays my org has started to use Power BI so there is also multiple dashboards that all need to be updated.
Conversely, trying too hard to DRY when requirements at call sites start to diverge can lead to an unnecessary complex single implementation of something where there could be two very similar but still straightforward pieces of code.
I think another big part of deciding to use DRY is the complexity of what you're repeating. Copying and pasting lots of business rules means having to make complex edits whenever that logic needs to change. Having simple rules and a small amount of surface area for data reduces the initial need to have DRY code, and it's easier to take the initial risk of using duplicate code.
At least in my experience, it's easier to start with duplicates of simple logic/data, and use DRY for more complex logic/data. The difficult areas are still those where the logic and/or data fall between simple and complex, or are expected to change often, but unknown whether they will change together or diverge quickly.
How is applying DRY entering premature optimization territory (maybe relative to LOC?)? I argue it is instead: premature abstraction.
Optimization is specialization (which is the opposite of DRY): to enable DRY you likely need to generalize the problem (i.e. abstract) such that you remove duplication.
I've always seen "Premature Optimization" as an umbrella that covers a variety of cross-cutting concerns, ranging from:
- Performance
- Code structure / abstraction
- Data structure
- Team organization / org structure
I'd argue that DRY (and a focus on abstractions more generally) are optimizations of the codebase. Not all optimizations are optimizing the same thing.
Yeah, it's like reminding people that code can change, so it's ok to have known flaws day 1. Something forgotten too often.
One thing that really goes against the usual programming grain is DBMSes. We're taught to always decouple/abstract things, but I'm convinced that it's impossible to abstract away your DBMS in most applications. It's just too big of an interface, and performance considerations leak right through it. It's always one of the selling points of an ORM, "you can switch databases later," and then nobody actually switches.
Indeed - the acronym comes from The Pragmatic Programmer, and the author defined it in this way. Every blog post I've read criticizing/cautioning against DRY were not doing DRY as originally defined.
DRY is almost always a good thing to do. Coupling superficially similar code is definitely not a good thing to do.
Yeah, here's the quote from the later editions addressing this:
> Let’s get something out of the way up-front. In the first edition of this book we did a poor job of explaining just what we meant by Don’t Repeat Yourself. Many people took it to refer to code only: they thought that DRY means “don’t copy-and-paste lines of source.”
That is part of DRY, but it’s a tiny and fairly trivial part.
> DRY is about the duplication of knowledge, of intent. It’s about expressing the same thing in two different places, possibly in two totally different ways.
> Here’s the acid test: when some single facet of the code has to change, do you find yourself making that change in multiple places, and in multiple different formats? Do you have to change code and documentation, or a database schema and a structure that holds it, or...? If so, your code isn’t DRY.
And in response, people will complain that they're being dismissed with "you're doing it wrong!" Because that happens with everything in programmer-land.
The easy response to someone feeling this way is to point them to the origin of DRY: The Pragmatic Programmer.
In the book, the authors explicitly call out that many people took the wrong idea from the original writing. They clarify that DRY is not about code, it's about what they call "knowledge", and that code is just one expression of it.
People can still disagree, but the original intent behind DRY is very well articulated.
Yeah, applies to databases and documentation especially. Databases have the ol' 3NF, you also want to avoid copying data from one source of truth to another in a multi-service environment, and sometimes I intentionally avoid writing docs because I want the code or API spec (with its comments) to be the only documentation.
It's a good distinction to make. In the example given, while the code itself looks and does the same - comparing a date to the current date - they represent two different business logic processes, which can change over time.
I mean if there's more instances of this (the DRY "rule" usually says that you deduplicate if there's 3 or more instances) you could refactor it into a utility like "isBeforeNow", but that doesn't add that much value.
Yeah, premature DRY is a pet peeve of mine. Especially since the "size" of the code necessary to trigger DRY is totally subjective: some people apply DRY when they see similar blocks of code, others are so averse to repetition they start abstracting out native syntax.
It is especially hurtful when people apply DRY immediately on some spaghetti code already mixing abstractions.
Then you find yourself untangling intertwined fatorized code on top of leaky abstractions, losing hours/days and pulling your hair out… (I’m bald already but I’m pretty sure I’m still losing hair in these situations)
A good alternative to DRY is WET, or “Write Everything Twice.” Or, in your case, “Write Everything Thrice”. Both better alternatives than automatic, dogmatic DRY.
Seems like a strawman. The thing being repeated here is something which raises if the datetime isn't in the future. So abstract that out and you then get both methods calling raiseIfDateTimeNotInFuture() which then also serves as documentation.
(But yes, if the actual code is as simple as this example, you may as well just repeat it.)
I mean, sure. I'm generally more WET than most of my colleagues, but this...
> Applying DRY principles too rigidly leads to premature abstractions that make future changes more complex than necessary.
... is just one of those things that sounds wise, but is just basically a tautology. Use the best tool for the job, etc. No kidding? Would never have thought of that on my own, thanks sensei.
Seriously, the issue with the quoted statement is not that it's new to anyone, it's that no one thinks they ARE applying DRY principles "too rigidly". This is just chin beard stroking advice for "everyone else".
No, you probably don't know as well as you think you do. No, you're not going to get there by grinding leetcode. No, you aren't going to get there quickly, or without a lot of interaction with more-experienced peers, or without being told that your judgment is bad a few times. (And if you don't listen - really listen - then you don't learn.)
Good judgment in these things takes time and experience. If you have a year of experience and think you know, you're probably wrong.
Like the article ends with, DRY goes hand in hand with YAGNI. The point isn't to build a million abstractions; it's to find the places where you have duplication and de-duplicate it, or where you know there'll be duplication and abstract it, or to simply rearchitect/redesign to avoid complexity and duplication. This applies to code, data models, interfaces, etc.
The duplication is typically bad because it leads to inconsistency which leads to bugs. If your code is highly cohesive and loosely coupled, this is less likely [across independent components].
And on this:
> When designing abstractions, do not prematurely couple behaviors
Don't ever couple behaviors, unless it's within the same component. Keep your code highly cohesive and loosely coupled. Once it's complete, wall it off from the other components with a loosely-coupled interface. Even if that means repeating yourself. But don't let anyone make the mistake of thinking they both work the same because they have similar-looking interfaces or behaviors, or you will be stuck again in the morass of low cohesion. This is probably one of the 3 biggest problems in software design.
Libraries are a great help here, but libraries must be both backwards compatible, and not tightly coupled. Lack of backwards compatibility is probably the 4th biggest problem...
This DRY-sceptical viewpoint is a bit similar to database denormalization.
Sure, in theory you want to store every bit of information only once. But in practice it can make a real difference in smoothing out the access pattern if you don’t follow this normalization religiously.
The same applies to code. If you have to jump through hoops to avoid repeating yourself, it will also make it harder for someone else reading the code to understand what’s going on. A bit of “code denormalization” can help the reader get to the point more quickly.
Right? Creating a noun instead of a verb is the real anti-pattern I see here. (Once you have a DeadlineSetter, it's a slippery slope down to ClassInstanceFactoryConfigProxyManager, etc.)
A practical rule for the presented problem is "wait until you have 3". The number being in reference to the amount of different cases which need to be handled. You're not likely to catch everything that will come up but you'll get enough to think of an extensible abstraction if you don't realize that you already have a workable one.
It really depends on what this code is doing. If it is dialog window rendering - yes, not so important. If it's complicated data validation - you better make it reusable and pure from the beginning.
I agree. The data validation example doesn't seem contrary to the advice; you would generally have at least three data types you need to handle in such a case.
The difference with the dialog window is that (presumably) you don't know the different flavors of window you'll need to render so adding an abstraction on top of the existing rendering abstraction fits squarely in "premature optimization".
407 comments
[ 4.5 ms ] story [ 448 ms ] threadIn the industry code that isn't DRY is a much bigger problem than code that is too DRY.
I absolutely agree with this, and the only thing I would add is that is difference is even more pronounced in codebases using a dynamic language.
Sure it's not easy to navigate a bowl of duplicated spaghetti, but navigating opaque DRY service classes without explicit types is a nightmare.
Luckily as an industry we've realized the benefits of static typing, but your point still holds true there.
Good way to go at it is to isolate the functionality that is used many times and to pull it aside in its own function (or similar). That's just good code practice and also makes it easy to refactor and modify as needed.
Shareable components are more effort to maintain, so just creating them because they consolidate code is not always a good idea. You really want to have positive ROI here and you only get that if you actually reduce maintenance burden. For raw code duplication that doesn’t have a maintenance issue on it‘s own, the bar is a lot higher than most people think.
It would have been OK if they'd used the right methods on Buffer but they didn't.
These encoding/decoding methods are a very good example of code that should be centralized, not least so you can write tests for them. (It is a favorable case for testing because the inputs and outputs are well defined and there are no questions of whether execution is done like you might encounter testing a React component) It is so easy to screw this kind of thing up in a gross way or an a subtle way (I'm pretty sure btoa's weirdness doesn't affect my application because codepoints > 255 never show up... I think)
There's the meme that you should wait until something used 3 times before you copy it but here is a case where two repetitions were too many and it had a clear impact on customers.
To get internet points easily, write something of that:
“Clean code is overrated”
“SOLID is holding you back”
“Tests are less important than profits”
“KISS is the only important principle”
“Declarative programming is only suitable for pet projects”
“Borrow checker is the plague of Rust”
and so on.
Huh. When you put it that way, that's actually a good point. In my experience, competent programming will try to consolidate repeated code, and then cite "because DRY" if asked why, but I can't think of any case where I or anyone else competent started with "needs more DRY" as the original motivation (as opposed to "this is a incomprehensibly verbose mess" or the like).
Conversely, starting with "don't repeat yourself [and don't let anything else repeat itself]" as a design goal does seem to correlate well with cases where someone temporarily (newbie) or permanently (moron/ideologue) incompentent followed that design principle off a cliff.
My experience is the opposite. The less code, the better. I just spent a week on refactoring UI automation test code where they had copied the same 30 lines of code into almost 100 places. Every time with an ID changed and some slightly different formatting. It took me a few days to figure out that these sections do the same thing so I decided to introduce a function with ID as parameter. It was a lot of work to identify all sections and then to make sure they are really equivalent.
Saved us 3000 lines of code and now we can be sure that timeouts and other stuff is handled correctly everywhere. An we can respond to changes quickly.
that's DRY to me. Don't copy/paste code. Introduce functions. Ideally in the simplest way. When you have functions, you declare the same behavior everywhere.
As with anything dogmatic, it truly depends. There are times when the abstraction cost isn't worth it for a few semi-duplicate implementations you want to combine into a single every-edge-case function/method.
One of my biggest gripes (related to the post) is the data structure
which is of course an academic example but that I've seen in many kind of e-business application. Nobody ever seems to think of it until later but two obvious requirements are: (1) query to see what state a user was in at a given time, (2) show the history of a given user. The code to do that in the above is highly complex and will change every time a new state gets added. The customer also has experiences like "we had a student who took two leaves of absence" or "some students apply, get rejected, apply again, then get accepted" When you find data designs like this you also tend to find some of the records are corrupted and when you are recovering the history of users there will be some you'll never get right.If you think before you code you might settle on this design
which solves the above problems and many others in most situations. (For one thing the obvious queries are trivial and event complex queries about times and events can be written with the better schema.) I can't decide if the thing I hate the most about being a programmer is having to clean up messes like the above or having to argue with other developers about why the first example is wrong.If "No code" is to really be revolutionary it's going to have to have built-in ontologies so that programmers get correct data structures for situations like the above that show up everyday in everyday bizaps where there is a clear right answer but it is usually ignored.
1. The first table structure is a flat non-normalized table structure that trades normalization for easy to query and select computed properties
2. Second structure is a normalized table structure that trades the normalization for joins.
It is easy to write a query for the first that gets a list of students names and the dates they applied. That query is harder for the second one. On the other hand figuring out what state a user was in at time t could be a very hard problem with the first table.
My experience with the first is that you find corrupted data records, one cause of that will be that people will cut and paste the SQL queries so maybe 10% of the time they wind up updating the wrong date. Systems like that also seem to have problems with data entry mistakes.
The biggest advantage of #2 is ontological and not operational, which is that in a business process an item is usually in exactly one state out of a certain set of possible states. Turns out that this invariant influences the set of reasonable requirements that people could write, the subconscious expectations of what users expect, needs to be implicitly followed by an application, etc.
Granted some of the dates I listed up there don't quite correspond to a state change, for instance the system needs to keep track of when a student started an application and when the last document (transcripts, letters, etc.) has been received. With 5 documents you would have 32 possible states of received or not and that's unreasonable, particularly considering that a student with just one letter and a very strong application in every other way might get accepted despite that. It's fair to say the student can have an "open application" and a "complete application". Similarly you could say the construction of an airplane or a nuclear power plant can be defined by several major phases but that these systems have many parts installed so if the left engine is installed but the right engine is not installed these are properties of the left and right engine as opposed to the plane.
which industry is that?
in general programming, absolute nope
not-DRY code can be weaseled out with a good ide
badly abstracted code, not so much
in fact in a way, DRY is the responsibility of the IDE not the programmer - an advanced IDE would be able to sync all the disparate code segments, and even DRY them if necessary
but when I read DRYed code, the abstraction better be a complete and meaningful summary, like 'make a sandwich', and without many parameters (and no special cases), or else I'd rather read the actual code
i understand the impulse to try to factorize everything but it just doesn't work beyond a certain point in the real world; it's too difficult to read, and there's always an 'oh, can you just' requirement that upends the entire abstract tower.
same same
i don't believe there is much evidence, certainly nothing conclusive, in this debate
but factorizing code concentrates the logic
that can be an advantage, to a certain degree, but it also reduces resilience, by specializing the code, and can reduce readability by forcing lookups of nested abstractions
For every purported best practice, there are teams/orgs that painted themselves into a corner by getting carried away and others that really would have benefited from applying it more than they did.
In the case of DRY, it's an especially accessible best practice for inexperienced developers and the project leads many of them become. Many many teams do get carried away, mistaking "these two blocks of code have the same characters in the same sequence" with "these two delicate blocks of code are doing the same thing and will likely continue to do so"
Having advice articles floating around on both sides of practices like this helps developers and teams find the guidance that will get them from where they are to where they need to be.
Context, nuance, etc, etc
Best practices can be like that. "Here's something to avoid!" "OK, I'll back far away from that." Yeah, but there's another cliff behind you, of the opposite error that is also waiting to wreck your code base.
Listen to best practices. Don't apply them dogmatically, or without good judgment.
And if you only need the code twice, you very likely wasted time writing the abstraction because copying updates between the two locations is not hard.
This is a rule of thumb, I’m not trying to tell anyone how to do their job.
My brain has a tendency to desire refactoring when I see two similar functions, I want to refactor--it's almost always a bad idea. More often than not, I later find out that the premature refactoring would've forced me to split the functions again.
1: https://en.m.wikipedia.org/wiki/Rule_of_three_(computer_prog...
DRY when it's a wielded as a premature optimization (like all other premature optimization) prevents working code that is tailored to solving a problem from shipping quickly.
Let’s say you have a business rule that you can never have more than 5 widgets. You can make this assumption in multiple places, even with totally different code, and that’s damaging DRY when the rule changes to allowing 6. On the other hand, having a bit of duplicated HTML can help, as they may only be the same by accident.
So DRY isn’t an obvious default for me.
More importantly, cleverer people than me said "premature optimization is the root of all evil"
In raw performance this could manifest as issues with data duplication bloating structures and resulting in cache misses, generic structures expressed in JSON being slower then a purpose-built struct, chasing pointers because of functions buried in polymorphic hierarchies. But I doubt that any of this would really matter in 99% of applications.
In some cases it makes sense, like writing SIMD/etc. specific assembly for compression/decompression or video/audio codecs, but more often than not the readable version is just as good -- especially when compilers can do the optimizations for you.
A lot of times I've found performance increases have come from not duplicating work -- e.g. not fetching the same data each time within a loop if it is fixed.
Also, your digression about modern compilers is irrelevant to the context of the quote, since Knuth talked about premature optimization at a time when compilers were much simpler than today.
If you measure the code's performance and see the ++i/i++ is consuming a lot of the CPU time then by all means change it, but 99% of the time don't worry about it. Even better, create a benchmark to test the code performance and choose the best variant.
But it's your life to live. Who cares what someone else thinks?
If you're talking about consolidating some code from one in-process place to another in the same language, you're mostly right: there's only going to be an optimization/performance concern when you have a very specific hotspot -- at which point you can selectively break the rule, following the guidance you quoted. This need for rule-breaking can turn out to be common in high-performance projects like audio, graphics, etc but is probably not what the GP had in mind.
In many environments, though, DRY'ing can mean moving some implementation to some out-of-language/runtime, out-of-process. or even out-of-instance service.
For many workloads, the overhead of making a bridged, IPC, or network call swamps your algorithm choice and this is often apparent immediately during design/development time. It's not premature optimization to say "we'll do a lot better to process these records locally using this contextually tuned approach than we will calling that service way out over there, even if the service can handle large/different loads more efficiently". It's just common sense. This happens a lot in some teams/organizations/projects.
A really common example is overhead of polymorphism, although that overhead can vary a lot between stacks. Another is just the effect caused by the common complaint about premature abstraction: proliferation of options/special cases, which add overhead to every case even when they don’t apply.
premature abstraction -> not understood dry (https://news.ycombinator.com/item?id=40525064#40525690)
If you want a specific example look at something that needs to be performant, i.e. in those 3%, let's say OpenSSL's AES implementation for x86, or some optimized LLM code, you'll see the critical performance sections include things that could be reused, but they're not.
Also the point Knuth is making is don't waste time on things that don't matter. Overuse of DRY falls squarely into that camp as well. It takes more work and doesn't really help. I like Go's proverb there of "A little copying is better than a little dependency."
Source: Donald Knuth on the Lex Fridman podcast, when Lex asks him about that phrase
It might not specifically be DRY, but still related generic vs specialized code/systems.
https://ieeexplore.ieee.org/document/1410100
There is no question in my mind that this should always have been built as either a single pipeline with some parameters to cover the two use-cases, or perhaps as a toolbox of reusable components that are then used for the overlapping area. But I expect the mentality at the time the second one was being stood up was that it would be less disruptive to just build the new stuff as a parallel implementation and figure out later how to avoid the duplication.
Only in critical regions of code though.
by "get wrong" I mean through static analysis (linters or type checkers) or if it is plainly obvious by running it.
This example is not wrong, but it's not particularly insightful either. Sandi Metz said it better here, 8 years ago https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction
The testing pyramid nonsense is probably the worst one though. Instead of trying to find a sensible way to match the test type to the code, they pulled some "one size fits all" shit while advertising that they aren't that bothered about fixing their flaky tests.
I think this is where AI could be helpful in explaining and inspecting large codebases, as an assist to a developer.
That's a great point. Everyone lauds the benefits of chatgpt/copilot in generating new code, but I'm starting to learn that the places they might shine is onboarding onto projects and preliminary code reviews. What LlMs excel at is context, and they should excel in activities where context-awareness is key.
You got it entirely backwards. Readability becomes far more important with the size of your project.
When you get a bug report of a feature request, you need to dive into the code and update the relevant bits. With big projects, odds are you will need to change bits of the code you never knew they existed. The only way that's possible is if the code is clear and it's easy to sift through, understand, and follow.
> You need a system of code management (documentation, diagram, IDE, tests, etc), to explain in a human-friendly way what the hell is going on.
That system of code management is the code itself. Any IDE supports searching for references, jump to definitions, see inheritance chains, etc. Readable code is code that is easy to navigate and whose changes are obvious.
As someone who has read 10,000+ lines in order to track down surprising behavior in other people's code, I can say without a doubt that readability still matters at that scale.
Code management systems can sometimes be helpful, but they are no substitute.
Ravioli code is a real problem though. Saying small chunks are readable is not enough. The blast radius of a five byte change can be fifteen code paths and five million requests per hour.
A number-crunching app or a data processing pipeline packed with spaghetti business logic is far harder ti read.
>A project with a state machine, commands, strategy patterns, etc requires an awful lot of repetitive boilerplate.
Yeah, good thing we're not a java shop...
Your task is to deliver a good product, not necessarily good code.
the problem is even that in concrete terms can be controversial. everyone wants to minimize maintenance work; not everyone agrees on what kind of code will achieve that.
I wanted to see what compile flags were used by guix when compiling emacs. `guix edit emacs-next` brings up a file with nested definitions on top of the base package. I had to trust my working memory to unnest the definitions and track which compile flags are being added or removed. https://git.savannah.gnu.org/cgit/guix.git/tree/gnu/packages...
It'd be more error prone to have each package using redundant base information, but I would have decoded what I was after a lot faster.
Separately, there was a bug in some software aggregating cifti file values into tab separated values. But because any cifti->tsv conversion was generalized, it was too opaque for me to identify and patch myself as a drive-by contributor. https://github.com/PennLINC/xcp_d/issues/1170 to https://github.com/PennLINC/xcp_d/pull/1175/files#diff-76920...
This gives us reuse without obscuring the real definition.
I automated this in my emacs to be able to "macroexpand" the current buid file in a new buffer. It saves me a lot of time.
"Usually" being the keyword and what the article is all about IMHO. I work in a codebase so DRY that it takes digging through dozens of files to figure out what one constant string will be composed as. It would have been simpler to simply write it out, ain't nobody going to figure out OCM_CON_PACK + OCM_WK_MAN means at a glance.
I don't know the codebase, but to my mind that level of abstraction means it's a system-critical string that justifies the work it takes to find.
When code is serial, with comment blocks to point out different sections, it is much easier to read, follow, and debug.
This is also a little bit of a tooling problem
At a high level, at first glance, the code might look good and it "makes sense". But once you want to understand what's happening and why, you're jumping through five different classes, two dozen methods and you still don't know for sure until you run a test request against the API and see what shows up where in the debugger. And you realize your initial glimpse of understanding was just window dressing and actually nothing makes sense unless you understand every level of the abstractions being used.
It's suddenly a puzzle to understand another software developer instead of software engineering.
Now I work in C#, we have a lot of classes with a few functions, a lot of helper functions. Doesn't matter since it's so easy to use the tooling to build a mental picture - let alone refactor it in an instant if that variable name feels a bit off, or we think a function is not used (such things were always a risky exercise in Perl).
We refactored one insurance based project to use generic base classes extensively since all insurance shares some attributes and features - this really helped cut down complexity of changes and overall just reduced code on the screen to sift through. I had a lot of fun doing this, I'm a weirdo who almost likes deleting code more than writing it. Once you hit the lowest level it is a little less intuitive due to being generic but at the higher levels we mostly work at, it's simpler, and rolling out a new product we get a lot of stuff for free. They got a long way copy-pasting the product logic (4 or 5 product lines) but at this point it made sense to revisit, and I sneak a bit more in each time I have a change to do.
interface MyProps { thing: boolean; bar: string; baz: string; stuff: string; }
Kind of wondering what I'm missing now.
That's almost entirely coincidental with being easy to read. But even easiness to read is a side effect.
I worked in Perl. Yes it has a reputation for being hard to read, but that was not the problem. Our scripting was pretty basic and easy to read. It's the loose typing, the runtime evals, the lack of strict function parameters, no real IDE, “Only perl can parse Perl” - the fact you can load a module from a network share at runtime, import it, and call a function, based on a certain run flag - and so on. Refactoring was always a mine field and there was a lot I wanted to do in my old job but could not justify it due to the risk.
Side effects are usually unrelated or unwanted.
Luckily, our compilers and interpreters have gotten so good and advanced that, in 95%+ of cases, we need not make premature “optimizations” (or introduce hierarchies of “design patterns”) that sacrifice readability for speed or code size.
They actually have a Fortran example of "optimized" code that's quite difficult to follow, but allegedly faster according to the comments. But they rewrote it to be more readable and ... turns out that's actually faster!
So this already applied even on 197something hardware. Also reminds me about this quote about early development of Unix and C:
"Dennis Ritchie encouraged modularity by telling all and sundry that function calls were really, really cheap in C. Everybody started writing small functions and modularizing. Years later we found out that function calls were still expensive on the PDP-11, and VAX code was often spending 50% of its time in the CALLS instruction. Dennis had lied to us! But it was too late; we were all hooked..."
And Knuth's "premature optimisation is the root of all evil" quote is also decades old by now.
Kind of interesting we've been fighting this battle for over 50 years now :-/
(It should go without saying there are exceptions, and cases where you do need to optimize the shit out of things, after having proven that performance may be an issue. Also at scale "5% faster" can mean "need 5% less servers", which can translate to millions/dollars saved per year – "programmers are more expensive than computers" is another maxim that doesn't always hold true).
The old salty professor who taught numerical physics at my uni insisted that function calls were slow and that it was better to write everything in main. He gave all his examples in Fortran 77. This was in the 2010s...
Was he, though? I mean, yeah having to push and pop a call stack does indeed require more work than not having to do that. However, compilers can and do inline and optimize out function calls.
And what's the real performance impact of calling functions a constant number of times outside of the hot path? Is an untestable spaghetti salad of things better than a few hypothetical push and pops?
There's wisdom behind Knuth's remarks on premature optimization.
If "I don't want to maintain three copies of this" is your reaction unifying likely makes sense.
But that assumes the maintenance would be similar which is obviously a big assumption.
Having 0% DRY is probably bad, having 100% DRY is probably unhinged
you are using it wrong
https://news.ycombinator.com/item?id=40525064#40525690
Yes. Especially at the beginning when it's critical to ensure that the logic is correct.
You can then go back and DRY it up while making sure your unit tests (you did write those, right?) still pass.
PS: same applies to "fancy" snippets that save you a few lines; write it the "long way" first and then make it fancy once you're sure it runs the way it's supposed to
not gonna happen once merged
So while - as you say - best practices can be at odds with each other - dev teams might be following both over time, just prioritizing one in some phase while completely disregarding it during another.
[1] E.g. the UI of the actual product might pivot multiple times at phase 1 because the product has yet to find its niche or core offering. While at a later stage the focus might be on massive scaling, either in numbers of devs or rolling out the product in new jurisdictions. Other phases might be a maintenance one, when an "offshore" team is given ownership or a sundown of an application.
Imo the very best approach is a codebase that's small enough that you can just do chunky refactors every so often rather than building in extensibility as a "thing". Not applicable to all problem spaces (I'd hate to do this for UI code), but for a lot of stuff it works really nicely.
For me this often looks like an external DSL/API that stays relatively constant (but improving), with guts that are always changing.
Whenever I have to have a "mental model" of the code, I know I screwed up.
This way, the reader can just do a global grep to find all the places to change, and you don't have to list them in each place and keep them in sync.
DRY is not just about code duplication, it’s about information/knowledge duplication, and code happens to be one representation of information.
Hyper focusing on code duplication quickly gets into premature optimization territory, and can result in DRYing things that don’t make sense. Focusing on information duplication leaves some leeway for the code and helps identify which parts of the code actually need DRY.
The difference is important, and later editions of the Pragmatic Programmer call this out specifically. But the concept of DRY often gets a bit twisted in my experience.
The goal of DRY as a refactoring, is first-and-foremost to obviate such developer errors.
And therefore — if you want to be conservative about applying this "best practice" — then you could do that by just never thinking "DRY" until a developer does in fact trip over some particular duplication in your codebase and causes a problem.
This. Ironically the example on TFA is vulnerable to this issue. Each of the deadline setting methods has a copy of the validation ensuring that the date is in the future. If it's discovered that we need to ensure deadlines are set no later than the project deadline (since that wouldn't generally make sense), it's awfully easy to only update one and miss the others, especially after code has been added and these implementations are no longer visually near each other. I'm not saying that this means the code must be DRY'ed, but it is a risk from the beginning of the project, so one that needs to be weighed during initial implementation.
In fact, the name probably already gives some hint as to what sorts of human orgs this sort of principle would be applicable in.
And, well, the vast majority of orgs require some sort of coordination.
I.e. what you said. Couple logic that needs to be coupled. Decouple logic that shouldn’t be coupled.
It is not a problem to /have/ the same code 2, 3 or even 4 times in a code base. In fact, sometimes just straight up copy-paste driven development can be a valid development technique. Initially that statement horrified me, but by now I understand that just straight up copy-pasting some existing code can be one of these techniques that require some discipline to not overdo, but it's legit.
And in quite a few cases, these same pieces of code just start developing in different directions and then they aren't the same code anymore.
However, if you have to /change/ the same code in the same way in multiple places, then you have a problem. If you have to fix the same bug in multiple places in similar or same ways, or have to introduce a feature in multiple places in similar way - then you have a problem.
Once that happens, you should try to extract the common thing into a central thing and fix that central thing once.
It feels weird to work like that at first, but I've found that often it results in simpler code and pretty effective abstractions, because it reacts to the actual change a code base experiences.
I once worked in a year-old startup of ~5 developers that found it had written the same line of code (not even copy-pasted, it was only one line of code so the devs had just written it out) 110 times. A bug was then discovered in that line of code, and it had to be fixed in 110 places, with no guarantee that we'd even found all of them. This was a very non-obvious instance of DRY, too, because it was only one line of code and the devs believed it was so simple that it couldn't possibly be wrong. But that's why you sometimes need to be aware of what you're writing even on the token level.
That's why we have principles like "3 strikes and then you refactor". 3 times fixing a bug isn't too onerous; even 4-6 is pretty manageable. Once you get to 20+, there starts to be a strong disincentive to fixing the bug, and even if you want to, you aren't sure you got every instance.
At a practical level, these situations sucked. Someone had to search for the common expression, look at each instance, decide to change it to the central place or not. They spent 2-3 days on that. And then you realize that some people were smart and employed DRY - if they needed that one expression 2-3 times, they'd extracted one sub-expression into a variable and suddenly there was no pattern to find those anymore. Those were 2-4 fun weeks for the whole team.
But at the same time, I think people learned an important concept there: To see if you are writing the same code, or if you're referring to the same concept and need the same source of truth, like the GP comment says. I'm pretty happy with that development. Which is also why my described way is just one tool in the toolbox.
Like, one of our code bases is an orchestration system and it defines the name of oidc-clients used in the infrastructure. These need to be the same across the endpoints for the authentication provider, as well as the endpoints consumed by the clients of the oidc provider - the oauth flows won't work otherwise.
And suddenly it clicked for a bunch of the dudes on the team why we should put the pedestrian act of jamming some strings together to get that client-id into some function. That way, we can refer to the concept or naming pattern and ensure the client will be identical across all necessary endpoints, over hoping that a million different string joins all over the place result in the same string.
In such a case, early or eager DRY is the correct choice, because this needs to be defined once and exactly once.
Yes, it will be, because copy-pasted code is never the same verbatim. First and foremost, name changes make it almost impossible to identify different copies. Then, there are different tweaks for each copy to make it suitable for the context. I always DRY early, because it's always free to copy-paste later.
Now, what probably should've been 3 abstractions is one incredibly convoluted "abstraction" that makes no sense, and its 3x harder than 3 individual abstractions to deduplicate and inline. It further pulls and invites complexity, as its current size is implicit invitation to include additional cases and places.
Furthermore, while without DRY fixing bugs may've been tedious, now with DRY it may be almost impossible due to high risk of breaking a lot of things that depend on that code. (You might be lucky enough to be able to and have written extensive tests with 100% edge case coverage for it - if that's the case then you've postponed the moment of pain somewhat)
Both can be true. It depends on the context whether benefits exceed costs. Decisions should be made based on a specific context and with thinking applied, not generic rules.
It's not like they just jump into your eye when you edit some code.
We've all been there though, at some point in our careers. Possibly multiples of times (try changing thousands of "echo" statements to call a logger because it was initially meant to be a simple script that just kept growing).
It sucks but I've also been on the other side, where it was DRY but 20% of the calls to the function now needed different behavior. Finding all of those usages was just as hard.
Been there - now unless it is a very simple / throwaway code, I always start with logging setup from the start. It also helps with print based debugging because you can tune the output.
You absolutely do have to be careful. But even so, it's arguable that having to update something in 100 different places is better than updating in one place and having it affect 100 different paths where you only want 99 of them (this is some hyperbole, of course).
Making changes quickly get onerous when the query (or slight variation on it) is pasted into multiple places. Nowadays my org has started to use Power BI so there is also multiple dashboards that all need to be updated.
At least in my experience, it's easier to start with duplicates of simple logic/data, and use DRY for more complex logic/data. The difficult areas are still those where the logic and/or data fall between simple and complex, or are expected to change often, but unknown whether they will change together or diverge quickly.
Optimization is specialization (which is the opposite of DRY): to enable DRY you likely need to generalize the problem (i.e. abstract) such that you remove duplication.
- Performance - Code structure / abstraction - Data structure - Team organization / org structure
I'd argue that DRY (and a focus on abstractions more generally) are optimizations of the codebase. Not all optimizations are optimizing the same thing.
One thing that really goes against the usual programming grain is DBMSes. We're taught to always decouple/abstract things, but I'm convinced that it's impossible to abstract away your DBMS in most applications. It's just too big of an interface, and performance considerations leak right through it. It's always one of the selling points of an ORM, "you can switch databases later," and then nobody actually switches.
DRY is almost always a good thing to do. Coupling superficially similar code is definitely not a good thing to do.
> Let’s get something out of the way up-front. In the first edition of this book we did a poor job of explaining just what we meant by Don’t Repeat Yourself. Many people took it to refer to code only: they thought that DRY means “don’t copy-and-paste lines of source.” That is part of DRY, but it’s a tiny and fairly trivial part.
> DRY is about the duplication of knowledge, of intent. It’s about expressing the same thing in two different places, possibly in two totally different ways.
> Here’s the acid test: when some single facet of the code has to change, do you find yourself making that change in multiple places, and in multiple different formats? Do you have to change code and documentation, or a database schema and a structure that holds it, or...? If so, your code isn’t DRY.
I've taken to calling that activity (removing syntactic redundancy that is only coincidental) "Huffman coding".
And in response, people will complain that they're being dismissed with "you're doing it wrong!" Because that happens with everything in programmer-land.
In the book, the authors explicitly call out that many people took the wrong idea from the original writing. They clarify that DRY is not about code, it's about what they call "knowledge", and that code is just one expression of it.
People can still disagree, but the original intent behind DRY is very well articulated.
I mean if there's more instances of this (the DRY "rule" usually says that you deduplicate if there's 3 or more instances) you could refactor it into a utility like "isBeforeNow", but that doesn't add that much value.
"Looks the same right now" != "Is the same all the time"
Bad abstraction is worse than no abstraction
Then you find yourself untangling intertwined fatorized code on top of leaky abstractions, losing hours/days and pulling your hair out… (I’m bald already but I’m pretty sure I’m still losing hair in these situations)
(But yes, if the actual code is as simple as this example, you may as well just repeat it.)
> Applying DRY principles too rigidly leads to premature abstractions that make future changes more complex than necessary.
... is just one of those things that sounds wise, but is just basically a tautology. Use the best tool for the job, etc. No kidding? Would never have thought of that on my own, thanks sensei.
Seriously, the issue with the quoted statement is not that it's new to anyone, it's that no one thinks they ARE applying DRY principles "too rigidly". This is just chin beard stroking advice for "everyone else".
Learn when to DRY, and when not.
No, you probably don't know as well as you think you do. No, you're not going to get there by grinding leetcode. No, you aren't going to get there quickly, or without a lot of interaction with more-experienced peers, or without being told that your judgment is bad a few times. (And if you don't listen - really listen - then you don't learn.)
Good judgment in these things takes time and experience. If you have a year of experience and think you know, you're probably wrong.
Or maybe asking yourself what end goal you're trying to achieve by building an abstraction.
But they should think twice if they are building abstractions only for the sake of DRY.
The duplication is typically bad because it leads to inconsistency which leads to bugs. If your code is highly cohesive and loosely coupled, this is less likely [across independent components].
And on this:
> When designing abstractions, do not prematurely couple behaviors
Don't ever couple behaviors, unless it's within the same component. Keep your code highly cohesive and loosely coupled. Once it's complete, wall it off from the other components with a loosely-coupled interface. Even if that means repeating yourself. But don't let anyone make the mistake of thinking they both work the same because they have similar-looking interfaces or behaviors, or you will be stuck again in the morass of low cohesion. This is probably one of the 3 biggest problems in software design.
Libraries are a great help here, but libraries must be both backwards compatible, and not tightly coupled. Lack of backwards compatibility is probably the 4th biggest problem...
Sure, in theory you want to store every bit of information only once. But in practice it can make a real difference in smoothing out the access pattern if you don’t follow this normalization religiously.
The same applies to code. If you have to jump through hoops to avoid repeating yourself, it will also make it harder for someone else reading the code to understand what’s going on. A bit of “code denormalization” can help the reader get to the point more quickly.
The difference with the dialog window is that (presumably) you don't know the different flavors of window you'll need to render so adding an abstraction on top of the existing rendering abstraction fits squarely in "premature optimization".
Just step in when they go too far
https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction