DRY is Not Maintainable

The mantra of Don’t Repeat Yourself (DRY) is often invoked in the quest of making code more maintable; maintainability is the reason for DRYness and DRYness is a crucial cudgel against maintenance costs, but DRY code does not imply maintainable code.

Much of this circles around notions such as the Rule of Three and the fundamental challenges in producing reusable code. When done properly and using the appropirate abstractions it is enormously powerful, but if done improperly it can quickly yield code that is less maintainable and higher risk.

Perhaps one of the most glaring anti-patterns attached with DRY is when similar code is DRYed. The centralized logic ends up doing multiple things in a package that is more complex and less understandable than treating them as separated simpler problems. This is particularly dangerous in that not only does the increased complexity make the resulting compontent more difficult to maintain, but is use across a wider variety of call sites increases the blast radius if a bug is introduced. This is particularly pernicious in that the issues introduced may not even be relevant for the call sites that are impacted; this lack of independence feels like a fundamental problem introduced in the the arena where improvement was likely pursued. This very often occurs as a result of premature extraction as the variations are not initially known which results in the accretion of logic that is centralized but increasingly unshared.

Even in cases where appropriately generalized logic has been extracted, maintainability may still suffer. The code itself may be more difficult to understand, and while the blast radius for changes may be appropirately bound the cost of introducing such changes may remain higher than what can be justified. Much of this amounts to cost increases similar to those suggested by Fred Brooks where there is roughly a threefold increase of costs in writing code and publishing a library. Such costs may not be worthwhile, and if they are not paid then the usability may suffer. This particular argument may apply more to how code is shared rather than if it is shared. This also aligns fairly neatly with the general advice to keep things scoped as closely as possible: scoping a reusable component to some bounds provides a well defined context in which it can be understood and changed.

A final case that is worth calling out is that the advice is “Don’t Repeat Yourself” not “Don’t Say What You’re Doing”. In some cases DRY is implemented through providing some form of assumed context in which code will operate. The code itself may therefore benefit from some implicitly provided magic which is likely to be a time bomb of confusion waiting for future developers. It only takes a single line to replace assumed knowledge that could waste someone’s time to a readily discoverable reference, trading potential surprises for self-expression.

Similar to abstraction is not decoupling much of this amounts to the notion that design requires thought and attention rather than blindly adopted mechanical recipes. In this particular case this may also be a manifestation of a solution looking for a problem. I’ve seen “DRY” code that has had no maintenance costs and I’ve similarly worked with “DRY” code in which maintenance involves changing all the components components. Neither of these derive practical benefit from the practice and both speak to the fundamental uncertainty that suggests that premature and immature shots are very unlikely to land where the target turns out to be.