As a developer we very often hear a lot of acronyms for good practices that often seem like good ideas.
DRY means “Don’t repeat yourself” so every piece of your code must be in only one place. It is a prohibition to duplicate
At the opposite, the acronym WET “Write Every Time” or “Write Everything Twice” considers that we can duplicate code without problems.
To these 2 contradictory injunctions, let’s try to see more clearly: DRY seems to be a good enough obvious practice to be respected everywhere. Is it always that simple ?
The objective of this article is to share my opinion on the subject to give a maybe less clear cut :)
How to be DRY?
The most obvious way is to use functions, features available in all languages.
With OOP languages we have access to more advanced paradigms like inheritance and polymorphism.
We can also use the right design pattern corresponding to our need.
We can also abstract some business or technical logic and package it to share the code with others projects.
It is very often a good idea not to duplicate, for several reasons.
It is much longer and harder to understand a large code base if entire pieces exist in several places, in different forms, sometimes with only little variation.
Over time the duplicated code will diverge more and more, raising the costing of future evolutions.
Moreover the codebase grows unnecessarily, especially if one takes into account the additional unit tests and the possible integration tests.
The code grouped in one place can be wrapped in functions and abstractions which allows to reuse a maximum of functionality for future needs.
When a business logic is duplicated and it must accommodate changes, it’s a safe bet that most of the time it will have to follow the same changes in the different portions of the code. This therefore creates a high risk of bugs proportional to the number of duplications.
The broken window theory should not be underestimated. Applied to programming, a codebase deteriorates much faster if it is not maintained. This means that past bad decisions, bugs, some duplications, or ugly code should be fixed when discovered.
Ok we said DRY coding is great, so why write this article?
Because I think that trying to be DRY at all costs can lead to certain disappointments.
Let me tell you a story:
“It is a classic day in our startup, the business owner of our product comes to us with a need. He explains the what, the why, then it is up to us, the development team, to define the how and how much.
The subject is complex, we discuss between developers and we end up with a solution accepted by all.
The development starts and goes well, it is necessary to regroup some code, to refactor in order to reuse as much as possible the existing one, extract functions, classes, finally everything works perfectly.
Two weeks pass and a new need arises: it is necessary to add functionalities to this module.
We analyze the need, and it appears that it does not fit at all in the box. This request calls into question our previous choices, we must therefore start a refactoring which had already been carried out a few weeks previously in order to connect this functionality to the existing one.”
Have you ever experienced this situation? probably yes.
“We created the wrong abstraction”
The situation could have been worse where in the urgency of evolution, we would have had to twist the code and complicate our abstractions to the detriment of quality.
It is usual to respond to a need with an evolution that abstracts part of the code to add the new functionality. However, sometimes the new functionality seems close to what existed in the codebase, but it was just an impression. The business need is completely different, and the future evolutions of this functionality will call into question the choices of reuse.
Sometimes when adding a need to the codebase, part of the existing one can appear very close. Sometimes the code has a similar but very different business purpose
Resist the urge to go into the development and perhaps premature refactoring :
- Challenge the need
- Suggest solutions and ask for pain points if it’s does not fit 100% to the primary need
- Pay attention to the expression of solution rather than an expression of need
- Ask as many questions as necessary, ask for details
- Ask how the functionality will evolve, what is planned for the future of this functionality
- Ask for the vision to your business owner and / or those in the know on the subject
Sometimes it is difficult to get precise answers, don’t hesitate to use the Five Why.
Give you all the cards in your hand so you don’t get one bullet in the foot :
“Create abstractions create future limitations”
This does not mean that abstractions are to be avoided, but only : beware to silly refactoring and thickheaded attempts to make generic code.
When development begins
If it is not obvious to add the business requirement in the existing code base, if there is a lot of existing code to adapt: it will probably be necessary to put the pencils down and design a solution with other team members.
First we try to detect if there is a completely wrong abstraction in our code ; or maybe an abstraction to adapt. If there are a lot of parameters on a component, it’s usually a bad thing. It is important to analyze the code and try to determine if a refactoring is necessary.
At this step, be careful of premature optimization. Do we have enough perspective to design something in an higher level?
“A good design is the one with the least comitment” Robert C. Martin
I observed that it often takes at least 3 different contexts for a feature to have the necessary perspective allowing to get to the right level.
Now is the time to make the right choice. Depending on the analysis, not being 100% DRY is perfectly acceptable.
This is called the chosen technical debt and it is perfectly normal on some projects.
The most important thing is to document the technical debt in an ADR, take it into account for the next development on this part of the code base and do not forget to buy it back.
On our project we have many different business contexts to manage by processing sometimes similar, sometimes different data.
These objects are DTOs and we have chosen to isolate these contexts with distinct classes, therefore by duplicating certain fields. Reuse is partial on these classes. This design is chosen, is not a pain point on a daily basis has often allowed us to adapt specificities very easily.
Duplication is part on our design and is not technical debt.
Wait and learn
A few years ago our company started business in other countries. The market is different, the business rules are differents, we clearly needed to learn how our business operates in these countries. The choice made at the time was to duplicate certain parts of the codebase in order to adapt them, to go quickly and to be able to easily adapt the rules specific to each country.
In this case, I think it was a good idea. On the other hand, once we have learned enough, we should not delay in merging these codebases. The difficulty lies in finding the right moment and the right abstraction, at the right level.
If we wait too long, too much time and energy is spent maintaining and upgrading these codebases. If we don’t wait enough, we may not have learned enough and therefore create a bad abstraction.
This is typical technical debt and must be redeemed at the right time.
I often created frontend components (like button, panel or table) and wanted of course to reuse them as much as possible. Unfortunately as the project grows, the specifics of displays between the pages become more and more numerous and when a component reaches ten parameters, we can say to ourselves that it is time to split it. I think in this case it can be acceptable to duplicate in order to simplify the understanding and evolution of the code.
It’s a middle ground, a chosen technical debt part of our conception. We know it’s not perfect but we live with it without pain.
DRY: not duplicating the code is often a good idea but you should always be wary of magic words.
I think It is a mistake to worship some concept and use it as an absolute truth.
I prefer search the happy medium and always challenge the “fit them all” solution.
Abstractions and generic code are useful but beware of future limitations.
Remember: “the best is the enemy of good” and do not be blinded by ready-made sentences ;-)
Thanks for reading