Abstractions in code are easy to add and challenging to remove. Sometimes we feel stuck with them. But the good news is, you can back out of almost any abstraction with the right mindset.
Let’s look at an example.
A Breaking Abstraction
Imagine a currency formatter.
function formatPrice(amount) {
return `$${amount.toFixed(2)}`
}
formatPrice(10) // "$10.00"
Soon our company decides to launch in Europe. Let’s add that conditional.
function formatPrice(amount, currency) {
if (currency === "EUR") return `€${amount.toFixed(2)}`
return `$${amount.toFixed(2)}`
}
formatPrice(10) // "$10.00"
formatPrice(10, "EUR") // "€10.00" 😊
Hang on; the Czech Republic puts the currency symbol after the monetary amount. I guess we have to add a conditional that supports this:
formatPrice(10, "CZK") // "10,00 Kč"
Dammit, there’s a comma in there! Guess it’s time for more code in that conditional? We aren’t even in Europe yet, and yet we have two conditionals.
This kind of code has a way of spiraling. Pretty soon we’re writing:
formatPrice(amount, currency, locale, roundingMode, context)
Next, we’re launching an accounting business in the US, and need our formatter to handle accounting notation. Balance sheets sometimes show negative values inside parentheses:
formatPrice(-10, "US") // "($10.00)"
I guess we need to add another argument?
formatPrice(amount,
currency,
locale,
roundingMode,
context,
displayStyle // 😬 New sixth argument!
)
Is this a function you want to call or extend? Nope. We don’t have to live like this.
Solution: Duplication
So, what’s the solution? Good-old-fashioned duplication. The kind we were taught to avoid.
Sandi Metz once said: “Prefer duplication to the wrong abstraction.” What we have here is the wrong abstraction. We don’t have to prefer it because it’s in the code.
Let’s pick a version where our function has grown a bit.
function formatPrice(amount, currency) {
if (currency === "EUR") return `€${amount.toFixed(2)}` // Euro
if (currency === "CZE") return `${amount.toFixed(2)} Kč` // Czech koruna
if (currency === "HUN") return `${amount.toFixed(2)} Ft` // Hungarian forint
if (currency === "POL") return `${amount.toFixed(2)} zł` // Polish złoty
if (currency === "SWE") return `${amount.toFixed(2)} kr` // Swedish krona
return `$${amount.toFixed(2)}`
}
We look at this and think: “Adding another argument here doesn’t feel right.” That’s a great instinct worth honoring.
Instead, we duplicate it.
function formatPrice(amount, currency) {
if (currency === "EUR") return `€${amount.toFixed(2)}` // Euro
if (currency === "CZE") return `${amount.toFixed(2)} Kč` // Czech koruna
if (currency === "HUN") return `${amount.toFixed(2)} Ft` // Hungarian forint
if (currency === "POL") return `${amount.toFixed(2)} zł` // Polish złoty
if (currency === "SWE") return `${amount.toFixed(2)} kr` // Swedish krona
return `$${amount.toFixed(2)}`
}
// Duplicated! 🤢
function formatAccountingPrice(amount, currency) {
if (currency === "EUR") return `€${amount.toFixed(2)}` // Euro
if (currency === "CZE") return `${amount.toFixed(2)} Kč` // Czech koruna
if (currency === "HUN") return `${amount.toFixed(2)} Ft` // Hungarian forint
if (currency === "POL") return `${amount.toFixed(2)} zł` // Polish złoty
if (currency === "SWE") return `${amount.toFixed(2)} kr` // Swedish krona
return `$${amount.toFixed(2)}`
}
Then, we make our change. We’re only launching the accounting business in the US, so currency is gone.
function formatAccountingPrice(amount) {
const abs = Math.abs(amount).toFixed(2)
// Handling our new requirement! 😊
if (amount < 0) {
return `($${abs})`
}
return `$${abs}`
}
The original function is unchanged, and we call this new function in an accounting context.
formatPrice(10, "US") // "$10.00"
formatAccountingPrice(-10) // "($10.00)"
formatPrice doesn’t have to do everything. Splitting this up might be the right abstraction for our business.
The Bigger Picture
We aren’t locked into formatPrice and six ordered arguments because all the engineers who ever worked on our project made it so. They didn’t know what we know. It’s rare to get the right abstraction on the first try.
When you encounter a busy function like this, take pride in having the experience to recognize it. Then, hold your nose and duplicate it, and let a new abstraction appear.