The worst and hardest to maintain code that I've seen or written has been in pursuit of DRY βΒ Don't Repeat Yourself. It's one of the first design principles engineers learn and we love to go wild with it.
Why DRY
DRY is a good muscle to develop when you're learning the basics. You should always turn code like this:
console.log(1)
console.log(2)
console.log(3)
console.log(4)
// ...
Into DRY'd up code that uses a loop:
for (let i = 1; i < 5; i++) {
console.log(i)
}
Yes that's a silly example from beginner programming classes and you wouldn't write code like this at work. But you might write something similar when dealing with a few more layers of indirection.
Say Joe builds a navigation menu the laziest way possible:
const NavigationMenu = () => {
return (
<ul>
<li>
<a href="/about">
<img src="question-icon.png" />
About
</a>
</li>
<li>
<a href="/contact">
<img src="person-icon.png" />
Contact
</a>
</li>
<li>
<a href="/buy">
<img src="cash-icon.png" />
Buy
</a>
</li>
// ...
</ul>
)
}
Every new item is a copypasta of previous items with a small change to the label, url, and icon. Super repetitive.
Jane looks at the pull request and says this code is
- tedious to maintain
- difficult to read
- easy to make mistakes when rushing through an update
We all know you're going to rush when updating this menu because it feels like a mindless task you could do in your sleep.
That's when mistakes happen π
How DRY goes wrong
Jane suggests DRYing up this code with a loop. An obvious choice when you have a repeating pattern with small variations.
const NavigationMenu = () => {
const items = [
{
url: "/about",
icon: "question-icon.png",
label: "About",
},
{
url: "/contact",
icon: "person-icon.png",
label: "Contact",
},
{
url: "/buy",
icon: "cash-icon.png",
label: "Buy",
},
// ...
]
return (
<ul>
{items.map((item) => (
<li>
<a href={item.url}>
<img src={item.icon} />
{label}
</a>
</li>
))}
</ul>
)
}
The team went from 10 lines of code to 28. But it's less repetitive and error prone! A single line of code defines the markup for every element, which means you only need to make changes once.
Jane isn't happy about that config object but the team can live with that. Pull request approved β
More DRY with a factory
Before hitting the merge button Alice gets an idea βΒ that config object looks annoying as heck. Jane and Joe didn't remove the repetitiveness, they just smeared it around.
Next person to add a link will copypasta an object then change string values. That's not great.
She decides to pull out ye olde hammer factory factory and write a function that spits out each config element. Later this could be expanded into a smarter factory with more logic.
function makeNavItem(url, icon, label) {
return { url, icon, label }
}
const NavigationMenu = () => {
const items = [
makeNavItem("/about", "question-icon.png", "About"),
makeNavItem("/contact", "person-icon.png", "Contact"),
makeNavItem("/buy", "cash-icon.png", "Buy"),
// ...
]
return (
<ul>
{items.map((item) => (
<li>
<a href={item.url}>
<img src={item.icon} />
{label}
</a>
</li>
))}
</ul>
)
}
The team is back to 20 lines of code thanks to JavaScript's convenient object creation syntax. That's double the original 10 lines, but very DRY.
- a factory returns each config object
- you collect those in a list
- loop through data to render items
Adding and removing items is now easy. You could even make it dynamic and pull the list from a content management system.
But unless Jane, Joe, and Alice use this pattern everywhere, reading the code got harder. You have to jump around and maintain mental state to understand how it works. As opposed to linearly reading top-to-bottom.
Confusing code paths aren't even the worst part. This is the wrong abstraction.
Why this is a bad abstraction β separation of concerns
A few months pass and the marketing team wants to run an experiment: Will the buy button get more clicks with a red border?
Joe looks at the code and his heart sinks. The abstraction is optimized for keeping all buttons the same. They're all coupled together and there are no affordances for buttons to evolve in diverging directions.
Now Joe has a choice to make:
- throw out the DRY and rewrite back to simple code
- add more parameters to configure buttons
- rewrite the abstraction
Notice two of those follow the "You can't fix the wrong abstraction" idea. One makes it worse and goes down the path of factories with a bunch of boolean arguments to carefully tune behavior with each use.
That's a common thing that happens with factories. They become so complex you might as well write the underlying code directly.
The team's mistake was that they didn't wait long enough to observe how this code evolves. At the time it looked like all navigation buttons need to look the same. But one button was not like the others, it had different semantics.
About
and Contact
are navigation items. Buy
is an action that starts a user flow. This is a subtle but important difference because it indicates that the Buy button is likely to diverge in behavior.
These subtle distinctions are almost impossible to notice ahead of time. But they're always obvious in retrospect.
Creating a better abstraction
Joe takes this opportunity to do some codebase gardening. The previous abstraction was premature, but the styling change that marketing asked for gives him insights into how to separate concerns.
You have 2 concerns:
- The menu
- The button
Those can become React components.
const MenuItem = ({ href, style, icon, children }) => (
<li style={style}><a href={href}><img src={icon} />{children}</a>
)
const NavigationMenu = () => {
return (
<ul>
<MenuItem href="/about" icon="question-icon.png">About</MenuItem>
<MenuItem href="/contact" icon="person-icon.png">Contact</MenuItem>
<MenuItem href="/buy" icon="cash-icon.png" style={{ border: '1px solid red' }}>Buy</MenuItem>
</ul>
)
}
This abstraction makes it easy to make exceptions. You don't need to fiddle with a loop that behaves differently in one of the iterations.
And the composition pattern (using children
) makes it easy to render rich labels. You can add any additional markup to the Buy
button, if you want.
Popular design libraries lean into this further by taking React components for the icon
prop. That way you have more control over rendering the icons.
The separation of concerns becomes:
- NavigationMenu for the structure of the menu
- MenuItem for the structure of each item
- each rendered entry for the values of an item
That's the pattern design libraries landed on after observing how thousands of codebases evolved over years of development. Observing desire paths works :)
Cheers,
~Swizec
Continue reading about DRY β the common source of bad abstractions
Semantically similar articles hand-picked by GPT-4
- DRY is a footgun, remember to YAGNI
- How to think of your business logic as data
- DRY vs SoC, a difficult choice
- Variants β a quick tip for better React components
- Why dataviz does better for your career than TODOapps
Learned something new?
Read more Software Engineering Lessons from Production
I write articles with real insight into the career and skills of a modern software engineer. "Raw and honest from the heart!" as one reader described them. Fueled by lessons learned over 20 years of building production code for side-projects, small businesses, and hyper growth startups. Both successful and not.
Subscribe below π
Software Engineering Lessons from Production
Join Swizec's Newsletter and get insightful emails π on mindsets, tactics, and technical skills for your career. Real lessons from building production software. No bullshit.
"Man, love your simple writing! Yours is the only newsletter I open and only blog that I give a fuck to read & scroll till the end. And wow always take away lessons with me. Inspiring! And very relatable. π"
Have a burning question that you think I can answer? Hit me up on twitter and I'll do my best.
Who am I and who do I help? I'm Swizec Teller and I turn coders into engineers with "Raw and honest from the heart!" writing. No bullshit. Real insights into the career and skills of a modern software engineer.
Want to become a true senior engineer? Take ownership, have autonomy, and be a force multiplier on your team. The Senior Engineer Mindset ebook can help π swizec.com/senior-mindset. These are the shifts in mindset that unlocked my career.
Curious about Serverless and the modern backend? Check out Serverless Handbook, for frontend engineers π ServerlessHandbook.dev
Want to Stop copy pasting D3 examples and create data visualizations of your own? Learn how to build scalable dataviz React components your whole team can understand with React for Data Visualization
Want to get my best emails on JavaScript, React, Serverless, Fullstack Web, or Indie Hacking? Check out swizec.com/collections
Did someone amazing share this letter with you? Wonderful! You can sign up for my weekly letters for software engineers on their path to greatness, here: swizec.com/blog
Want to brush up on your modern JavaScript syntax? Check out my interactive cheatsheet: es6cheatsheet.com
By the way, just in case no one has told you it yet today: I love and appreciate you for who you are β€οΈ