Inline Class: Merge a Class That Does Too Little
Learn the Inline Class refactoring through a school committee story. Merge a class that does too little back into its user and remove useless indirection.
📋 The committee with one member and one job
Sunrise Public School loves committees. There is a sports committee, a cultural committee, a discipline committee — and then there is the famous Library Committee.
Years ago, the Library Committee was a big deal: eight members, book fairs, reading week, author visits. The school magazine once ran a full page on it. But slowly, the work moved elsewhere. The book fair went to the cultural committee. Reading week went to the class teachers. The librarian, Mrs. Pillai, took over purchases herself because the committee meetings only slowed her down. Today, the Library Committee has exactly one member — poor Nikhil from Class 8 — and exactly one job: pinning the "new arrivals" list on the notice board, once a month.
But because it is still officially a committee, the full ceremony continues. Nikhil must book the meeting room for a "committee meeting" with himself. He sits alone at a long table, calls the meeting to order, and writes minutes of that meeting — "Present: Nikhil. Apologies: none." He submits a monthly report to the principal, Mrs. Rao, who files it without reading. The cultural committee, which manages all notice boards, must send him a formal letter asking for the list, then wait for his formal reply. Three pieces of paper and two days of delay — for one pin on one board.
Measure where the committee's effort actually goes, and the picture is embarrassing:
Mrs. Rao finally sees the situation clearly during the annual review and asks: "Why does this committee still exist?" Nobody has an answer — not even Nikhil, who looks deeply relieved to be asked. So she dissolves it. Nikhil joins the cultural committee, the "new arrivals" pin-up becomes one small line in the cultural committee's duty list, and all the letters, minutes, and monthly reports simply vanish. Nothing of value was lost — only ceremony was lost.
In code, we regularly find classes like the Library Committee: a class that once mattered, or was created with big hopes, but today holds one field and forwards one call. Every use of it costs an extra hop, an extra file, an extra name to remember. Mrs. Rao's cure has a name in our catalog: Inline Class.
🤔 What is Inline Class?
Inline Class is a refactoring where we move all the features of a nearly-empty class into the class that uses it, and then delete the empty shell. In Martin Fowler's Refactoring catalog it sits right beside its mirror twin: it is the exact inverse of Extract Class.
The recipe in one breath:
- Confirm the class truly carries almost no responsibility (and usually has a single client).
- Move every field into the host class with Move Field.
- Move every method into the host class with Move Method.
- Point any remaining references at the host, then delete the empty class.
As always, behaviour does not change. The same answers come out; one layer of wrapping paper is removed. The new-arrivals list still goes up on the board — it just stops needing a committee, a meeting room, and three letters to get there.
Why bother removing a small, harmless-looking class? Because no class is free. Every class costs the reader something: a name to learn, a file to open, a jump to follow while tracing what the code does. We happily pay that cost when the class carries real responsibility — a genuine concept, real behaviour, reuse across the project. But when a class merely passes things through, the cost continues while the benefit has stopped. Such a class is the Lazy Class smell, and the reading experience becomes: open Shipment, see it call TrackingCode.asString(), open TrackingCode, discover that asString() just returns the string you started with. Two files, one hop, zero information.
A one-question test for laziness: "If this class disappeared and its contents lived inside its only user, what would we lose?" If the answer is "nothing — same behaviour, one less file", inline it. If the answer is "we would lose type safety / a growing concept / a seam used by three clients", the class is earning its keep — leave it alone.
How do classes become lazy? Two common life stories:
- Born too early. Someone extracted a class "because we will surely need it when the feature grows". The feature never grew. This is Speculative Generality joining hands with Lazy Class.
- Hollowed out over time. The class was genuinely useful once, but refactoring after refactoring moved its responsibilities elsewhere, until only a shell remained — exactly like the Library Committee after the book fair and reading week moved away.
Both stories end the same way: the class no longer pays rent, and Inline Class is the polite eviction.
College corner: in metrics terms, a lazy class is the opposite failure of the Large Class. Where a god class fails on cohesion (high LCOM, many unrelated members), the lazy class fails on size and contribution: very low WMC (Weighted Methods per Class — the summed complexity of its methods is near zero because they only forward), tiny NOM (Number of Methods), and a fan-in of one. Detection strategies in the Lanza–Marinescu catalogue look for exactly this trio. There is also a system-level argument: every class added to a design increases what the architecture community calls conceptual weight — the number of names a maintainer must hold in their head. Studies of class-level defect density consistently show that indirection without abstraction (hops that add no decision, no invariant, no name worth knowing) raises comprehension time without lowering defects. Inline Class is how you pay down that tax.
📋 When do we need it?
Reach for Inline Class in these situations:
1. Lazy Class — the main customer. A class with one or two members, no decisions of its own, and mostly forwarding calls. Lazy Class is the smell; Inline Class is the named cure in every catalog.
2. After your own Extract Class went too far. You split a Large Class optimistically, but one of the pieces never developed a real job. Riding the seesaw back is the correct move — extraction and inlining are opposite directions on the same plank, and good designers travel both ways as requirements change.
3. A Middle Man in class form. When most of a class's methods simply delegate to another object, the class is a corridor, not a room. Sometimes the fix is Remove Middle Man (let callers talk to the target directly); when the corridor-class has a single client, inlining the whole thing into that client is the cleaner finish.
4. Before a bigger reshuffle. Fowler notes a practical trick: sometimes you inline two classes together temporarily so you can re-split them along a better seam afterwards. Inline Class is not only a destination — it can be a staging step that melts a bad boundary so a good one can be drawn.
5. Shotgun Surgery caused by over-splitting. When one tiny change forces edits in four micro-classes, the responsibilities were sliced too thin. Merging the splinters back — see Shotgun Surgery — turns four edits into one.
And when should you refuse to inline, even though the class looks tiny?
- Type-safety wrappers. A class like
TrackingCodewrapping one string may exist so the compiler stops you from passing a customer name where a tracking code belongs. That tiny class is actively preventing the Primitive Obsession smell and real bugs. Small is not the same as lazy — check what the class prevents, not just what it does. - Multiple clients. Inlining a class used by three clients means stuffing it into one and re-coupling the other two to that one. A shared class with several clients usually deserves to stay.
- Imminent growth. If a real, scheduled feature is about to give the class genuine work, inlining today and re-extracting next month is churn. (But be honest — "someday maybe" is not scheduled work.)
Use this table as the committee-review checklist, the same one Mrs. Rao ran in her head:
| Question | Lazy Class answer | Keep-it answer |
|---|---|---|
| How many members does it have? | One or two, all trivial | Several, with real bodies |
| Does it make any decisions? | No — it forwards or wraps | Yes — validation, rules, invariants |
| How many clients use it? | One | Two or more |
| What breaks if it vanishes? | Nothing | Type safety, a shared seam, a growing concept |
| Is growth actually scheduled? | "Someday, maybe" | A real ticket, this quarter |
Plot any small class on these two axes and the verdict appears:
👀 Before and after at a glance
A tiny TypeScript example. LateFineRule was extracted long ago with big plans; today it holds one number and one multiplication:
// BEFORE — a class that is one number in a costume
class LateFineRule {
private finePerDay = 2;
fineFor(daysLate: number): number {
return daysLate * this.finePerDay;
}
}
class Library {
private rule = new LateFineRule();
totalFine(daysLate: number): number {
return this.rule.fineFor(daysLate); // a hop that buys nothing
}
}To understand totalFine, a reader must open a second file and learn a second name — and the reward at the end of the jump is daysLate * 2. Here is that jump, drawn out — every single call pays this toll:
Dissolve the committee:
// AFTER — same behaviour, one class, zero hops
class Library {
private finePerDay = 2;
totalFine(daysLate: number): number {
return daysLate * this.finePerDay;
}
}One fewer class, one fewer file, and Library reads top to bottom without a detour. Nothing of value was lost — only ceremony.
🪜 Step-by-step, the safe way
Even a small merge deserves small steps. Mrs. Rao did not simply burn the committee's files; there was a last meeting, a handover note, and a quiet update to the cultural committee's duty list — then, and only then, the committee's name came off the staffroom board. Here is the same care as a procedure.
Step 1 — Verify laziness. Use "Find Usages" on the class. Confirm: (a) very few members, (b) no behaviour that makes real decisions, (c) one client — or you have consciously chosen the host. Also run the keep-it checks: is it a type-safety wrapper? Is growth genuinely scheduled? If both answers are no, proceed.
Step 2 — Prepare the host. In the absorbing class, create space for what is coming: declare the incoming fields and method stubs. Nothing is deleted yet.
Step 3 — Move the fields. Use Move Field for each field. The intermediate state keeps the lazy class working by delegating backwards to the host:
// INTERMEDIATE STATE — field has moved to Library; old class delegates
class Library {
finePerDay = 2; // moved in
private rule = new LateFineRule(this);
totalFine(daysLate: number): number {
return this.rule.fineFor(daysLate); // still hopping, for one more step
}
}
class LateFineRule {
constructor(private host: Library) {}
fineFor(daysLate: number): number {
return daysLate * this.host.finePerDay; // reads from its future home
}
}Compile, run the tests. Behaviour identical — the data has moved, the method has not yet.
Step 4 — Move the methods. Use Move Method for each method. fineFor's body lands inside Library.totalFine, and the old method becomes unused.
Step 5 — Redirect external references. If any other code still mentions LateFineRule (constructing it, importing it, using its type in a signature), point each reference at Library instead — one site at a time, compiling between changes.
Step 6 — Delete the shell. When "Find Usages" shows zero references, delete the class and its file. Then sweep up: remove the now-pointless rule field, any leftover constructor parameters, and unused imports.
Step 7 — Look again at the host. Inlining sometimes reveals the next refactoring: now that the logic is home, you may spot duplication with existing host code, or realise the host has quietly become large. Refactoring is a conversation, not a single move.
The whole dissolution as states — every state is a working, shippable program:
Do not skip the tests because the class "obviously does nothing". The classic Inline Class accident is a subtle behaviour change during the merge — for example, the lazy class lazily created its value on first use, and your inlined version creates it eagerly in the constructor, changing initialisation order. Run the suite after Steps 3, 4, 5, and 6 separately. If the lazy class has no tests at all, write one characterisation test first, even a trivial one.
🏫 A bigger real-life example
Now the full committee story in TypeScript. Here is the one-member Library Committee, with all its ceremony, and the cultural committee that must write letters to it:
// BEFORE — a committee of one, with full paperwork
class LibraryCommittee {
private member = "Nikhil";
private newArrivals: string[] = [];
recordArrival(book: string): void {
this.newArrivals.push(book);
}
monthlyNoticeText(): string {
return `New arrivals (by ${this.member}): ${this.newArrivals.join(", ")}`;
}
}
class CulturalCommittee {
private boardNotices: string[] = [];
private library = new LibraryCommittee();
requestArrival(book: string): void {
this.library.recordArrival(book); // formal letter to the committee
}
pinMonthlyNotices(): string[] {
// formal reply received, then pinned
this.boardNotices.push(this.library.monthlyNoticeText());
return [...this.boardNotices];
}
}Check the laziness signs against the checklist table: LibraryCommittee has one client (CulturalCommittee), two tiny members, and no decision-making of its own — it stores names and joins them with commas. Every operation arrives as a forwarded call. On the Figure 4 quadrant it sits deep in the inline-it corner. Dissolve it:
// AFTER — Nikhil joins the cultural committee; the paperwork vanishes
class CulturalCommittee {
private boardNotices: string[] = [];
private newArrivals: string[] = [];
private arrivalsInCharge = "Nikhil";
recordArrival(book: string): void {
this.newArrivals.push(book);
}
pinMonthlyNotices(): string[] {
this.boardNotices.push(
`New arrivals (by ${this.arrivalsInCharge}): ${this.newArrivals.join(", ")}`,
);
return [...this.boardNotices];
}
}
// Callers now write:
// committee.recordArrival("Malgudi Days");
// committee.pinMonthlyNotices();Count the savings. One class deleted. The forwarding method requestArrival deleted — callers call recordArrival directly, with the same one-line body it always had at the end of the chain. Reading pinMonthlyNotices no longer requires a trip into another file. And notice what inlining revealed: the duty is now one honest line in the cultural committee's list, exactly as the principal intended.
The reader's cost, measured the way a new programmer would feel it — files to open and hops to follow before understanding one notice-pinning call:
College corner: the metric being halved in Figure 10 has a research name — navigation cost or defect of locality. Program-comprehension studies (and every code-reading session you will ever sit in) show that understanding time grows with the number of delocalised fragments a reader must stitch together; each extra file roughly doubles the chance of losing the thread. There is also a coupling angle worth writing in an exam answer: a Middle Man class does not reduce coupling, it launders it. CulturalCommittee was still fully coupled to the arrivals data before the merge — the dependency just travelled through an extra node, making the dependency graph look more modular while adding a node and two edges. Inlining makes the true coupling visible and cheaper at the same time.
💻 The same refactoring in C# and Python
The same merge in C#, with a courier flavour. ConsignmentNumber wraps one string and adds pure ceremony:
// BEFORE
class ConsignmentNumber
{
public string Value { get; }
public ConsignmentNumber(string value) => Value = value;
public string AsText() => Value; // returns what you gave it
}
class Parcel
{
private readonly ConsignmentNumber _number;
public Parcel(string number) => _number = new ConsignmentNumber(number);
public string Label() => $"Consignment: {_number.AsText()}";
}// AFTER
class Parcel
{
private readonly string _consignmentNumber;
public Parcel(string number) => _consignmentNumber = number;
public string Label() => $"Consignment: {_consignmentNumber}";
}One honest caution before you copy this everywhere: a wrapper like ConsignmentNumber is lazy only if it does nothing. If it validated the format ("CN-" prefix, twelve digits) or stopped you from passing a phone number where a consignment number belongs, it would be earning its keep by preventing Primitive Obsession — and you should keep it. Inline the costume, not the bodyguard.
The Python version of the same merge, in miniature:
# BEFORE — one number in a costume
class LateFineRule:
def __init__(self):
self.fine_per_day = 2
def fine_for(self, days_late):
return days_late * self.fine_per_day
class Library:
def __init__(self):
self._rule = LateFineRule()
def total_fine(self, days_late):
return self._rule.fine_for(days_late) # hop
# AFTER — the costume comes off
class Library:
def __init__(self):
self._fine_per_day = 2
def total_fine(self, days_late):
return days_late * self._fine_per_day🧰 IDE support
| Tool | Support | How |
|---|---|---|
| IntelliJ IDEA / JetBrains family | Strong | Inline (Ctrl+Alt+N) for methods and more; combine with Move for the fields |
| JetBrains Rider / ReSharper (C#) | Strong | Inline Method and Inline Variable are one keystroke; the class-level merge follows the step sequence |
| Visual Studio (plain) | Manual | Inline Temporary Variable exists; the full class inline is the seven safe steps |
| VS Code (TypeScript) | Manual | "Find All References" plus rename; merge field by field, method by method |
A pleasant truth about Inline Class: because the class being removed is tiny by definition, even the fully manual version is usually a fifteen-minute job — provided you go step by step with tests.
⚖️ Benefits and risks
| Benefit ✅ | Risk / cost ⚠️ | |
|---|---|---|
| Readability | Callers read straight through with no detour into a second file | If the host was already big, the merge can push it towards Large Class |
| Simplicity | One less name, file, constructor, and forwarding layer to maintain | Inlining a class used by several clients re-couples them through the host |
| Honesty | The code stops pretending a one-liner is a grand concept | A type-safety wrapper inlined away can let real bugs (mixed-up strings) back in |
| Momentum | Often reveals duplication and the next useful refactoring in the host | Subtle initialisation-order changes can slip in during the merge — test each step |
| Design flow | Lets you melt a bad boundary and re-split along a better seam | Inlining a class that was genuinely about to grow causes churn next month |
The seesaw: Inline Class ↔ Extract Class. Remember the plank with two seats. Extract Class sits on one end and answers "this class does too much"; Inline Class sits on the other and answers "this class does too little". A healthy codebase moves in both directions over its lifetime: a concept is extracted when it grows real weight, and folded back when its weight drains away. The Library Committee itself rode this seesaw — it was extracted from general school duties years ago when book fairs were big, and inlined back when the weight left. Neither direction is a defeat. The only mistake is refusing to move — keeping a giant class out of laziness, or keeping an empty class out of sentiment. Judge every class by today's responsibility, not yesterday's plans.
🧪 Which smells does it cure?
| Smell | How Inline Class helps |
|---|---|
| Lazy Class | The direct cure — the class that no longer pays rent is merged away |
| Middle Man | A class that only forwards calls collapses into its single client |
| Speculative Generality | The "we might need it someday" class is removed until a real need exists |
| Shotgun Surgery | Over-split micro-classes merge, so one change edits one place |
| Large Class | Indirect help — inlining can be the staging step before re-splitting along a better seam |
📦 Quick revision box
+--------------------------------------------------------------+
| INLINE CLASS — CHEAT CARD |
+--------------------------------------------------------------+
| Story : one-member Library Committee -> merge into the |
| cultural committee, delete the paperwork |
| Problem : a class that does too little but still costs a |
| name, a file, and a hop on every call |
| Smell : Lazy Class (main), Middle Man, Speculative Gen. |
| Fix : verify lazy -> Move Field x N -> Move Method x N |
| -> redirect references -> DELETE the shell |
| Test : after every move; watch initialisation order |
| Keep it : type-safety wrappers, multi-client classes, |
| genuinely scheduled growth |
| Inverse : Extract Class (the other end of the seesaw) |
+--------------------------------------------------------------+✏️ Practice exercise
Here is a suspicious little class. Decide its fate, then act.
class HouseColour {
constructor(private colour: string) {}
get(): string {
return this.colour;
}
}
class SchoolHouse {
private colour: HouseColour;
constructor(
public name: string,
colour: string,
) {
this.colour = new HouseColour(colour);
}
banner(): string {
return `${this.name} House — wear ${this.colour.get()} on sports day`;
}
}Your tasks:
- Run the laziness checklist table on
HouseColour: how many members? Any decisions? How many clients? What would be lost if it vanished? Then place it on the Figure 4 quadrant. - Inline it using the safe steps: move the field into
SchoolHouse, collapse theget()call, redirect references, delete the class. Write one test first:new SchoolHouse("Red", "red").banner()must read "Red House — wear red on sports day", and it must pass unchanged after every step. - After inlining, read
SchoolHousealoud. Is it easier to follow than before? Count the files a new student must open to understandbanner()— before and after — and compare with Figure 10. - Bonus judgement question: now imagine
HouseColourinstead contained a check that only "red", "blue", "green", or "yellow" are allowed, throwing an error otherwise. Would you still inline it? Which smell would inlining it invite back? - Final seesaw question: write one sentence each for (a) a situation where you would extract a class this month, and (b) a situation where you would inline one — proving you can ride the seesaw both ways.
If task 4 made you say "keep it — it guards against Primitive Obsession", you have learned the most grown-up lesson of this refactoring: small classes are not always lazy; only useless ones are. Nikhil's committee was dissolved not because it was small, but because nothing — not one thing — would have been lost without it. That is the test, and now it is yours.
Frequently asked questions
- What is the Inline Class refactoring in simple words?
- Inline Class means taking a class that does almost nothing and merging all its fields and methods into the class that uses it, then deleting the empty shell. The program behaves exactly the same; we have simply removed a pointless extra layer that readers had to jump through.
- How do I recognise a class that should be inlined?
- Look for a class with one or two members, no real behaviour of its own, and usually only one client. It mostly forwards calls or wraps a single value. Ask: if this class vanished and its contents lived inside its user, would anything be lost? If the honest answer is no, it is a Lazy Class — inline it.
- Is Inline Class the opposite of Extract Class?
- Yes, exactly. Extract Class splits a class doing too much; Inline Class merges away a class doing too little. They are two directions on the same seesaw. Designs breathe over time — yesterday's useful extraction can become today's empty shell after responsibilities migrate away, and inlining it back is healthy, not shameful.
- When should I NOT inline a small class?
- Keep the class if it is genuinely about to grow, if multiple clients use it, or if it exists for type safety — a small wrapper like TrackingCode prevents you from mixing it up with ordinary strings, which fights the Primitive Obsession smell. A class earning its keep through safety or planned growth is not lazy.
- What are the basic steps of Inline Class?
- First confirm the class has little responsibility and (usually) one client. Then move each field into the host with Move Field, move each method with Move Method, redirect every remaining reference to the host, and finally delete the empty class. Compile and run tests after every small step.
Further reading
Related Lessons
Extract Class: Give an Overworked Class a Helping Partner
Learn the Extract Class refactoring with a fun school office story. Split one overloaded class into two focused classes, each with a single clear job to do.
Move Method: Shift Work to the Class Where It Truly Belongs
Learn the Move Method refactoring through a simple school story. Shift a method into the class whose data it uses most so behaviour and data stay together.
Move Field: Keep Data in the Room Where It Is Used
Learn the Move Field refactoring with an easy school story. Move a piece of data to the class that really uses it, so state and behaviour live side by side.
Lazy Class: The Watchman Whose Only Job Is Pressing One Lift Button
Learn the Lazy Class code smell with a society watchman story. Find classes that do too little to deserve existing, and cure them with Inline Class.