The Shape of the System

The Wrong Abstraction

Two functions can match to the last character and still be two different things.

Here are two functions. They match each other to the last character - same arguments, same body, the same thing returned. A linter calls it duplication. A reviewer asks, fairly enough, why you haven't merged them into one. You won't, and you're right not to. One of them works out the tax a shopper pays at the till. The other one works out the tax the business owes the revenue office. Right now the sum is the same number, down to the penny. But they take their orders from two separate rulebooks, written by two different people who have never met, and on the morning one of those rates moves and the other one stays put, whoever fused the two functions is going to be standing inside a building that has no internal walls, trying to shift one room without dragging the next one along with it. The duplication was never the problem. The likeness was a coincidence. Someone is about to take that coincidence for a law.

Text being the same is not the same as the reason being the same. Two bits of code look identical when two things happen to agree today. They're only the same code if they have to agree forever, by the same rule, for the same cause. Telling those two cases apart is the whole skill, and there's one question that does it. If I change this for some reason, does the other one have to change too, for that same reason? If the answer is no, then they were never one thing, however alike they read on the page. This is worth saying flat out, because the trade has quietly lost track of what the rule was even for. "Don't repeat yourself", back when Andy Hunt and Dave Thomas brought it in in 1999, was a rule about knowledge - every fact should have one home that owns it. And then at some point we traded a rule about meaning for a rule about typing, and started chasing matching characters as though repeating yourself were the sin itself. Two identical functions that stand for two different decisions don't break the rule Hunt and Thomas wrote. Merging them does.

What springs the trap is a good deed, and that's the dangerous bit. You spot two blocks that are nearly the same. You pull out the shared part, you give it a name, you point both callers at it, the diff gets smaller, and you get that small clean feeling of having tidied something up. People don't do this out of laziness. They do it because they're conscientious, and that's the whole reason telling people to be more careful gets you nowhere - it's the careful ones who reach for it in the first place. The payoff comes straight away and you can see it. The bill turns up much later, and it goes to somebody else.

Then time does the thing time does. A new requirement shows up that's almost what the shared thing already does. Almost. So you add a parameter and a small branch - if it's this caller, do it a bit differently. Next quarter there's another near-fit, so another flag. The function that got pulled out to remove duplication ends up with a control panel of switches that nobody can read, and now every caller drags the others' special cases through its own path. The thing you built to kill off conditionals has turned into a machine for making them. And you end up where everyone has been before: a shared function so threaded with flags that touching it to help one caller breaks two others you've never even heard of, so nobody will go near it, and there it sits, set hard, a monument to one tidy afternoon years back.

You can watch this happen to something as small as a function that formats a person's name. It got shared the day someone noticed the shipping label and the welcome email built the name the same way. For about a year, they do. Then the email wants a warm first name on its own, and the label needs the full legal name the courier will actually accept, and the shared function grows a flag for friendliness and a flag for legality and a comment that says, with no irony left in it at all, do not touch. Two callers that were never the same thing, welded together on the strength of one year's coincidence, and now neither of them can move without the other one signing off.

Here's the cruel part. The more tangled and central that abstraction gets, the more it feels too important to take apart. We read all the effort already poured into it as proof it was worth doing, when really it's the other way round. We keep feeding it parameters instead of owning up to the fact that the seam got cut in the wrong place to begin with. We defend the thing precisely because pulling it apart looks expensive, and the expense is the whole reason to pull it apart.

Lay the trade out as an actual sum and not a slogan. A duplicated block costs you one thing: the day you have to change it, and you've got to change it in two places, and you might miss one. The wrong abstraction isn't a one-off cost like that. It taxes every future change to either caller, and the tax keeps compounding. Two copies that can drift apart cleanly come out cheaper than one copy you can never split at all. Sandi Metz put it best - duplication is far cheaper than the wrong abstraction. The word doing the work in that sentence is "far". There's an old guard against all this too, the Rule of Three. The second time you see something repeated, that's permission to wait, not an order to merge. You unify on the third, because two points will draw whatever line you fancy, and you need a third one before the real shape turns up.

For new code the rule is almost rude in how simple it is. Duplicate it on purpose, and leave it that way, until the second and third real uses have shown you what actually varies and what's genuinely fixed. Let the abstraction get discovered instead of designed. And for the code you're already stuck inside, the way out runs backwards. Inline the shared thing back into each of its callers, delete the branches that each one never used, and only then go looking for the smaller, truer shape that's been hiding under all the flags. It feels like walking the wrong way. Nothing else has ever reliably worked. The end state of never doing any of this, of letting one abstraction keep generalising forever, has a name in the folklore: you slowly build, badly, a worse version of the programming language you were already writing in. The skill was never spotting duplication. Anyone can spot duplication. The skill is refusing to act on it until you can actually see why the two things are alike.


In the manifesto, this is tenets (I), (XIV) and (XXI).

Sources

One of a series of field notes on building software for the way minds actually work: tired, distractible, ordinary, and now partly machine. They all lead back to the manifesto behind them, The Shape of the System.