9 ms·
The Pros and Cons of DRY Code
- adam_arthur 6y agoDRY is about creating a common abstraction for code that is "semantically" equivalent, not "incidentally" equivalent. Even if the code is the same today, is there a case to be made for a new feature that would require them to evolve independently? If so, the "logical" meaning of your function is independent between the two use cases. The code looks the same, incidentally (by accident). Speaking moreso to "domain"/business logic. And of course, there's the more obvious DRY case of truly context agnostic operations, like math functions, string manipulations etc. Ideally you're using some standard library for these things... agree with the author that it's better to avoid sharing tiny custom libraries if amount of logic is too low to warrant the extra abstraction. The cost of abstraction is not fixed, of course, but dependent on how common and familiar that abstraction is to the broader audience.
- tomaszs 6y agoFor well known scenarios i use DRY or not DRY right away. But when I don't know how code will evolve i use three times rule. Three use cases are often enought to form an understanding of code usage, to apply DRY if needed. So I just leave the mess for a while and see what will grow. It is a perfectly good solution if we accept that refactoring is also a thing. And it should be. Code can not be perfect from day one. These rules are can be applied widely. For example sometimes it is good to repeat code in s React component and leave it that way. Especially if it is unknown how the design will evolve. So also it is sometimes better to leave some mess that is easier to manage than to slice everything that may make it harder to manage the code further on. Often chasing DRY and other rules too early leads to trouble. I call it early optimization. Just let the code breath! Give it some time to make it perfect!
- dragontamer 6y agoI've heard of WET before, but I don't think that's the best "anti-DRY" phrase. In the unit-testing world, they say "DAMP, not DRY". Descriptive and Meaningful Phrases, which happens to be repeating things a lot more than twice! Some things are just clearer when repeated. Under such conditions, repeat yourself all the time (as long as it is clearer). The question of DAMP vs DRY is eternal. Repetition is sometimes more clear, but sometimes is overly verbose and unclear. Its hard to know without experience.
- colechristensen 6y agoWhen I have encountered DRY in the wild, it has always been DRY zealotry. Repeated code was absolutely never allowed, and everything had to be modularized such that if anyone could ever imagine a future where something might get used again, it needed to be structured like that from the first merge. What it led to was an inscrutable codebase where unless you had been working with it for years, finding out where a thing was actually done was an exercise not unlike an archaeological dig. Like most things, it’s a matter of something which is a good idea, but the reasons it is a good idea are lost in its most common application which is at the exteme.
- tartoran 6y agoIt's a fairly common thing in our industry to take ideas to extremes where the initial benefit becomes a liability. Then comes the next thing and the same zealotry kills the next thing as well. The next thing is very welcome because it allows people to start as a blank slate and escape the corners they painted themselves in.
- potta_coffee 6y agoI think one key problem is whey people attempt to DRY the code before actually understanding what they're building. I like to let the code "tell me what to do"; I'm building something, and I repeat the pattern a few times, so I keep an eye on it. Eventually, I'll have a few repeated patterns I can look at. A lot of times, they're similar, but not the same. Waiting gives me the opportunity to review several similar patterns and find a more elegant solution that can accommodate all of the use cases, rather than forcing a sub-optimal abstraction for the sake of being DRY alone.
- Uehreka 6y agoThere are definitely times where the DRY vs DAMP question is a tough one, but I’ve also had lots of discussions where the DRY argument basically adds up to “not knowing ‘Find/Replace in Project’ exists”. Man, if I had a nickel for each time this has happened: I’m discussing a refactor with a DRY engineer, and they say something like “So what do you suggest? Just replacing that name in - every - single - place - it’s used? That would be error prone and time consuming.” Then I run a find/replace, the unit tests run, the green light flashes, heads explode. Again: DRY vs DAMP is often a very good thing to be debating, but in my personal experience the DRY advocates have gotten way too dogmatic.
- jackconsidine 6y agoYepp. It took me a long time to embrace the mindset that repeating things is sometimes way more elegant than unreadable abstractions. Dan Abramov's article [1] brilliantly put into words how I feel about DRY. It's a great heuristic, but still a heuristic. https://overreacted.io/goodbye-clean-code/ https://overreacted.io/goodbye-clean-code/
- colechristensen 6y agoThat is a great bit of self awareness. “This of how I changed my mind” is a narrative we all need to see more and experience more ourselves.
- SomeCallMeTim 6y agoI felt that Abramov's article showed...nothing useful at all. Yes, he found an abstraction that turned out to be not useful. But that doesn't mean there wasn't an abstraction that could have been useful. Or even that the judgment call that his exact abstraction wasn't useful was correct. DRY is important. Maybe he didn't find the right abstraction, but for the most part it's possible to refactor repetitious code with readable abstractions.
- jackconsidine 6y agoYeah I generally agree with you (excepting Abramov's article being useless). > but for the most part it's possible to refactor repetitious code with readable abstractions. The operative phrase being "for the most part".
- cccc4all 6y agoDan's point in that article was that he spent time doing something basically for the sake of keeping up appearances. Kind of like going through motions without achieving anything useful. It wasn't solving any problems. It's better to focus on solving problems as they rise up, as opposed to doing things to keep in line with some arbitrary standards that may not be relevant. Key is learn how to discern between trivial issues and real issues that affect the project.
- tyleregeto 6y agoDRY is great guideline. KISS is another one. I find that it is not uncommon for the two of them to butt heads a little, and it can be tricky to find the correct balance. In general, I am finding that I favour repetition over abstraction more and more.
- peterohler 6y agoCouldn't agree more. If the code is going to be maintained by someone less experienced then KISS certainly is more helpful than DRY.
- gilbetron 6y agoCorollary: there many great abstractions, but few abstractions are great.
- SomeCallMeTim 6y agoI find DRY to be useful in some cases even when there's only a second copy of the code. I hate seeing any duplicate code past a certain level of complexity. Other times I'll copy a few lines of code all over the place and not worry about it. The core conclusion I've come to is to take ALL such "rules" with a grain of salt. Programming is a skilled craft, and there are no rules that you can apply universally.
- wvenable 6y agoDRY is not about reducing code -- in fact, it's right in the definition the author provided: > Every piece of knowledge must have a single, unambiguous, authoritative representation within a system. It says "knowledge" not "code". This is an important distinction. Removing duplicate code might not be removing duplicate knowledge and having duplicate/similar code doesn't necessarily mean you're duplicating knowledge. A toy example: if you happen to have 2 products that are the same price you still wouldn't want to combine them into one constant value. Some code might be similar by happenstance and combining that will cause problems when that happenstance stops being the case.
- ninkendo 6y agoWhile I largely agree with you about what the right kind of takeaway you should get from DRY, it seems like a no-true-Scotsman argument to me. DRY stands for “don’t repeat yourself”. that’s its de facto definition, unfortunately... whether it’s useful or not. Just like REST and OOP and a host of other things that “everyone gets wrong”, you have to consider that if everyone gets it wrong, maybe the messaging isn’t very good in the first place.
- dragontamer 6y ago> Just like REST and OOP and a host of other things that “everyone gets wrong”, you have to consider that if everyone gets it wrong, maybe the messaging isn’t very good in the first place. In my experience, it just means that an army of ignorant middlemen grabbed hold of a concept and ran too far with it. OOP / UML / XML / etc. etc. They all actually solve problems very well. But the people who taught those concepts were mostly selling snake-oil under the guise of XML or whatever. If any particular paradigm becomes useful: the parasite class of marketers / con-men come out and sell fake versions of the strategy. Eventually, the con-men completely corrupt the concept and we just have to move on to the new concepts that haven't been corrupted yet.
- fckthisguy 6y agoI see this all the time in well-intentioned "agile" projects. They fail to fully understand the domain and then micro optimisations like this lead to features breaking in interesting and unexpected ways.
- bob1029 6y agoI prefer a form of lazy DRY / normalization. Its super fast & easy to take a business service like CustomerLoginService and copypasta it into a new version called CustomerLoginService2 which addresses 99% of the same stuff but has a small tweaks throughout to help with some special business edge case or migration to a new way of doing things. Adding the new service to DI and being able to access it (in a well-designed application) is typically 1 line of code on both counts. I don't worry too much about this kind of stuff anymore. If we end up with 5+ copies of this thing and it actually causes problems or otherwise slows us down, then I would probably consider taking the time to actually clean it up using something like parameters, inheritance, or generics to establish desired context of usage and normalize code. This sort of scheme might look tacky and trigger developers a little bit, but seeing CustomerLoginService2 in a stack trace and being able to instantly zoom to a scoped implementation unique for that context of usage is a pretty big advantage. Contrast with if you were to normalize down to a single CustomerLoginService. Debugging the added conditionals & state will certainly be more cumbersome.
- hertzrat 6y agoIt depends how clear it is to future maintainers that there are 5 places to change the code to implement a given functional change. The major risk is often that one of those 5 files gets forgotten about and it leads to a surprise later on. You can use parallelism and naming conventions to help communicate this
- avereveard 6y agoInterfaces do well for that, let you work as much as you need while breaking early if some of the classes diverges too much and being one "find implementations" away from each other
- dsego 6y agoSure, but my experience is that this is more in line with the client's or manager's mental model and expectations. Eg, a designer will lay out some sort of user story which typically defines "flows" from page to page. And they don't expect that the work on this new "flow" will break the existing ones. In this case sharing code can become a liability. And this sh* happened a bunch of times where other devs trying to be clever, instead of just duplicating a UI component or page and making a few changes, they put in a bunch of params to support the new use cases (a better way of course is to extract common elements). The result as predicted was a spaghetti mess to untangle. Refactoring it is like dismantling a bomb.
- jbay808 6y ago> A great example would be a math function like log2. That function should work for every case that you need to calculate a logarithm – each call is fundamentally the same. Even in the case of log2, some log2s might need to be fast approximations, others accurate down to the LSB, some operating on complex numbers, and others on array-like data...
- contravariant 6y agoIt's still correct in the sense that you shouldn't repeat the code for calculating the logarithm in every single scenario where you need one. Of course if you need a function for a different purpose then it makes sense to make another one. Of course mathematics is always an endless game of expanding and contracting definitions so might need some additional versions anyway (most commonly something like f(x) = log(1+x)), though if you're clever about it you can rephrase most of what you want with existing functions.
- cccc4all 6y agoPremature optimization is root of all evil - Donald Knuth https://stackify.com/premature-optimization-evil/ https://stackify.com/premature-optimization-evil/ Don't be evil.
- kiwiandroiddev 6y agoNice. I like the distinction the author identified - coincidentally similar vs. fundamentally similar code. I think the passage of time is sometimes the only way to tell the difference between the two. More specifically, by seeing if the code changes in all places for the same reasons. I wrote an article on this that came to similar conclusions: https://coderefinery.nz/2019/01/28/beyond-dry-why-redundancy-makes-your-code-more-robust-and-less-fragile/ https://coderefinery.nz/2019/01/28/beyond-dry-why-redundancy...
- maerF0x0 6y ago>Obviously, the cognitive burden becomes greater and greater the more definitions you need to jump through. Who says the cognitive burden is lowered just because of adjacency? If you have to keep N variables in mind, its about the same if they're at current depth or 1 level deep. To me the most evil form of DRY is when you get functions calling functions with arguments that are just passthrough. If a function only uses an argument to call a subfunction, it's a sign that those functions should be decoupled and better orchestrated.
- adam_arthur 6y agoI've found the problem of "too many abstractions" fairly common in functional programming contexts... though not saying it's inherent to the paradigm. Usually it manifests in having tens or hundreds of tiny, stateless functions, with very little logic in each one. There's a common line of thinking about this being "good" because you can "mathematically" prove the correctness of a function, and thus can ignore its implementation. But abstraction is only a big win when the abstraction is easily and widely understood. When you create a one off, ad hoc abstraction its purpose is not going to be obvious to the reader off the bat. The important thing is that the total "api surface area" within your context is low, and each function/abstraction has sufficient meaning. There's a sweet spot where you can fit most of the local functions in your head, without having to constantly re-read to remember the purpose of a given function. Naming is quite important to this end.
- hinkley 6y agoA single level of pass-through may indicate a boundary between two modules. Two layers of pass-through means you have two abstractions talking to each other, which is not good. I'm removing some of these from our code right now. The hard thing to remember about code is, nobody gives a shit about your code when everything is working. Nobody is poring over it basking in your magnificence. If they're looking at your code, it's because something is wrong. So they're looking for wrong things, and they don't know where the wrong thing is. So they're looking at every code smell they see along the way doing a probability calculation. Your ornate clever code causes them to dump important state, such as "why am I looking at this code in the first place?". In the case of argument passing, we're all familiar with argument transposition. I have to check you haven't done it. If you rename variables, I have to watch for that too. If you have a stack of empty methods just ferrying arguments around, you've multiplied the surface area I have to think about.
- betenoire 6y agoThe article is about DRY, but it opens talking about clean code. IMO, "clean" is a poor word to use when evaluating code quality, not because there is no clean code like the author says, but because clean is an undefined subjective judgement call. What do we _mean_ when we say clean? Easy to debug? Follow? Maintain? Extend? Predict? These are more objective qualities and can be quantified. I think using more precise language instead of "clean code" results in ... uh.. cleaner code? lol
- potta_coffee 6y agoI know someone that hates "for" loops and views them as "unclean". I find this aggravating.
- ZephyrBlu 6y agoAfter getting used to .map() and .forEach() in JS, I have become far more fond of those methods than for loops.
- potta_coffee 6y agoI used those methods too, but I don't have the dogmatic opinion that loops are not "clean code".
- hinkley 6y ago> Are there special cases or arguments only used by a fraction of callers My thought is special case arguments are fine for internal calls but are lousy for external ones. If you have two public functions with the same meaning but slightly different circumstances, having a protected method that handles both via an extra argument makes sense. Consequently, the only way to end up with five conditional arguments on that function is to have an enormous number of public functions all doing more or less the same thing. Somewhere around 2, alarm bells should have been going off in your head, asking why you/we have done this and perhaps now is a good time to stop?
- ehnto 6y agoI find the concept of pontification useful. If you find yourself pontificating in your software, that is, writing abstractions for the sake of abstracting things, you may be going too far. Abatractions should be clear, concise and purposeful.
- NuSkooler 6y agoI would absolutely DRY the example. Auth middleware is an example of DRY, which in the real world would likely live here.
- jwalton 6y ago> Every time we extract a chunk of code from a larger function into a smaller more encapsulated one, the code becomes just a little bit harder to follow. I, in most cases, disagree with this. If this were true, I suspect the best and easiest to understand functions would be thousands of lines long Often you write functions that only get called from one place, just so you can give a name to a block of code and make it easier to understand. Just today, for example, I wrote a big case/switch statement to figure out the URL for a user on a system where the URL is different depending on where the user exists in a hierarchy (I didn’t write the server side of this - gotta work with what I have). I extracted this all out into a “getUserUrl()” function. In the caller, it’s much easier to understand what is going on when you see “getUserUrl()” than it would have been if there was the giant switch statement there. Cognitive burden would generally not be improved by inclining every function I call.
- jmchuster 6y agoDon't worry about DRY until you hit three copies. You probably don't know enough to make the right tradeoffs until then. To me DRY has always been about, i need to change something, and now need to magically know everywhere else it was copied to to change as well. So that's kind of my mental check for when it starts being worth it.
- aryehof 6y agoThe end of the 1940s gave us the subroutine. A means through which one could encapsulate some functionality into a named construct that could be called (reused) throughout a program. It’s a construct available for a programmer to use. DRY encourages one to do so when appropriate. Dogmas interpreted that one must always do so are simply unhelpful.
- weeboid 6y agoTakeaway: your code should be WET AF
- nojvek 6y agoI personally follow a rule called “simplest model that explains foreseeable reality”. So at first you really have to dig into what “reality” is .e.g if you are building an auth system, how many ways do you login? Login page for customer, login via sso, login via google auth federated etc. Now foreseeable reality is what is most likely going to happen. YAGNI proponents would say don’t do this but I’ve found that thinking through future scenarios that competitors are doing or customers asking for (even though you don’t have time to do it) should be taken into account. You realize it would be nice to support auth via personal tokens restricted to certain api calls. Having a solid understanding about foreseeable future is paramount. Task #1 Then it’s understanding what pieces need to be stitched together to make it happen. What changes together, what changes independently. What are the separation of concerns between the pieces? You need to represent different auth types in db. A central place that takes a request and it’s headers to authenticate. Also authentication and authorization are different things and shouldn’t be bolted in the same place. Having an in-depth understanding of separation of concerns and boundary contracts is Task #2. Nailing 1 and 2 down usually leads fo clean implementations. Also huge believer of continuous refactoring. Sometimes reality changes. Focus on a small thing you understand for sure. Incrementally add things and re-think the separation of concerns and how reality is represented in datastructures. If that needs a change, make it happen. Building on wrong level of abstraction and working around it has caused a great level of pain.
- Person5478 6y agoThere are a lot of things that go into the decision to future-proof (and how) vs doing the absolute simplest thing and moving on. One of the things that goes into it is how clear the direction the business is headed in and how experienced you are with both the tech stack and solving the specific problem.
- ro-mx 6y agoI find Repeating Code very useful to find common patterns and then refactor over those to do DRY.
- Cthulhu_ 6y agoGotta know the common factor before you can make the right refactoring. And then you can make the judgment whether the abstraction is an improvement over the duplication. Better to have multiple lots of dumb code than one lot of clever code.
- remify 6y agoMy motto is abstract at the third duplication. Before that you don't know if you're going to need it.
- peterohler 6y agoDRY is something to strive for but as pointed out there are often tradeoffs with DRY. One that I ran into not too long ago when writing https://github.com/ohler55/ojg https://github.com/ohler55/ojg was the performance penalty that DRY code can incur. Sadly there is a lot of repetition in that code but by avoiding a dry approach the code is also several times faster than if more functions were used to avoid repetition.
- Applejinx 6y agoI think the whole problem with DRY is that it's too often about adopting layers of abstraction that aren't useful. They're not worth the overhead they demand. Therefore, the opposite of the DRY issue could (should!) be Worth A Preemptive Unhooking of State. And most of that stuff will not be at all worth unhooking whatever state you've got in mind, in order to go into whatever layer of abstraction is required to DRY up something absolutely trivial and meaningless. Semantics matter. We need to be focussing on what's important in the problem set, not getting derailed into computational linquistics. Code that is WAPUS-Y might be deeply unfamiliar territory to our more rigorous brethren, because the very idea of asking whether something is Worth A Preemptive Unhooking of State implies there are matters outside what we learn in comp sci, a subject to our coding outside the coding itself. But that is so often true. And maybe this framing of it can be, erm, sticky and illustrative in its own right :D