Unless you find yourself writing the exact block of code, without any modifications, 3 or more times it’s better to have the “duplicated” code. Until you hit 3+ times you are just guessing at future usage and in my experience developers, myself included, are terrible at guessing the future.
I’ve watched “DRY” code turn into a monster when someone, like myself, tries to force a bunch of use cases into a single flow in order to be DRY. You end up with confusing code that’s trying to do too many things in a single function/block littered with if/else such that stepping through it (in your head) is complicated and error-prone.
I regularly ask the developers who work under me to first duplicate the code and use it a few different times before going back and deciding “can this be made generic without standing on our heads?”.
Recently I had to deal with an item list component that was made to display 2 vastly different types of data. The component was responsible for rendering out the items themselves since they had some UI similarities. The code is nightmare with a ton of input properties to tweak the display/functionality based on the type of item you want to render out. All in the name of DRY, the “well these look similar so we better abstract this and use it in both places”.
When I was a younger developer I thought this way and wrote code this way. It’s unmaintainable, entirely too “clever” (that’s a bad thing), and hard to reason about. Nowadays I value readability and ease of understanding over “DRY for DRY’s sake”.
Instead, if you focus on not repeating the important “knowledge” of your code (algorithms, business rules, etc), it’s easier to avoid the trap of over-abstracting.
Better put, if you have logic A, B, C, D and 2 code paths:
Code path 1: ABD
Code path 2: ACD
Then you should have:
Function 1: ABD
Function 2: ACD
Not
MegaFunction: A (if X then B) (if !X then C) D
Too many people see a common “A” and “D” and rush to have a common function that if/else’s the B/C.
The first example I gave is parsers—even after you've factored out as many helper functions as you can, you'll eventually hit a floor where you have to use those helper functions and piece the results together. That floor is necessarily repetitive—you end up with a bunch of 5 to 10 line functions calling the abstractions and feeding the results together.
Unit tests is another example: I find that when people get too DRY in unit tests it just ends up obfuscating what the test is testing and makes changing the program later harder. Same as with parsing, there is a floor for how much abstraction is reasonable and once you have hit that floor you still have to go through the rote repetition of stringing functions together and asserting things about the results.
With a parser you actually have to do this, but with unit tests people will as often as not give up on the tedium and just not cover all the edge cases. Copilot enables developers to stop writing the repetitive code and spend their unit testing time thinking of edge cases.
The next evolution as you are saying would be to detect if its a repetitive code and modularise it or refactor it.