as most of us know we have DRY which is a very important rule to keep things maintainable. To keep our application behaviour deterministic we want to make sure that we only have 1 place where certain things are happening.
Like having only one query function instead of 14 in different files with slightly different behaviour.
But as functions fullfill our needs, we tend to create a pivotal point so we only have 1 function to rule them all and push unneeded complexity onto it.
My question is -> when do you think it's valid to break DRY for the sake of simplicity ? I refer to simplicity in the context of Bill Hickey the simple made easy.
I am a backend dev and usually that means DRY and generics are the holy grail! In the frontend however it tends to be less efficient, because the bottleneck is often not efficiency of code but output of layouts.
Having 10 specific tables is better than just having 1 with 80 parameters for example -> faster adoption, less complexity.
Just to be sure .... I don't say violate it in general, I ask what could be reasons to violate it.
Instinctively, I know that breaking DRY is sometimes necessary, but the few examples I've thought of can still be handled with limited refactoring in order to keep to DRY. So, I'm coming up empty. I'm sure there's something, though...
As an older die-hard DRY coder I was a little flummoxed to have to accept a pretty substantial copy/paste-coding approach in a non-production prototyping scenario. JavaScript developers tend to lean on copy/paste more than other languages in my experience, but I fought a losing battle on that front.
The situation was a pretty complicated one:
If this had been a production context I would have more strongly argued for a fully DRY, fully tested approach. It's actually a great case for integration tests being able to provide added security. But since the requirements were driven by visuals and constantly-changing specs, it just turned out that it was too heavy-handed of an approach, and in fact the more we tried to share any part of the app that differed the messier things became.
We ended up continuing to share a large portion of logic, structure and components between the 2 app prototypes, but moved to a fully non-DRY approach for anything with significant differences. Of course that led to sloppy cases where similar logic was not maintained across both apps, and all the expected problems that can arise from non-DRY code duplication, but since it was a prototyping context it didn't matter as much.
Again, in production I would suggest that everyone (especially JavaScript coders!) try and adhere to DRY strictly. But I also learned that there are certain cases where strict coding standards can end up adding drag to a fast-moving process.
My take on DRY is that it is just a pattern like any other with upsides and downsides. It has appropriate applications and situations in which it really doesn't make sense.
One example, when DRY is not appropriate, is when you have different contexts and try to enforce a DRY relationship between them. Later on, you decide that in one context, your function should behave slightly different, so you change it. But to your surprise, you change other contexts as well, which you really didn't want to. DRY is good, but use it reasonably!
As a javascript dev, there is one useful use case where breaking the DRY principle has a benefit - the arguments object built-into the function body.
arguments is an Array-like object, but it is not an Array. To use array methods on it, one usually borrows from the Array prototype to convert it to an array:
var _slice = Array.prototype.slice
function () {
var args = _slice.call(arguments)
// "args" is an array of the arguments this function has received
}
This causes two performance caveats:
_slice.call creates a new bound function every time it executes.arguments -arguments overloading, arguments mutation and leaking arguments. If you pass arguments to a function, that counts as leaking arguments.Since you can't pass arguments around as function parameters (if you want your code to be optimized), you're forced to do this in every function:
function () {
var args = []
for (var i = 0; i < arguments.length; i++) {
args[i] = arguments[i]
}
}
This is solved in ES2015, where one can use rest parameters:
function (...args) {
}
But an interesting reason to break the DRY principle nonetheless :)
If breaking DRY can simplify things then there is almost certainly a problem somewhere.
It might be in your application, design decisions, the framework being used or the language itself. It may not be practically feasible to solve it given time constraints/business requirements etc. but there is almost always a 'technically' better option.
A simple example is java annotations - They are not easily composable/abstractable. It is a limitation with java - but can be partly mitigated (as the author points out) through better design practices. It is also possible to switch to a sufficiently dynamic language (eg. ruby) or a language having sufficiently advanced hygenic macro system (eg. Rust), where you have this level of flexibility - but it might not be a good idea because of existing investment in the technology.
Another example is CSS rules - You can modularize at selector level but from a maintenance perspective it would be a lot easier to keep code DRY (through multiple design iterations) if you could modularize at a more abstract level (functions, mixins etc.) and later associate them with selectors. Here CSS pre-processors can help and you can additional dependencies like Sass/postcss on top of your existing stack to enforce DRY.
The ideal approach would be to start with very small functions (1-2 lines) that do very specific things (SRP), and then compose them to get other functions/services that have higher level responsibilities. In this case even if you eventually realize that your higher level functions are not suited for some edge case - you can "peel off" layers of abstraction downwards till you get a function that is sufficiently low-level.
A hypothetical example:
queryDB(sqlTemplate, params) ^ | queryDB(sqlTemplateFileName, params) ^ | queryDB(repository, params) ^ | queryProducts(params)You do end up with a lot of functions, but there is nothing wrong (or less DRY) with it.