Alternative Classes with Different Interfaces: Two Tiffin Services, Two Languages
Learn this code smell with a tiffin delivery story: two classes do the same job with different method names, so you cannot swap them. Fix it step by step.
🍱 Two tiffin services that speak different languages
Sunita Aunty in Mumbai runs a small office of thirty people near Dadar station. Every working day, she orders lunch tiffins for her staff. She uses two tiffin services, because each covers a different part of the city.
The first one is Anna's Tiffins, run by Anna himself, famous for his sambar. To order, you message: "deliverLunch — 12 boxes, veg, by 12:30." Anna confirms with "lunch delivered" when done.
The second is Ghar Ka Khana, run by the Deshmukh family. Same dabbas, same dal-chawal-roti-sabzi, same price. But their ordering style is completely different. You must say: "sendMeal — meal count 12, type V, slot afternoon." They confirm with "meal dispatched."
Same job. Same food. Different language.
Now watch Sunita Aunty's daily struggle. Monday's orders go to Anna, so she writes the message in Anna-speak. Tuesday's go to Ghar Ka Khana, so she rewrites everything in their dialect. She keeps a little translation notebook: lunch = meal, boxes = count, veg = type V, 12:30 = afternoon slot. When Anna's delivery boy is sick, she cannot simply forward Monday's message to Ghar Ka Khana — she must sit and translate the whole order, and one wrong word means thirty hungry employees.
One Thursday it actually happens. Anna's scooter breaks down at 11 a.m. Sunita Aunty grabs her notebook, translates the day's order in a hurry, and writes "sendMeal — meal count 12, type V, slot morning." Morning slot. The tiffins arrive at 9:45 a.m., stone cold by lunch, and her accountant Prakash eats biscuits at his desk while the whole office grumbles. One mistranslated word — 12:30 to morning instead of afternoon — and the system failed. The notebook did not just cost her time. It gave her a new category of bug: the translation error.
And when a third service, Swad Tiffins, opens nearby with better prices? Sunita Aunty groans. Not because of the food — because Swad has invented a third dialect ("bookThali — thali 12, jain no, hour 12"), and her notebook needs a whole new chapter, and every chapter multiplies the chances of another cold-tiffin Thursday.
The absurd part: all three services do the identical job. If only they all accepted one standard message — say, order(boxCount, foodType, deliveryTime) — she could switch between them in one second, route orders to whoever is cheapest each day, and welcome every new service with zero new learning.
That translation notebook is the Alternative Classes with Different Interfaces code smell: two (or more) classes doing essentially the same job but exposing it through different method names and shapes, so callers cannot swap one for the other without rewriting their code. Keep Sunita Aunty, Anna, the Deshmukhs, and that cold Thursday in mind — this whole post is their story.
🔍 What is this smell?
This is the last of the Object-Orientation Abuser smells, and it is the quiet one. The others shout — giant switches, throwing overrides, null-stuffed fields. This one just... fails to exist. The abuse here is the absence of the abstraction OOP wanted you to build.
The definition: two or more classes perform the same conceptual job, but their public interfaces do not match — different method names (deliverLunch vs sendMeal), different parameter orders, different shapes (one method here, two methods there), different return styles (void vs boolean). Because there is no shared contract, callers must know exactly which class they hold and speak its private dialect.
It helps to see this smell next to its sibling, because Fowler's Refactoring presents them as mirror images:
- Refused Bequest: classes share a contract they should not — a false "is-a."
- Alternative Classes with Different Interfaces: classes lack a contract they should share — a missing "is-a."
One hierarchy lies by existing; the other lies by not existing.
How does it happen? Almost never on purpose:
- Independent evolution. Two developers — or two teams, or you-in-January and you-in-June — solve the same problem in different corners of the codebase, each inventing their own names. Neither knows the other exists.
- Third-party libraries. Two payment gateways, two SMS vendors, two storage SDKs — each vendor invents its own API, and the difference leaks deep into your code instead of being stopped at the boundary.
- Copy-and-diverge. Someone copies a class for a new context and "helpfully" renames methods to fit the new domain, breaking the alignment that would have allowed a shared type.
- Nobody owned the abstraction. The common concept — "a thing that delivers tiffins" — was never given a name, so every concrete implementation grew its own surface.
One-line summary: this smell is two classes doing one job in two vocabularies — the missing shared interface forces every caller to become a translator.
The whole smell on one map:
🕵️ How to spot it
The checklist:
- Two classes have parallel responsibilities but mismatched method names:
sendvsdispatch,open/closevsconnect/disconnect. - You cannot swap one class for the other without rewriting call sites — even though either would do the job.
- Caller code branches on which concrete class it holds:
if (useAnna) { anna.deliverLunch(...) } else { ghar.sendMeal(...) }. - Little translation helpers exist whose only purpose is converting one class's vocabulary into the other's.
- The two classes contain near-duplicate logic that cannot be unified because the surfaces differ.
- The same parameters appear in different orders, or one class merges into one method what the other splits into two.
- When a bug is fixed in one class, someone must remember to fix its "twin" — and sometimes forgets.
Symptom table:
| Symptom you see | What it really means |
|---|---|
| Same job, different method names across two classes | One concept, two vocabularies — the abstraction was never named |
if (isVendorA) a.foo() else b.bar() in callers | Lost polymorphism; every caller branches on concrete type |
| Hand-written translator/wrapper glue | Callers are paying a translation tax the classes should have prevented |
| Near-identical logic in both classes | Duplicate Code hiding behind the mismatched surface |
| Bug fixed in one twin, alive in the other | The classes are drifting apart with every release |
| Adding vendor #3 means editing every call site | Open/Closed Principle blocked by the missing interface |
A quick detection trick: take both classes and write their method lists side by side on paper. Draw a line between each pair that means the same thing. If you can connect most of the lines — deliverLunch ↔ sendMeal, cancelOrder ↔ stopMeal — you are looking at one interface wearing two costumes.
There is also a time-audit version of the test. Sunita Aunty once tracked where her lunch-ordering hour actually went. The result explains the smell better than any definition:
Only one slice is the real job. The other three slices — eighty percent of the hour — exist purely because two services never agreed on one vocabulary. When you profile a caller class that juggles two mismatched vendors, you will find the same shape: a thin core of business logic wrapped in a thick crust of translation.
⚠️ Why it is a problem
Cost 1: Lost polymorphism. OOP's superpower is "call order() and let the right class respond." With mismatched interfaces, that power is switched off. Callers must know which concrete class they hold and speak its dialect — coupling every caller to every implementation.
Cost 2: Branching infects the callers. Since no common type exists, callers grow if (which one?) conditionals — and now you also have the Switch Statements smell, scattered through code that should have been one polymorphic call.
Cost 3: Duplication drifts. The twins almost always share logic — retry rules, validation, formatting. With no common home for it, the logic is duplicated, and duplicated logic diverges: a bug fixed in AnnaTiffins lives on in GharKaKhana for months.
Cost 4: Each new alternative makes it worse. A third vendor has no contract to implement, so it invents a third vocabulary; every call site that wants to support it grows a third branch. Cost grows multiplicatively: callers × vendors. With a shared interface, the cost is one new class, flat.
Now watch one single order travel through the system on the cold-tiffin Thursday. The manager must speak a different language depending on who answers the phone — and the translation step is exactly where the bug slips in:
The vendors did nothing wrong. Anna was honest about the scooter; the Deshmukhs delivered exactly what was asked. The bug lives entirely in the translation step — a step that exists only because the interfaces differ. Here is the same Thursday as a mood graph:
And the multiplication problem in plain numbers — how many places must change each time a new vendor joins:
Without a contract, every caller method grows one branch per vendor, so the bars climb with each arrival. With the contract, the answer is always one: write the new class. The bar never grows again.
💻 A real-life code example
Let us code Sunita Aunty's problem. Two tiffin services, written by two different developers who never met:
// Written by developer A, in January
class AnnaTiffins {
deliverLunch(boxes: number, isVeg: boolean, byTime: string): void {
console.log(`Anna: ${boxes} ${isVeg ? "veg" : "non-veg"} boxes by ${byTime}`);
}
cancelLunch(boxes: number): void {
console.log(`Anna: cancelled ${boxes} boxes`);
}
}
// Written by developer B, in June — same job, different language
class GharKaKhana {
// different name, different parameter ORDER, different types!
sendMeal(slot: "morning" | "afternoon", mealType: "V" | "NV", count: number): boolean {
console.log(`GharKaKhana: ${count} type-${mealType} meals, ${slot} slot`);
return true;
}
stopMeal(count: number, refund: boolean): void {
console.log(`GharKaKhana: stopped ${count} meals, refund=${refund}`);
}
}And here is Sunita Aunty — the caller — forced to be a translator with a branching notebook:
// BAD CODE: the caller pays the translation tax, every single day
class OfficeLunchManager {
constructor(
private anna: AnnaTiffins,
private ghar: GharKaKhana,
private useAnna: boolean, // concrete-type switch in disguise
) {}
orderForStaff(staffCount: number, vegCount: number): void {
if (this.useAnna) {
this.anna.deliverLunch(vegCount, true, "12:30");
this.anna.deliverLunch(staffCount - vegCount, false, "12:30");
} else {
// translate: 12:30 -> "afternoon", true -> "V", reorder the params...
this.ghar.sendMeal("afternoon", "V", vegCount);
this.ghar.sendMeal("afternoon", "NV", staffCount - vegCount);
}
}
cancelToday(count: number): void {
if (this.useAnna) this.anna.cancelLunch(count);
else this.ghar.stopMeal(count, true); // and don't forget the refund flag!
}
}Every method of OfficeLunchManager is half real logic, half dictionary. Look at the else branch of orderForStaff — that hand-translation of "12:30" into "afternoon" is exactly the line where the cold-tiffin Thursday lives. Write "morning" there by mistake and no compiler, no test of GharKaKhana, nothing complains. The bug is in the translation, and the translation is in the caller.
And when SwadTiffins launches with bookThali(thaliCount, jain, deliveryHour)? Every method here grows a third branch, the constructor grows a third field, and the useAnna: boolean becomes an enum. The caller rots a little more with each vendor.
🧹 Cleaning it up, step by step
The standard cure has a clear order, straight from Fowler's playbook: make the interfaces identical first, then extract the shared contract, then point callers at it.
Step 1: Rename Method — agree on one vocabulary. Pick the best names (or invent better ones) and rename both classes' methods to match. Modern editors do this safely in seconds:
// Step 1: same names everywhere (signatures not aligned yet)
class AnnaTiffins {
order(boxes: number, isVeg: boolean, byTime: string): void { /* ... */ }
cancel(boxes: number): void { /* ... */ }
}
class GharKaKhana {
order(slot: "morning" | "afternoon", mealType: "V" | "NV", count: number): boolean { /* ... */ }
cancel(count: number, refund: boolean): void { /* ... */ }
}Step 2: Unify the signatures. Align parameter order, types, and return values. Where one class needs an extra detail (Ghar's refund flag), either add it to both with a sensible default or move it into a config the class holds internally:
// Step 2: one shape. Anna's "12:30" and Ghar's "afternoon" both
// become a proper shared type. Each class translates INTERNALLY.
type FoodType = "veg" | "non-veg";
class AnnaTiffins {
order(count: number, food: FoodType, deliveryHour: number): boolean {
const byTime = `${deliveryHour}:30`; // its own dialect, hidden inside
console.log(`Anna: ${count} ${food} boxes by ${byTime}`);
return true;
}
cancel(count: number): boolean { /* ... */ return true; }
}
class GharKaKhana {
order(count: number, food: FoodType, deliveryHour: number): boolean {
const slot = deliveryHour < 11 ? "morning" : "afternoon"; // hidden inside
const mealType = food === "veg" ? "V" : "NV";
console.log(`GharKaKhana: ${count} type-${mealType} meals, ${slot} slot`);
return true;
}
cancel(count: number): boolean { /* refund handled internally */ return true; }
}This is the key insight of the whole refactoring: the translation did not disappear — it moved. It used to live in every caller; now each class translates its own dialect privately, exactly once. The deliveryHour < 11 rule that once lived in Sunita Aunty's error-prone notebook now lives inside GharKaKhana, written once, tested once, impossible for a hurried caller to get wrong.
Step 3: Extract the shared contract. Now that the surfaces are identical, naming the abstraction is trivial:
// Step 3: the concept finally gets a name
interface TiffinService {
order(count: number, food: FoodType, deliveryHour: number): boolean;
cancel(count: number): boolean;
}
class AnnaTiffins implements TiffinService { /* as above */ }
class GharKaKhana implements TiffinService { /* as above */ }Step 4: Point the callers at the contract. Sunita Aunty throws away the notebook:
// Step 4: the caller speaks ONE language to ANY service
class OfficeLunchManager {
constructor(private service: TiffinService) {} // any vendor fits
orderForStaff(staffCount: number, vegCount: number): void {
this.service.order(vegCount, "veg", 12);
this.service.order(staffCount - vegCount, "non-veg", 12);
}
cancelToday(count: number): void {
this.service.cancel(count);
}
}
// Swapping vendors is now ONE line at setup time:
const manager = new OfficeLunchManager(new GharKaKhana());When SwadTiffins launches, it implements TiffinService, and OfficeLunchManager does not change by even one character. Here is the finished shape:
If the services also share real logic (order validation, delivery-time checks), lift it into a common base class with Extract Superclass — and quite often you will discover the two classes were so similar that one of them can simply be deleted.
College corner: this refactoring is interface unification, and it quietly delivers the "D" of SOLID — the Dependency Inversion Principle. Before: OfficeLunchManager (high-level policy) depended directly on AnnaTiffins and GharKaKhana (low-level details). After: both the manager and the vendors depend on the abstraction TiffinService, and the dependency arrows point inward toward the contract — that inversion is what makes vendors pluggable and the manager testable with a fake. A second idea worth knowing: TypeScript and Go use structural typing — any class whose shape matches TiffinService satisfies it automatically — while Java and C# use nominal typing, where a class must explicitly declare implements. Structural typing makes unification cheaper (align the shapes and you are done), but the naming of the contract still matters: an interface called TiffinService documents the concept, anchors test fakes, and gives the next developer the vocabulary that Sunita Aunty's notebook never standardized. Ecosystem-scale proof that this matters: SLF4J in Java and Microsoft.Extensions.Logging in .NET are exactly this refactoring applied to a whole industry's logging dialects.
The repair itself moves through clear stages — and knowing the stages helps you do it safely on a live codebase, one commit per transition:
Note the two endings. Sometimes you stop at "callers on contract" — two genuinely different vendors behind one interface. And sometimes, after unification, you discover the twins were the same class all along, and the happiest refactoring of all happens: git rm one of them.
What if you cannot edit the classes — for example, two third-party SDKs? Then apply the same idea at the boundary: write a thin Adapter class for each SDK that implements your TiffinService interface and translates internally. You cannot rename a vendor's methods, but you can stop their vocabulary at your door. Note this aligns with the Strategy idea too — interchangeable services behind one contract is exactly what the Strategy pattern formalizes.
The same smell in C# 💾
A shorter C# version — two file-storage helpers written by two teams:
// BAD: same job, two dialects
class LocalDiskStore
{
public void SaveFile(string name, byte[] data) { /* write to disk */ }
public byte[] LoadFile(string name) => Array.Empty<byte>();
}
class CloudBucketStore
{
public bool Upload(byte[] content, string key) { return true; } // reversed params!
public byte[] Download(string key) => Array.Empty<byte>();
}
// Every caller branches:
void Backup(string name, byte[] data, bool useCloud,
LocalDiskStore disk, CloudBucketStore cloud)
{
if (useCloud) cloud.Upload(data, name);
else disk.SaveFile(name, data);
}After rename + signature alignment + interface extraction:
// GOOD: one contract, swappable implementations
interface IFileStore
{
void Save(string name, byte[] data);
byte[] Load(string name);
}
class LocalDiskStore : IFileStore
{
public void Save(string name, byte[] data) { /* write to disk */ }
public byte[] Load(string name) => Array.Empty<byte>();
}
class CloudBucketStore : IFileStore
{
public void Save(string name, byte[] data) { /* upload, params translated inside */ }
public byte[] Load(string name) => Array.Empty<byte>();
}
void Backup(string name, byte[] data, IFileStore store) => store.Save(name, data);Backup shrank from a branching translator to a single honest line — and it now supports storage backends that have not been invented yet. Notice the reversed-parameter trap in the bad version: Upload(data, name) vs SaveFile(name, data). Pass the arguments in the wrong order during a hurried vendor switch and, depending on types, it may even compile. That is the C# version of "morning slot instead of afternoon."
🗺️ Where this smell hides in real projects
- Payment gateways. One SDK says
charge(amount, currency), the next sayscreatePayment(currencyCode, value), a third wants a builder object. Codebases that let all three dialects leak inward end up with vendorifs in checkout, refunds, and reporting. Mature teams define onePaymentProviderinterface and adapter-wrap each vendor at the edge. - Notification channels. Email says
send(to, subject, body), SMS saysdispatch(phone, text), push sayspublish(deviceToken, payload). Conceptually all are "notify a person" — and refactoring them under oneNotifieris the textbook exercise for this smell. - Logging libraries. Decades of
log.error(...)vslogger.Error(...)vsconsole.error(...)is why facade projects like SLF4J (Java) andMicrosoft.Extensions.Logging(.NET) exist — they are this refactoring performed at ecosystem scale. - Storage backends. Local disk, S3-style buckets, database blobs — same save/load job, three vocabularies, until someone extracts a
FileStorecontract. - In-house twins. The most common case of all:
CustomerCsvExporter.exportData()andOrderCsvWriter.writeFile()— two teammates, two corners of the repo, one job. Code review and shared naming conventions are the vaccine. - Test fakes vs real services. A hand-written fake whose methods drifted from the real client's methods, so tests exercise a dialect production never speaks.
The general pattern: this smell collects wherever the same capability has multiple providers and nobody stood up early to name the shared contract.
⚖️ When it is okay to ignore
| Situation | Verdict | Why |
|---|---|---|
Similarity is coincidental — both classes have process() but live in unrelated domains | Do not unify | A shared interface here is a false abstraction, worse than the smell |
| Only one caller, no realistic prospect of a second implementation | Leave it | A tiny local if is cheaper than building and maintaining an abstraction that earns nothing |
| The two classes come from third-party libraries | Adapt, don't rename | You cannot edit them; wrap each in an Adapter behind your one interface at the boundary |
| The classes match on 9 of 10 methods already | Finish the job | You are one rename away from a clean contract — do it before they drift again |
| Same job, mismatched APIs, multiple callers branching | Refactor | This is the smell at full strength; every new vendor multiplies the pain |
| Logic inside the twins is near-identical | Refactor + Extract Superclass | You will likely delete one class entirely — the best possible outcome |
Two questions place any pair of lookalike classes on the map: how truly interchangeable are they, and how many callers juggle them?
The tiffin services sit in the unify-now corner: genuinely interchangeable, with every ordering screen and cancellation screen paying the tax daily. The unrelated process() lookalikes stay on the left — forcing them together would create a false abstraction that is harder to undo than the smell itself.
The honest test before unifying: "Would a caller genuinely accept either class for the same purpose?" If yes, they are true alternatives — unify them. If no, they only look similar — leave them apart. Wrong abstractions cost more than missing ones.
🛠️ Which refactorings cure it
| Refactoring | Use it when |
|---|---|
| Rename Method | First step, always: make the two classes describe the same operation with the same word |
| Change Function Declaration (add/reorder parameters) | Align signatures — same parameters, same order, same return shape |
| Move Method | A responsibility sits in the wrong class, preventing the surfaces from lining up |
| Extract Interface | Surfaces match; name the contract and make callers depend on it |
| Extract Superclass | The twins also share logic, not just shape — lift it into a common parent |
| Adapter (pattern) | You cannot edit the classes (third-party); wrap each behind your unified interface |
| Delete one class | The happiest ending: after unification the twins are identical — keep one |
📦 Quick revision box
+----------------------------------------------------------------+
| ALTERNATIVE CLASSES WITH DIFFERENT INTERFACES — CHEAT SHEET |
+----------------------------------------------------------------+
| Story : Two tiffin services, same dabbas, but one speaks |
| "deliverLunch" and the other "sendMeal" — Sunita |
| Aunty keeps a translation notebook. |
| Smell : Two classes, one job, two vocabularies — callers |
| cannot swap them and must branch + translate. |
| Spot it : send vs dispatch, reversed params, if(vendorA) |
| ladders, glue translators, drifting twin logic. |
| Danger : Lost polymorphism, duplicated logic that diverges, |
| every new vendor multiplies caller edits. |
| Cure : Rename Method -> unify signatures -> Extract |
| Interface/Superclass -> callers use the contract. |
| Third-party code? Adapter at the boundary. |
| Mirror : Refused Bequest = false shared contract; |
| this smell = missing shared contract. |
| Mantra : One job deserves one vocabulary. |
+----------------------------------------------------------------+✍️ Practice exercise
A school's website team wrote two PDF-report helpers in different semesters. Unify them:
class MarksheetPrinter {
generatePdf(studentName: string, marks: number[], term: string): Uint8Array {
/* builds a marksheet PDF */ return new Uint8Array();
}
emailToParent(pdf: Uint8Array, parentEmail: string): void { /* ... */ }
}
class CertificateMaker {
// same job family: produce a PDF for a student, send it home
buildDocument(activity: string, name: string): Uint8Array { // params reversed!
/* builds a certificate PDF */ return new Uint8Array();
}
dispatchHome(email: string, doc: Uint8Array): boolean { /* ... */ return true; }
}
// And the suffering caller:
function sendTermDocuments(student: { name: string; parentEmail: string },
marks: number[],
printer: MarksheetPrinter,
maker: CertificateMaker) {
const sheet = printer.generatePdf(student.name, marks, "Term 1");
printer.emailToParent(sheet, student.parentEmail);
const cert = maker.buildDocument("Science Fair", student.name);
maker.dispatchHome(student.parentEmail, cert);
}Your tasks:
- Map the dialects: Write the two method lists side by side and draw the matching lines: which method in
MarksheetPrintercorresponds to which inCertificateMaker? Note every mismatch — name, parameter order, return type. - Rename and align: Choose one vocabulary (suggestion:
build(studentName, details): Uint8ArrayandsendHome(pdf, email): void). Apply Rename Method and reorder parameters in both classes until the surfaces are identical. Decide: where doesdispatchHome'sbooleanreturn go? - Extract the contract: Define
interface StudentDocumentServiceand make both classes implement it. RewritesendTermDocumentsso it receivesservices: StudentDocumentService[]and contains zero knowledge of which concrete class is which. - Prove the win: The school now wants ID cards — a third PDF document. Add
IdCardMaker implements StudentDocumentServiceand count how many existing lines you had to edit. (Target: zero — compare with Figure 6.) - Bonus: Suppose
CertificateMakeractually came from an outside vendor's npm package and you cannot edit it. Write a five-lineCertificateAdapter implements StudentDocumentServicethat wraps it — and notice you just used the Adapter pattern exactly where it belongs: at the boundary. - Story check: Find the one line in the original
sendTermDocumentswhere a "cold-tiffin Thursday" bug could hide — a place where parameters could be swapped or a value mistranslated without any compiler complaint. What about your refactored version makes that mistake impossible?
Frequently asked questions
- What is the Alternative Classes with Different Interfaces smell in one line?
- Two classes do essentially the same job, but their method names and signatures differ, so callers cannot treat them interchangeably and must learn two vocabularies for one idea.
- How is this smell related to Refused Bequest?
- They are mirror images. Refused Bequest is classes sharing a contract they should not share. This smell is classes lacking a contract they should share. One has a false common interface; the other is missing a true one.
- What is the standard cure for this smell?
- First Rename Method and adjust signatures until the two classes' APIs match exactly. Then extract a shared interface or superclass and make callers depend on it. Often one class even becomes unnecessary and can be deleted.
- What if the mismatched classes come from third-party libraries I cannot edit?
- Use the Adapter pattern. Write a thin wrapper around each library that exposes your one unified interface. You cannot rename their methods, but you can hide their vocabulary behind yours at the boundary.
- Can forcing a common interface ever be wrong?
- Yes. If the similarity is only coincidental — two classes that both happen to have a process method but live in unrelated domains — a shared interface is a false abstraction. Unify classes that are true alternatives, not lookalikes.
Further reading
- Refactoring Guru: Alternative Classes with Different Interfaces article
- Refactoring (2nd Edition) by Martin Fowler book
- Samman Coaching: Alternative Classes with Different Interfaces article
- Adapter pattern in java-design-patterns (iluwatar) code
- SourceMaking: Alternative Classes with Different Interfaces article
Related Lessons
Refused Bequest: The Child Who Refused the Sweet Shop Recipes
Learn the Refused Bequest code smell with a family sweet shop story, Liskov violations in TypeScript and C#, and the delegation cure explained step by step.
Duplicate Code: Writing the Same Address on 50 Wedding Cards
Learn the Duplicate Code smell with a wedding card story. Understand DRY, the Rule of Three, and how Extract Method removes dangerous copy-paste code.
Switch Statements: The Receptionist With the Giant Rulebook
Learn the Switch Statements code smell with a school receptionist story, duplicated switch examples in TypeScript and C#, and the polymorphism cure.
Divergent Change: One Poor Clerk, Too Many Bosses
Learn the Divergent Change code smell with a school clerk story, simple definitions, TypeScript and C# examples, a clear comparison with Shotgun Surgery, and practice.