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.
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 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
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 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.
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.
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.