31 ms·
Someone else’s comment [1] I saved from an older post also about DRY I’ve usually heard this phenomenon called “incidental duplication,” and it’s something I f
by shhsshs 6y ago
Someone else’s comment [1] I saved from an older post also about DRY
I’ve usually heard this phenomenon called “incidental duplication,” and it’s something I find myself teaching junior engineers about quite often.
There are a lot of situations where 3-5 lines of many methods follow basically the same pattern, and it can be aggravating to look at. “Don’t repeat yourself!” Right?
So you try to extract that boilerplate into a method, and it’s fine until the very next change. Then you need to start passing options and configuration into your helper method... and before long your helper method is extremely difficult to reason about, because it’s actually handling a dozen cases that are superficially similar but full of important differences in the details.
I encourage my devs to follow a rule of thumb: don’t extract repetitive code right away, try and build the feature you’re working on with the duplication in place first. Let the code go through a few evolutions and waves of change. Then one of two things are likely to happen:
(1) you find that the code doesn’t look so repetitive anymore,
or, (2) you hit a bug where you needed to make the same change to the boilerplate in six places and you missed one.
In scenario 1, you can sigh and say “yeah it turned out to be incidental duplication, it’s not bothering me anymore.” In scenario 2, it’s probably time for a careful refactoring to pull out the bits that have proven to be identical (and, importantly, must be identical across all of the instances of the code).
[1] https://news.ycombinator.com/reply?id=22022603&goto=item%3Fid%3D22022466%2322022603 https://news.ycombinator.com/reply?id=22022603&goto=item%3Fi...
- js8 6y agoIn imperative languages without macros, it might be difficult to abstract structural similarity, like a common if statement. In functional languages, where every statement is a function, this is often easier.
- jayd16 6y agoYou can write your method in a functional style in the popular imperative languages. This can stretch how DRY you can be a bit further.
- systemvoltage 6y ago> Then one of two things are likely to happen You forgot the third most common case - 90% of those repeated methods will never have a bug and now you've got a code base that will start smelling bad and no one is going to want to work on it. I agree with some of the things here, but allowing multiple places of repeated code unless there isn't a bug sounds like a terrible idea. Lot of these small methods will never have a bug and they'll continue to rot the codebase.
- lostcolony 6y agoCan you get the trade off right 100% of the time? Because I can tell you, every time I've worked on a codebase that repeated itself, it has been a freakin' -delight-, compared to the times where DRY was taken as a commandment from on high. The former when something broke, I could just...fix it. And it would be fixed. Would other, similar situations, still be broken? Sure! And when those would be raised up we'd fix them too, and compare them with the other changes, and possible refactor. Fixing one bug = one bug less. The latter? Oh God. Something is broken? We'd fix it. Aaaand, now there'd be two bugs. Fixing one bug = more bugs. Perfectly balanced code, yes, fixing one bug = multiple bugs fixed. That's the goal. But you won't get it right if you do it pre-emptively. Which of those other two options would you prefer?
- deleted 6y ago[deleted]
- throw1234651234 6y agoI don't even know how to comment to something like this. I guess with an example - I spent some weekend time fixing 30 file paths in a Cloud function, because the junior developer couldn't abstract the root to a single method. After you do something like that, tell me how much you love fixing bugs everywhere.
- throwaway201103 6y agoFixing the 30 file paths was not just a quick global search/replace?
- leni536 6y agoRule of thumb: If you can't give the thing a name then maybe don't extract it. What you extract becomes an abstraction. No abstraction is better than a bad abstraction.
- bluSCALE4 6y agoThis is absolutely the rule. The problem I see a lot is that someone else will come in and want to add an edge case to my generalized code or will try to include edge cases in their "generalized" code. Personally, I have 0 problems with DRY code. I don't even treat it like a requirement but sometimes, I'm trying to code something and I KNOW the code needs to be generalized or at least part of it and that's when I'll spend an hour on a 15 min fix. Effective DRY comes with experience.
- axaxs 6y agoNot a bad rule, but from what I've seen mainly in corporate Java, you can give everything and anything a name.
- snapcore 6y agoThis sounds similar, but different to the three strike rule: https://en.wikipedia.org/wiki/Rule_of_three_(computer_programming) https://en.wikipedia.org/wiki/Rule_of_three_(computer_progra...
- Groxx 6y agoMy boundary on DRYing has settled on "when it's semantically the same code". Code that looks the same but is used for different purposes / has different meaning? Keep it separate. They may diverge in the future. Code that truly means the same thing, but may or may not look the same? DRY it up, any variation is probably unintended and will surprise people later when A doesn't behave like B.
- vlfig 6y agoExactly this. One thing is to have (today) the same behaviour, another is to (forever) be the same thing.
- dave_sid 6y agoUncle Bob covers this by talking about how real duplicate code should change for the same reason. If the two pieces of code look the same, but from a business perspective can change for very different reasons then you have incidental duplication and should be treated separately.
- throwaway201103 6y agoI found this so often when I was doing simple back-office crud coding. There would be a new business use case that was very similar to something we already had coded. Code would be copied, since that's the easiest thing to do and you at least started with something that you knew was working. Later, the new use case would evolve and have some new requirements. Had we abstracted the functionality originally, we'd have to go back and make the abstraction handle both cases. As it was, we could just change the copy that needed to change, and know that we weren't going to break the other case. This was also before the practice of automated unit testing was well understood and supported by development tools, so the motivation to "not risk breaking working code" was much stronger than maybe it is today.
- fiddlerwoaroof 6y agoSo, this take is sort of fashionable now, but it’s never really convinced me: what I’d suggest is that, when you discover that the extracted method is a bad abstraction, you do one of two things: (1) inline the method (IntelliJ is great at this for Java) or (2) duplicate the method and rename, adjusting the new version for the new use case. As hard as naming may be, adding the level of abstraction often ends up helping keep each method working at a single level of abstraction. In my experience, the biggest messes I’ve inherited were a result of not being DRY enough: the cases of incidental duplication I’ve run across have been comparatively easy to unwind.
- throw1234651234 6y agoThis is a very key point - when you re-use, you often need to rename. I actually can't think of a clear example, but this happens ALL the time.
- jonno123 6y agoCouldn’t agree more. I can agree that premature abstraction is bad. But unnecessarily duplicated code outweighs this by orders of magnitude, in my experience. So stay DRY, and later when you hit that 1 case in 100 where the code needs to diverge, it’s easy enough to copy/paste.
- dnissley 6y agoI like to say that the opposite of DRY is PYIAC: Painting Yourself Into A Corner -- because you often only notice a situation like this once it becomes incredibly difficult to get out of.
- rileymat2 6y ago> So you try to extract that boilerplate into a method, and it’s fine until the very next change. Then you need to start passing options and configuration into your helper method... and before long your helper method is extremely difficult to reason about, because it’s actually handling a dozen cases that are superficially similar but full of important differences in the details. What if the flaw is not the initial deduplication, but the flaw was to continue to use it by adding additional responsibilities? When I think of the incidental type, I think of things that just happen to be alike, a contrived example would be ENUM value that happens to be 3 used in different places ands that works because both happen to use 3.
- reader_mode 6y agoDRY is a good principle to default on, juniors need to learn to pick this up - I constantly need to fix this kind of thing in reviews, they haven't learned to recognise when stuff should be factored out and they also write buggy code so it's easy to show them that writing the code once, isolating dependencies and testing it is a better approach than coming up with the similar code over and over in ad-hoc fashion. Once you start getting it you tend to go too far (everything is a nail when all you have is a hammer) and experience helps you learn to judge when to use it - but it's an invaluable tool and you need to use to get the experience - so I would suggest extract shared code by default - inline when it gets too complicated.
- potta_coffee 6y agoI try to suggest this exact strategy, but it's hard to find acceptance in my current job. I call this "letting the code tell me what to do." I find that religious adherence to DRY and all kinds of other rules ends up in just...shitty, shitty code.
- SkyMarshal 6y agoGood observation. I seem to recall in one of Linus Torvalds' old rants about C vs C++, he mentioned locality, and how abstracting everything possible in a codebase resulted in loss of locality. Locality encompasses multiple concepts, one of which is what you mention. Another is the ability to look at any section of code and understand exactly what it does without having to dig through chains of abstractions. The implication is that locality results in more LOC, but a more tractable codebase, especially as it gets larger and more complex and more difficult for engineers to hold in their head.
- splittingTimes 6y ago"Let the code go through a few evolutions and waves of change. Then one of two things are likely to happen...". That sounds great in theory, but how long is your evolution time and do you keep track of the changes in practice? I would assume a good evolution time is 3 to 6 month. I would say half the time I work on a story i face that decision. If you close 1 to 3 tickets a week you have to track a lot these decisions [1]. Now add that you work on a team of 5 and you have 5 engineering teams overall.. it seems to me that this approach does not scale. [1] granted not all tickets will be new decisions in new places of the coffee base and you do come across some of your old changes and do the actual evolution.
- TeeMassive 6y agoThank you for pointing this out; I had many headaches pointing this out during code reviews where devs see the good habit of reusing code as a holy mandate. My rule of thumb is to only regroup what shares the same code and also the same purpose. If you think about it most code is doubled somewhere and only what they worth with change from occurrences. And yet it should not be a reason to create refactoring entities with multiple outbound references.
- malux85 6y agoIt’s a trade off - DRY vs abstract Dependency, are they superficially familiar (then don’t create a unnecessary dependency) or are they functionally familiar? Then DRY
- daitangio 6y agoI use a similar approach: first code, then take a refactoring session and look if some repetitive code can be "condensed" in a common method. By the way the copy-pasta code is very resilient: a bug introduced in one place cannot spread, because the other code is duplicated and old. Instead if you have too much condensed code, you end up with complex method which are difficult to maintain, because you have a huge "coverage land of code", and you get scared when you need to change it.
- gwillz 6y agoI've actively taught this to students as WET and DRY. Write Everything Twice, then DRY it later.
- MaxBarraclough 6y ago> before long your helper method is extremely difficult to reason about, because it’s actually handling a dozen cases that are superficially similar but full of important differences This reminds me of Mike Acton's Three Big Lies of C++, specifically Lie 2: Code should model the world. [0] > A chair is a chair, in real life. But in terms of data-transformations, in terms of what we do, these classes are really only superficially similar. In the context of a game, we have a Chair, a PhysicsChair, a StaticChair, a BreakableChair. These things are not at all similar. There's almost nothing that's the same between these contexts. How they're handled, how the data is managed, how the data is transformed, there's virtually nothing that's the same here, and yet the tendency would be because they share some world-modelling similarities, their similarities in the real world, they ought to be connected somehow in the code hierarchy, which is non-nonsensical. World-modelling leads to monolithic, unrelated data-structures and transforms. [...] You can't make a problem simpler than it is. [0] https://youtu.be/rX0ItVEVjHc?t=1153 https://youtu.be/rX0ItVEVjHc?t=1153
- rualca 6y ago> A chair is a chair, in real life. But in terms of data-transformations, in terms of what we do, these classes are really only superficially similar. In the context of a game, we have a Chair, a PhysicsChair, a StaticChair, a BreakableChair. These things are not at all similar. I feel like that comment either takes a very literal and naive analysis to the problem or fails to identify the objects being used. Just because the word "chair" pops up in a few objects that does not mean they are supposed to be the same thing, and thus modeled as specialization of a common Chair class. For example, PhysicsChair makes sense as a specialization of a physics-related class, not a Chair-related class. Breakable hair would also make sense as a specializationof a physics object, which might be comprised of multiple discrete elements or track damage to generate new particles when a threshold is reached. If we take the single responsibility principle seriously, it makes sense to have specialized physics and graphics classes that implement the functional requirements of handling a chair. This by no means implies that class Chair should be a superclass of all these other cases, or even that it makes sense to even consider them to be related at all. A failure to identify the models and their functional relationships doesn't mean that your domain has to include relationships that don't really exist nor make sense.
- m463 6y agoIt took me a while to realize this truth, because you might learn it only by seeing your own code after forgetting it, or spending enough time in other people's code to understand how it works through and through. One good example of this is paths in makefiles. It's so much easier if you just make all the paths relative to the root of the project, but people have to either over-abstract or under-abstract everything or both. $(CC) -o ../build/foo foo.o $(BARDIR)/bar.a ../bletch/bletch.o $(BAZLIB) etc.. why not just: $(CC) -o $ROOT/build/foo foo.o $ROOT/build/bar.a $ROOT/bletch/bletch.o $ROOT/baz/baz-2.0/bazlib.a this goes for all sorts of things. Yes, you might build a directory from somewhere else or might, might might. But why not make it clear what you are doing, not unclear depending on what you might do?
- thunderbong 6y agoRule of Three - https://en.wikipedia.org/wiki/Rule_of_three_(computer_programming) https://en.wikipedia.org/wiki/Rule_of_three_(computer_progra...