Stephan Schmidt
Sometimes DRY is not the right thing
Beware of Don't Repeat Yourself as a mantra
If one battle cry unites all developers, from Java to TypeScript, from Haskell to Elm, it’s “Don’t repeat yourself! (DRY)”. Everyone is aligned on that one.
“Every piece of knowledge must have a single, unambiguous, authoritative representation within a system” The Pragmatic Programmer
Blindly following this rule leads to disaster.
Lets go into some detail using abstractions as an example for DRY. Take a very simplified example from Inkmi - Dream Jobs for CTOs. In it’s core, Inkmi is a marketplace with employers and #CTOs. CTOs define their dream jobs and a company describes their job offer. CTOs define their salary, and if they want remote work and a four hour work week, the company describes their company policy. When there is a match, the CTO got their dream job (simplified ;-)
How does this work on a code level?
You need to define the company policy twice. On one side, you define the company that you seek, and the company that offers jobs on the other side.
type JobOffer struct{
RemoteFirst bool
}
type DreamJob struct {
RemoteFirst bool
}
Then you can match a JobOffer
to a DreamJob
.
But now we have the same company policy in two types. What if we want to rename it? Replace the bool
with an int
for “Remote First”, “Hybrid”, “No Remote”? You need to change two places. You need to remember all the places! Don’t repeat yourself!
You might abstract the code into a Company
struct that is used by both sides of the marketplace.
// This struct we want to reuse.
type Company struct {
RemoteFirst bool
}
type JobOffer struct{
Company Company
}
type DreamJob struct {
Company Company
}
This way, we can add to Company
without needing to change JobOffer
and DreamJob
. Code that depends on Company
- like CheckCompanyPolicy
- can now be reused.
You want to add the size of the company in people, because a dream job might depend on the size of the company (a CTO only wants to work for small or large companies):
type Company struct {
RemoteFirst bool
People int
}
But there might be a range where the CTO is comfortable with, so you write
type Company struct {
RemoteFirst bool
MinPeople int
MaxPeople int
}
Voila!
As you have abstracted the part of the concept dream-job and job-offer into Company
, the company offering a job now also has MinPeople
and MaxPeople
. Which doesn’t make sense. So for companies you decide that MinPeople == MaxPeople
. Problem solved. But to keep your abstraction working, you added technical debt. You might try to solve it with
type Company struct {
RemoteFirst bool
People people
}
// Not Go code, you'd use an interface
type CompanyPeople extends People {
Count int
}
type JobOfferPeople extends People {
Min int
Max int
}
Which creates a hierarchy of entanglement. JobOffer
in package job
, something specific, depends on Company
in package general
, something abstract, which depends on CompanyPeople
in package company
, something specific again. Which makes understanding your type hierarchy difficult and entangled with many parts of your application.
Just because you wanted to save something that you abstracted, that wasn’t the same thing in the beginning.
Words fool us, the company in JobOffer
is something different as a concept than company in DreamJob
. You have been fooled by the name and the moment in time, where coincidently, these two concepts were close enough that you could abstract them. Then time flowed on, and the concepts diverged again, pulling Company
in two different directions.
On top of this if we later want to split development into two teams, DreamTeam
and OfferTeam
, they have a dependency on the same code in Company
—who owns Company
? Co-owned? Who has the final say in changes, what if it is changed in incompatible ways? What if it is changed by two teams at the same time? What if we want to create two microservices? Abstractions create entanglement and dependencies.
Programming is breaking down a problem into smaller problems and those into even smaller problems, until you can write a solution as code—perhaps a method or a class (JobOffer
and DreamJob
from our example). After you’ve done this several times, you find the common thing between all the problems and create an abstraction, a class to be reused, an interface, a framework.
Given a set of code, there are many possible abstractions. Why do we DRY with abstract code in the first place? One reason is to make changes easier. You only need to change one part, and all those use cases the abstraction covers, are changed.
Abstractions assume things, most often the future. No one can predict the future, or we would all play the lottery and the lottery would go broke because everybody only wins. I don’t play the lottery, I can’t predict the future.
When you make abstractions, you need to be right about the future (future changes). There is one way to look into the future: Look at the vision and strategy of your company. If the company has a vision for the next 5 to 10 years, and a strategy to reach that vision, you kind of can look into the future of your code (there might be changes to the vision, or strategy though, argh).
This brings us back to DRY. There are things for DRY, like count orders
var count int
for _, o := range orders {
if order.amount > 0 {
count++
}
}
You would have that same code in several places but deduplicate to a function
func countNonZeroOrders(orders []Order) int {
var count int
for _, o := range orders {
if order.amount > 0 {
count++
}
}
return count
}
Then, as showed above, there are places where DRY leads to disaster. DRY is a tricky thing. You need to do it, to keep your code base manageable. But making the wrong things DRY, you create coupling and make your code more unmanageable.
Takeaway: If things do change at the same time, in the same direction and are the same thing, DRY. Replacing duplicate code then makes sense. But just because code is the same in two places doesn’t mean it is the same code that can be deduplicated or abstracted. Don’t overdue DRY.
My Upcoming Book
Technical Debt
The essential guide
Everyone has technical debt. Eveyone wants to get out of technical debt.