Code smellDispensables
Duplicate Code
The same logic appears in two or more places, so every fix and change must be made several times and the copies drift apart.
- Also known as
- Clones, DRY violation
Signs
- Two functions are identical except for a name, a constant or a field.
- A bug fix in one place needs to be repeated in another, and sometimes was not.
- Sibling subclasses contain the same method body.
- Two methods compute the same result by different algorithms.
- Both branches of an
ifend with the same lines. - Two components render near-identical markup, and only the shared data was deduplicated.
Why it happens
Copying working code is the fastest way to ship a similar feature. Different people solve the same problem without knowing the other solution exists. Deadlines discourage the extra hour it takes to find a shared shape. And sometimes code looks the same by coincidence today and is supposed to diverge tomorrow; that is not duplication.
Example
Two report exporters in an analytics service share the same date filtering and totaling logic.
interface Sale { at: Date; region: string; cents: number }
function exportCsv(sales: Sale[], from: Date, to: Date): string {
const inRange = sales.filter((s) => s.at >= from && s.at < to);
const total = inRange.reduce((sum, s) => sum + s.cents, 0);
const rows = inRange.map((s) => `${s.at.toISOString()},${s.region},${s.cents}`);
return ["at,region,cents", ...rows, `total,,${total}`].join("\n");
}
function exportJson(sales: Sale[], from: Date, to: Date): string {
const inRange = sales.filter((s) => s.at >= from && s.at <= to); // drifted: <= vs <
const total = inRange.reduce((sum, s) => sum + s.cents, 0);
return JSON.stringify({ rows: inRange, total });
}The copies already disagree on the end boundary. After Extract Method:
interface Sale { at: Date; region: string; cents: number }
function salesInRange(sales: Sale[], from: Date, to: Date) {
const rows = sales.filter((s) => s.at >= from && s.at < to); // half-open, one rule
const total = rows.reduce((sum, s) => sum + s.cents, 0);
return { rows, total };
}
function exportCsv(sales: Sale[], from: Date, to: Date): string {
const { rows, total } = salesInRange(sales, from, to);
const lines = rows.map((s) => `${s.at.toISOString()},${s.region},${s.cents}`);
return ["at,region,cents", ...lines, `total,,${total}`].join("\n");
}
function exportJson(sales: Sale[], from: Date, to: Date): string {
return JSON.stringify(salesInRange(sales, from, to));
}Decide which boundary is correct before merging. Merging silently changes behavior for one caller.
Treatment
- If the copies are in the same class, use Extract Method and call it from both places.
- If the copies are in sibling subclasses, extract, then use Pull Up Method.
- If the siblings share the same steps but differ in details, use Form Template Method.
- If two methods do the same thing with different algorithms, pick the clearer one with Substitute Algorithm.
- If the copies live in unrelated classes, use Extract Superclass when they share an "is a" relationship, or Extract Class when they do not.
- If several conditions lead to the same action, use Consolidate Conditional Expression. If every branch ends with the same code, use Consolidate Duplicate Conditional Fragments.
- If the copies are UI markup, extract one shared component. Sharing the data (an options list, a constant) is not enough: the copy-pasted tags and handlers stay until one component owns them.
Payoff
- One place to fix bugs and change rules.
- Less code to read, test and maintain.
- Hidden drift between copies surfaces and gets decided once.
When to ignore
- The code looks alike but represents different concepts that will change for different reasons. Merging couples them; wait for a third copy to confirm the pattern.
- The copies are in separate services or packages, and sharing would add a dependency that costs more than the duplication.
- Test code, where explicit repetition often reads better than a clever shared helper.
- Two or three trivial lines, where the helper would be harder to read than the copy.
Agent checklist
- You compared every copy line by line and listed the differences before merging.
- Each difference was either confirmed as a bug and fixed, or kept as a parameter.
- The copies change for the same reason; otherwise you left them separate.
- You checked for markup duplication, not only logic duplication.
- You did not add a cross-package dependency to remove a few lines.
- Tests for every former caller pass, including a case on each boundary that differed.
Treatments
- Extract MethodMove a coherent fragment of a long function into its own well-named function and call it from the original place.
- Pull Up MethodMove a method that is identical across subclasses into their superclass so the logic lives and changes in one place.
- Form Template MethodWhen subclass methods follow the same sequence of steps with different details, pull the sequence into the superclass and leave only the steps in subclasses.
- Substitute AlgorithmReplace the body of a function with a clearer or simpler algorithm that produces the same results.
- Extract SuperclassCreate a shared superclass for two classes with overlapping fields and methods, and pull the common parts up into it.
- Extract ClassSplit a class that does two jobs by moving one job's fields and methods into a new class it holds a reference to.
- Consolidate Conditional ExpressionMerge a run of separate checks that all lead to the same result into one condition with a name that states the shared rule.
- Consolidate Duplicate Conditional FragmentsMove code that runs identically in every branch of a conditional to before or after the conditional so each branch holds only what differs.
See also
- Shotgun SurgeryOne logical change forces small edits in many classes, so one missed edit ships an inconsistent system.
- Long MethodA function that does several jobs in a row and has grown too long to read, name or test as one unit.
- Alternative Classes with Different InterfacesTwo classes do the same job but expose different method names or signatures, so callers cannot swap one for the other.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.