skip to content

Three near-identical item stores were copy-pasted, one per item kind — what does collapsing them onto one type parameter actually change?

level: middleimportance: should knowfreq 52%

answer

  1. the same body, one word different
  2. three edits for one change
  3. the copy someone forgets
  4. one implementation, still checked per use
  5. where the per-kind branch goes

basics

~20 s

One body replaces three, so a fix lands once and reaches every item kind, while each use site is still checked against the kind it chose. The cost is that per-kind special cases lose their home inside the store.

solid answer

~50 s

The three copies are the same algorithm with one word different, so every change is a three-way edit and every bug is three bugs — and the copy someone forgets keeps the defect quietly. Replacing the item type with a placeholder `T` leaves one body: one tag index, one expiry sweep, one set of tests. What does *not* change is the checking — each use site supplies its own type argument, so an umbrella desk still refuses a coat. That is the whole difference from the other way of getting to one body, widening the field to a universal supertype, which buys the single body by giving up the check. The price of the placeholder is that a branch which only ever made sense for one kind can no longer live inside the shared body; it moves out to the caller, to a construction parameter, or onto the item itself.

code

pseudocode · 13 lines
pseudocode
// three copies, one per kind: same sweep, same index, same bug
class UmbrellaStore { deposit(tag, item: Umbrella) ... ; sweep() ... }
class LaptopStore   { deposit(tag, item: Laptop)   ... ; sweep() ... }
class CoatStore     { deposit(tag, item: Coat)     ... ; sweep() ... }

// one body, the kind left open
class Store<T> {
    deposit(tag, item: T) { entries[tag] = item }
    collect(tag): T       { return entries.remove(tag) }
    sweep()               { for each tag in expired(entries) { discard(tag) } }
}

desk = Store<Umbrella>()   // the kind is chosen here, per use, not per body

go deeper

for a junior

Recall the two things the merge changes: one body to fix instead of three, and no loss of checking because each use site still names its own kind. Be able to say why a missed copy is the real danger.

for a middle

Explain the mechanics of both merges and why they are opposites: widening to a top type collapses the checking along with the bodies, while a placeholder collapses only the bodies. Then name the cost — a per-kind rule loses its home.

for a senior

Show the maintenance angle: which of the three copies is under-tested, what drift between them already exists, and how you would migrate one use site at a time rather than in a single rewrite.

for a principal

The judgment is whether the kind is really the only axis of variation across teams that own these stores. Committing them to a shared body makes every future change a shared change, and that coordination cost is what you are buying.

## What the three copies really are Three lost-property desks were written by copying one: an umbrella desk, a laptop desk and a coat desk. Each has the same tag index, the same expiry sweep that discards anything held too long, the same hand-back, and the same handling of a tag nobody comes for. Diff any two of them and the only differences are the item type in three positions and the names. That shape has a specific, measurable cost, and it is not the disk space: - **Every change is three edits.** A new rule about expiry is written three times and reviewed three times. - **Every bug is three bugs.** Fixing the sweep in two of the three leaves the third quietly wrong, and nothing in the code says the three were supposed to agree. - **Every test is three tests, or worse, one.** Teams usually test the copy they touched most, so two of the three drift out from under their coverage. - **The copies drift apart under maintenance**, and after a year nobody can tell which differences were deliberate. ## What the placeholder removes Write the desk once as `Store<T>`, with `T` in the field, the deposit parameter and the collect return, and the three copies become three *uses*: one desk instantiated for each kind. What is removed is exactly the duplication and nothing else: 1. **One body to fix.** The expiry bug is fixed once, and all three kinds get the fix on the same commit. 2. **One body to test.** The tests are written against the shared implementation, and a use site only needs to prove it wired the right kind in. 3. **One body to optimise or instrument.** Adding metrics or a lock is one edit, not three that must stay in step. ## What the placeholder does not remove This is where the question is usually won or lost. The merge does **not** weaken checking. Each use site still supplies its own type argument, and the compiler checks that site against it: the umbrella desk still refuses a coat, exactly as the copy did. Nor does it let one desk hold several kinds at once — an instance fixes its argument when it is created, and holding a mixture is a different design decision with a different type. Contrast that with the other route to a single body, widening the field to the universal **top type**. That also collapses three bodies into one, but it collapses the checking too: every deposit is accepted and every retrieval is a downcast. The two merges look the same in the file count and are opposite in what they guarantee. | | three copies | one body, top type | one body, placeholder | |---|---|---|---| | fixing a bug | three edits, one may be missed | one edit | one edit | | a coat offered to the umbrella desk | rejected | accepted, fails later | rejected | | adding a fourth kind | a fourth copy | nothing | one use site | | a rule that applies to one kind only | lives naturally in its copy | a branch in the shared body | must move outside the body | ## Where the per-kind rule goes The honest cost of the merge is that last row. Suppose coats are held for ninety days and everything else for thirty. In the copy-paste world that rule sat in the coat desk and bothered nobody. In the shared body it has nowhere to sit, and the tempting move — a branch on the kind inside the shared sweep — puts a copy back in, disguised as a conditional. The three places it can honestly go are: - a **construction parameter**: the retention period is passed in when the desk is created, so the shared body only knows there is one; - a **property of the item**: the item answers how long it should be kept, and the body asks; - a **separate desk**: if the coat desk is genuinely a different algorithm, it should not have been merged, and the merge should be partially undone. ## How you know the merge was right The check is what the body looks like six months later. If it is still one algorithm with a placeholder and no per-kind branches, the kind really was the only thing that varied and the merge paid for itself several times over. If it has grown a switch, a pair of optional callbacks and a flag, the copies were never the same algorithm and they have been re-created inside one file, where they are now harder to read than they were when they were three honest bodies.

  • After the merge, can one desk instance hold an umbrella and a coat at the same time?
    No. Each instance fixes its type argument when it is created, so the umbrella desk holds umbrellas only. Holding a mixture means choosing a common supertype as the argument, and that choice brings the downcast back on retrieval — which is fine if you meant it and a bug if you did not.
  • One of the three copies kept its items for ninety days and the others for thirty. Where does that rule go after the merge?
    Not into a branch on the kind inside the shared body — that is a copy smuggled back in as a conditional. Pass the retention period as a construction parameter, or let the item answer how long it should be held. If the difference runs deeper than one number, that copy should not have been merged.

saying these in an interview costs you the question

  • Duplication only costs disk space, so three copies are fine
  • Merging them means widening the stored field to a universal supertype
  • One shared body means one desk can now hold every kind at once
  • Deduplicating the body removes the per-use kind checking
  • The shared body must be slower than three specialised copies