Replace Method with Method Object: Give the Big Dish Its Own Kitchen Station
Learn Replace Method with Method Object through a wedding kitchen story, TypeScript and C# examples, and safe steps to untangle long methods for beginners.
🍛 The Story of the Wedding Biryani Station
There is a wedding at your cousin's house, and two hundred guests are coming. The family cook, Salma Khala, is brilliant — thirty years of weddings, Eid feasts, and exam-day breakfasts behind her. But tonight's menu has a monster on it: the wedding biryani. Soaked rice, fried onions, marinated chicken, saffron milk, mint, ghee, sealed-pot steaming — dozens of ingredients and steps, all of which must be juggled at the same time.
At first, Salma Khala tries to make it at the ordinary stove, in the middle of the regular kitchen. It is a disaster waiting to happen. The biryani needs eleven bowls of prepared ingredients around her, but the counter also holds the dal for lunch, the chai kettle, and someone's tiffin boxes. Her nephew Imran, a willing helper, walks past and asks, "Khala, which bowl is the fried onion for the biryani and which is for the raita?" She cannot answer without turning around and pointing. She cannot hand over even one small task — "please add the saffron milk" — because the saffron milk sits somewhere in that crowded mess, and only she knows where. Worse, every time she hands Imran a job, she must also hand him an armful of bowls: "take this, and this, and this, and bring back the rice bowl and the masala bowl when you are done." Imran drops a bowl. The dal gets fried onions in it. Khala's patience, famously long, begins to shorten.
Then the wise old caterer uncle arrives, takes one look, and says the obvious thing: "Beta, this dish is too big for one person at one stove. Give it its own station."
So they set up a separate biryani station in the courtyard. It has its own table, its own stove, and — most importantly — its own labelled shelves: one shelf for the soaked rice, one for the marinade, one for the fried onions, one for the saffron milk. Everything the biryani needs lives at the station. Now magic happens. Salma Khala can tell Imran, "Go to the biryani station and do the layering step." Imran does not need to carry eleven bowls across the kitchen — everything is already on the shelves, exactly where the station keeps it. The giant job breaks naturally into small named steps: prepare rice, fry onions, layer the pot, seal and steam. Each step is small, teachable, and checkable. By evening, three cousins are working the station in parallel shifts, and Khala is supervising with a cup of chai, which is how it always should have been.
Hold this picture. In code, a huge method with many tangled local variables is the biryani at the ordinary stove. You cannot split it into helper methods, because every helper would need armfuls of those local variables passed in and out — Imran with his eleven bowls. The fix is the caterer uncle's advice: build a separate station for that one dish. In refactoring language, the station is a new class, its labelled shelves are fields, and the cooking steps are small private methods that all share those shelves. This refactoring is called Replace Method with Method Object.
🔍 What is Replace Method with Method Object?
Replace Method with Method Object takes one long, tangled method and turns it into its own class. The recipe:
- Create a new class named after the work the method does —
BiryaniCooker,PriceCalculator,ReportBuilder. - Move the method's parameters and local variables into the new class as private fields, set through the constructor (parameters) or assigned during the work (locals).
- Move the method's body into a single public method on the new class, usually called
compute(),run(), orexecute(). - Make the original method a one-liner that creates the object and calls it.
Why does this unlock anything? Because of scope. Inside the original method, the local variables can be seen only by that one method. Any helper method you try to extract is standing outside that scope, so every value must be passed in as a parameter and every change must be passed back as a return value. When five locals are read and written by every fragment, extraction becomes so clumsy that you give up — this is exactly the helper who must carry eleven bowls across the kitchen.
But once those locals become fields of a class, their scope changes from "this one method" to "this whole object." Every helper method inside the object can see them for free, the way every cook at the biryani station can reach every shelf. Suddenly Extract Method — which kept failing — becomes trivially easy, and the once-monolithic body can be carved into a clean sequence of small, well-named private methods.
One line to remember: when a method's local variables are too tangled to extract anything, promote the method to its own class — locals become shelves (fields) that every helper step can reach. The method object is not the goal; it is the trick that makes Extract Method possible again.
A naming note from Fowler's books. In the first edition of Refactoring (1999), this technique is called Replace Method with Method Object. In the second edition (2018), Martin Fowler renamed it Replace Function with Command, because the object you create — something that wraps one action, gets configured, and is then asked to run — is what he calls a command object, a close relative of the Command design pattern. Both names describe the same mechanics. Fowler also adds an honest warning in the new edition: he reaches for a plain function rather than a command object the overwhelming majority of the time, because a whole class for one computation is heavy. Use this refactoring when the tangle genuinely demands it — it is the heavy artillery of the Composing Methods family, not the everyday tool.
College corner: the phrase command object deserves a proper definition, because it appears in every design-patterns course. A command object reifies an action — it turns "doing something" into "a thing you can hold." Once an action is an object, it gains abilities a plain function call never has: it can be stored in a list and executed later (job queues), executed on another thread, logged with all its inputs, retried after failure, or paired with an undo() method that reverses it (every text editor's undo stack is a list of command objects). Functional programmers will notice that a closure can also capture state and be passed around — and indeed, in JavaScript or Python, a closure is often the lightweight alternative to a method object. The difference is surface area: a closure captures state invisibly, while a command object declares its state as named fields and can expose extra methods (undo, describe, getters for intermediate results). When you need those extras, the class earns its ceremony; when you do not, Fowler's advice stands — prefer the plain function.
🕑 When do we need it?
The signal is very specific, so learn it well: you tried to shorten a Long Method with Extract Method, and the locals defeated you.
Here is how that defeat usually feels:
- You select fifteen lines that clearly form one step, click Extract Method, and the IDE proposes a new function with five parameters and an impossible return. The fragment reads
subtotal,taxRate, anditems, and writessubtotalanddiscount. Most languages can return only one value, so the IDE suggests returning a tuple or an object — ugly enough that you cancel. - You try a different fragment. Same story, different five variables. The temporaries form a web that pins the whole method together as one indivisible blob.
- The method keeps growing, because every new requirement is easier to bolt on than to restructure.
Other situations that point the same way:
- The computation deserves its own tests. Once the algorithm lives in its own class, you can construct it with crafted inputs and test it directly, without dragging in the big class it used to live inside.
- The algorithm is a guest in the wrong house. A 100-line pricing calculation inside an
Orderclass bloats the class and hides the order's real responsibilities. Moving it to aPriceCalculatorslims the host — this also helps a Large Class. - You sense a design pattern hiding. Long tangled computations are often a Strategy or a Command waiting to be born. The method object is the first step of that birth.
And the situations where you should not use it:
- If a couple of lighter refactorings — Split Temporary Variable, Replace Temp with Query, Remove Assignments to Parameters — would untangle the locals enough for plain Extract Method, do those instead. Heavy artillery for heavy problems only.
- If the method is long but its locals are not tangled (a flat list of independent steps), plain Extract Method already works. No station needed.
The quadrant below is the caterer uncle's judgement, drawn as a chart. The further your method sits toward the top-right, the more the station pays for itself:
Because students often mix up the two tools, here is a plain comparison you can keep:
| Question | Extract Method | Replace Method with Method Object |
|---|---|---|
| What moves? | A fragment of lines into a new function | The whole method into a new class |
| What happens to locals? | Passed as parameters and returns | Promoted to private fields |
| Cost | Nearly free — one new function | A class, a constructor, several fields |
| When it works | Locals are few or independent | Locals are many and tangled |
| When it fails | Fragment needs 4+ params and 2+ returns | Almost never fails, but often overkill |
| Try it | Always first | Only after extraction is defeated |
👀 Before and after at a glance
Here is a compact TypeScript example. A function computes a student's final scholarship score from marks, attendance, and activity points. Its three working variables are read and written all over the body, so no fragment can be extracted cleanly:
// BEFORE: three locals woven through every step — Extract Method keeps failing
function scholarshipScore(marks: number[], attendancePct: number, activityPoints: number): number {
let base = 0;
let bonus = 0;
let penalty = 0;
for (const m of marks) {
base += m;
if (m >= 90) bonus += 5; // writes bonus, reads marks
}
base = base / marks.length;
if (attendancePct < 75) {
penalty += (75 - attendancePct) * 2; // writes penalty
if (base > 80) penalty = penalty / 2; // ...but reads base!
}
bonus += Math.min(activityPoints, 20);
if (penalty > bonus) bonus = 0; // reads penalty, writes bonus
return Math.round(base + bonus - penalty);
}Try extracting the attendance block: it reads attendancePct and base, and writes penalty. Try the activity block: it reads activityPoints and penalty, and writes bonus. Every candidate fragment touches two or three of the working variables. This is the biryani at the ordinary stove.
Now the method object — the dish gets its own station:
// AFTER: a station with shelves (fields) — every step can reach everything
class ScholarshipScorer {
private base = 0;
private bonus = 0;
private penalty = 0;
constructor(
private readonly marks: number[],
private readonly attendancePct: number,
private readonly activityPoints: number,
) {}
compute(): number {
this.scoreMarks();
this.applyAttendancePenalty();
this.applyActivityBonus();
return Math.round(this.base + this.bonus - this.penalty);
}
private scoreMarks(): void {
for (const m of this.marks) {
this.base += m;
if (m >= 90) this.bonus += 5;
}
this.base = this.base / this.marks.length;
}
private applyAttendancePenalty(): void {
if (this.attendancePct < 75) {
this.penalty += (75 - this.attendancePct) * 2;
if (this.base > 80) this.penalty = this.penalty / 2;
}
}
private applyActivityBonus(): void {
this.bonus += Math.min(this.activityPoints, 20);
if (this.penalty > this.bonus) this.bonus = 0;
}
}
// The original function becomes a one-line delegator
function scholarshipScore(marks: number[], attendancePct: number, activityPoints: number): number {
return new ScholarshipScorer(marks, attendancePct, activityPoints).compute();
}Look at compute() now. It reads like the caterer's checklist: score the marks, apply the attendance penalty, apply the activity bonus, return the total. Each helper method takes zero parameters and returns nothing, because the shelves — base, bonus, penalty — are right there as fields. The extraction that was impossible before became effortless.
From the caller's side, nothing about the conversation gets harder. Configure the station, ask it to cook, receive the dish:
And here is the payoff measured in the only currency that matters during extraction — how much luggage each helper step must carry:
🪜 Step-by-step, the safe way
This refactoring moves a lot of code, so discipline matters more than usual. The golden rule: get to a working, tested, delegating state first, and only then start decomposing.
Run your tests before you begin and after every numbered step below. The most dangerous moment is step 4, when you copy the body into the new class and convert locals to fields — one missed this. or one local left behind can silently change behavior. If the method has no tests, write characterization tests first: feed it several inputs, record the outputs, and assert them. You are about to perform surgery; do not operate without a pulse monitor.
We will refactor the scholarship example one careful step at a time.
Step 1 — Create the empty station. Make a new class named after the computation. Nothing else yet:
// INTERMEDIATE: empty class — code compiles, tests untouched and green
class ScholarshipScorer {}Step 2 — Add a field for each parameter, set via the constructor. Parameters are the ingredients delivered to the station; they never change after delivery, so mark them readonly:
// INTERMEDIATE
class ScholarshipScorer {
constructor(
private readonly marks: number[],
private readonly attendancePct: number,
private readonly activityPoints: number,
) {}
}Run the tests. Still green — nobody uses this class yet.
Step 3 — Add a field for each local variable. The locals are the working shelves: base, bonus, penalty, each starting at the same initial value the local had.
Step 4 — Copy the method body into compute() and convert every local and parameter reference to a field reference. This is the delicate step. Work mechanically: paste the body, delete the local declarations (they are fields now), and prefix each reference with this.. If the original method called other methods of its host class, pass the host object into the constructor too and call through it (our example does not need this).
Step 5 — Make the original method delegate. Replace its whole body with one line:
// INTERMEDIATE: behavior identical, structure new — the critical checkpoint
function scholarshipScore(marks: number[], attendancePct: number, activityPoints: number): number {
return new ScholarshipScorer(marks, attendancePct, activityPoints).compute();
}Run the entire test suite now. This is the checkpoint that matters most: same inputs, same outputs, new home. At this moment compute() is still one long ugly block — that is fine and expected. Do not decompose and move in the same step; movers do not rearrange furniture while the truck is driving.
Step 6 — Now decompose compute() freely with Extract Method. Because every former local is a field, each fragment extracts with no parameters and no return value. Extract one step, run tests. Extract the next, run tests. This is where scoreMarks(), applyAttendancePenalty(), and applyActivityBonus() were born in the "after" code above.
Step 7 — Tidy the station. Make sure all fields are private, give the class a doc comment saying what it computes, and check whether any field can become readonly. The public surface should be exactly two things: the constructor and compute().
Seen as states rather than steps, the method passes through exactly four conditions, and you should always know which one you are standing in:
College corner: notice the trade you just made, because it is a genuine engineering trade-off and not a free lunch. Local variables have a wonderful property — their lifetime is one method call, so no other code can ever observe them mid-change. Fields are different: they are shared mutable state within the object, and every helper method can read and write them in any order. The object is still safe externally (the fields are private, and a fresh object is built per computation, so there is no cross-call or cross-thread leakage as long as you do not cache and share one instance). But internally, correctness now depends on compute() calling the steps in the right sequence — call applyActivityBonus() before applyAttendancePenalty() in our example and the penalty > bonus rule misfires. This is called temporal coupling: an ordering dependency the compiler cannot check. Mature codebases tame it by keeping compute() as the single conductor that owns the order, never letting helper methods call each other, and asserting invariants between steps in tests. When you read about why functional programmers fear mutable state, this little class is a perfect classroom example — small enough to be safe, real enough to show the danger.
🧺 A bigger real-life example
Back to the wedding. The catering company has a quoting function that estimates the cost of cooking a feast: ingredients, labour, fuel, rentals, festival surcharges. It has grown for three years and now looks like this — a real biryani-at-the-ordinary-stove:
interface FeastOrder {
guests: number;
dishes: { name: string; costPerPlate: number; isSpecial: boolean }[];
isFestivalSeason: boolean;
needsTandoor: boolean;
}
// BEFORE: every block reads and writes ingredientCost, labourHours, surcharge
function quoteFeast(order: FeastOrder): number {
let ingredientCost = 0;
let labourHours = 0;
let surcharge = 0;
for (const dish of order.dishes) {
ingredientCost += dish.costPerPlate * order.guests;
labourHours += dish.isSpecial ? 3 : 1;
if (dish.isSpecial && order.guests > 100) {
labourHours += 2; // big special dishes need extra hands
surcharge += 500; // ...and a special-dish surcharge
}
}
if (order.isFestivalSeason) {
surcharge += ingredientCost * 0.1; // reads ingredientCost, writes surcharge
labourHours = labourHours * 1.25; // festival staff work slower shifts
}
if (order.needsTandoor) {
surcharge += 1500;
if (labourHours > 40) surcharge += 800; // reads labourHours, writes surcharge
}
const labourCost = labourHours * 200;
return Math.round(ingredientCost + labourCost + surcharge);
}Try to extract the festival block: it reads ingredientCost, writes surcharge, and rewrites labourHours. Try the tandoor block: it reads labourHours and writes surcharge. The three working variables crisscross every block. This method has resisted cleanup for three years for exactly this reason.
Give the quote its own station:
// AFTER: FeastQuote is the station; its fields are the labelled shelves
class FeastQuote {
private ingredientCost = 0;
private labourHours = 0;
private surcharge = 0;
constructor(private readonly order: FeastOrder) {}
compute(): number {
this.costAllDishes();
this.applyFestivalSeason();
this.applyTandoorCharges();
return Math.round(this.ingredientCost + this.labourCost() + this.surcharge);
}
private costAllDishes(): void {
for (const dish of this.order.dishes) {
this.ingredientCost += dish.costPerPlate * this.order.guests;
this.labourHours += dish.isSpecial ? 3 : 1;
if (dish.isSpecial && this.order.guests > 100) {
this.labourHours += 2;
this.surcharge += 500;
}
}
}
private applyFestivalSeason(): void {
if (!this.order.isFestivalSeason) return;
this.surcharge += this.ingredientCost * 0.1;
this.labourHours = this.labourHours * 1.25;
}
private applyTandoorCharges(): void {
if (!this.order.needsTandoor) return;
this.surcharge += 1500;
if (this.labourHours > 40) this.surcharge += 800;
}
private labourCost(): number {
return this.labourHours * 200;
}
}
function quoteFeast(order: FeastOrder): number {
return new FeastQuote(order).compute();
}Read compute() aloud: cost all dishes, apply festival season, apply tandoor charges, total it up. A new teammate understands the pricing policy in ten seconds. Each policy lives in its own named method, so when the boss says "festival surcharge is now 12 percent," the change is one line inside applyFestivalSeason() — and that method can even be unit-tested by building a FeastQuote with a crafted order.
And notice the bonus the caterer uncle never mentioned: now that the quote is an object, new powers come almost free. Want a detailed bill that shows ingredient cost, labour, and surcharge separately? Add three getter methods — the values are already sitting on the shelves. Want to compare a festival quote with a non-festival quote? Build two FeastQuote objects. Want to queue a hundred quotes overnight, or log every quote with all its inputs for the accountant? An object can wait in a list; a half-finished local variable cannot. The original tangled function could do none of this without major surgery.
💻 The same refactoring in C#
C# teams meet this pattern constantly in pricing, payroll, and report generation code. Here is the shape of the refactoring, compressed:
// BEFORE: tangled locals inside Payroll
public class Payroll
{
public decimal MonthlySalary(Employee emp, int daysWorked, int overtimeHours)
{
decimal basePay = emp.DailyRate * daysWorked;
decimal overtimePay = 0m;
decimal deduction = 0m;
if (overtimeHours > 0)
{
overtimePay = overtimeHours * emp.DailyRate / 8m * 1.5m;
if (basePay > 50000m) overtimePay *= 0.8m; // reads basePay!
}
if (daysWorked < 20)
{
deduction = (20 - daysWorked) * emp.DailyRate * 0.5m;
if (overtimePay > 0m) deduction *= 0.75m; // reads overtimePay!
}
return basePay + overtimePay - deduction;
}
}// AFTER: the computation gets its own class
public class SalaryCalculation
{
private readonly Employee _emp;
private readonly int _daysWorked;
private readonly int _overtimeHours;
private decimal _basePay;
private decimal _overtimePay;
private decimal _deduction;
public SalaryCalculation(Employee emp, int daysWorked, int overtimeHours)
{
_emp = emp;
_daysWorked = daysWorked;
_overtimeHours = overtimeHours;
}
public decimal Compute()
{
_basePay = _emp.DailyRate * _daysWorked;
ApplyOvertime();
ApplyAttendanceDeduction();
return _basePay + _overtimePay - _deduction;
}
private void ApplyOvertime()
{
if (_overtimeHours <= 0) return;
_overtimePay = _overtimeHours * _emp.DailyRate / 8m * 1.5m;
if (_basePay > 50000m) _overtimePay *= 0.8m;
}
private void ApplyAttendanceDeduction()
{
if (_daysWorked >= 20) return;
_deduction = (20 - _daysWorked) * _emp.DailyRate * 0.5m;
if (_overtimePay > 0m) _deduction *= 0.75m;
}
}
public class Payroll
{
public decimal MonthlySalary(Employee emp, int daysWorked, int overtimeHours)
=> new SalaryCalculation(emp, daysWorked, overtimeHours).Compute();
}C#-specific notes: mark the constructor-set fields readonly so the compiler guarantees the ingredients never change after delivery; keep working fields private; and if the original method used other members of Payroll, pass the Payroll instance into the constructor and call through it. Many C# teams also name these classes by role — SalaryCalculation, InvoiceBuilder, RouteOptimizer — which reads more naturally than MonthlySalaryMethodObject.
The same idea in Python, in its most compact form — Python's lack of access keywords just means the underscore convention does the work of private:
# AFTER, Python flavour: a tiny method object
class SalaryCalculation:
def __init__(self, daily_rate, days_worked, overtime_hours):
self._daily_rate = daily_rate
self._days_worked = days_worked
self._overtime_hours = overtime_hours
self._base = 0.0
self._overtime = 0.0
self._deduction = 0.0
def compute(self):
self._base = self._daily_rate * self._days_worked
self._apply_overtime()
self._apply_attendance_deduction()
return self._base + self._overtime - self._deduction
def _apply_overtime(self):
if self._overtime_hours <= 0:
return
self._overtime = self._overtime_hours * self._daily_rate / 8 * 1.5
if self._base > 50000:
self._overtime *= 0.8
def _apply_attendance_deduction(self):
if self._days_worked >= 20:
return
self._deduction = (20 - self._days_worked) * self._daily_rate * 0.5
if self._overtime > 0:
self._deduction *= 0.75🛠️ IDE support
This is one of the rare refactorings where one IDE family gives you a true one-click version:
- IntelliJ IDEA (Java): has a dedicated refactoring literally called Extract Method Object (Refactor → Extract → Method Object...). You select the code, and the IDE creates the class, turns locals into fields, builds the constructor, and rewrites the original method as a delegator — the whole steps 1 to 5 of our safe sequence in one action. It even offers an "inner class" option if you want the station to live inside the original class at first.
- JetBrains Rider / ReSharper (C#): no single-click method-object action, but the combination works well: Extract Method on pieces after you manually create the class, Extract Class to move grouped members, and Introduce Field to promote locals one at a time. Rider's Introduce Parameter Object is a smaller cousin useful when only the parameter list (not the locals) is the problem.
- Visual Studio (C#): Extract Method (Ctrl+R, M) and Introduce Field quick actions cover the mechanical pieces; the class creation and constructor wiring are manual but quick. Roslyn-based extensions add more.
- VS Code (TypeScript/JavaScript): Extract to method/function and Extract to constant refactorings help with step 6; the class setup in steps 1 to 5 is manual. The TypeScript compiler is your friend during the conversion — every missed
this.becomes a red squiggle naming an undeclared variable.
Even with one-click support, run the tests at the same checkpoints. IDEs are excellent but not magical, especially when the method touches fields of its host class or has sneaky early returns.
⚖️ Benefits and risks
| Benefits | Risks and limits |
|---|---|
| Tangled locals become fields, so Extract Method finally works — the long method can be carved into small named steps | A whole new class for one computation: more files, more indirection, more surface area |
The algorithm becomes independently testable — construct the object with crafted inputs and call compute() | Locals promoted to mutable fields means shared mutable state inside the object; if helper methods run in the wrong order, bugs appear that locals could never have |
compute() becomes a readable checklist of policy steps; each policy change touches one small method | Overkill when lighter refactorings (Split Temporary Variable, Replace Temp with Query) would untangle the locals — always try those first |
| Free bonuses: intermediate values can be exposed for itemized output, and the object can grow undo, logging, queueing, or async powers | The name "method object" tempts people to apply it everywhere; Fowler himself prefers a plain function the vast majority of the time |
| Often reveals a hidden design — many method objects mature into a proper Strategy or Command | If the host class's members are needed, you must pass the host in, which couples the new class back to it |
The honest summary: this refactoring trades a scope problem for a structure cost. Pay that cost only when the scope problem is real — when extraction has genuinely failed because of tangled locals — and the trade is excellent.
🧪 Which smells does it cure?
| Smell | How this refactoring helps |
|---|---|
| Long Method | This is the heavy-artillery cure for the worst long methods — the ones whose interwoven locals defeat ordinary Extract Method. Promote the method to a class, then decompose freely |
| Large Class | Moving a hundred-line computation out of Order or Account into its own class slims the host and returns it to its real responsibilities |
| Long Parameter List | Inside the method object, helper steps share state through fields instead of passing six parameters around; outside it, callers pass everything once, through one constructor |
| Duplicate Code | Once the algorithm lives in one named, testable class, scattered near-copies of the same computation can be deleted and pointed at the single station |
📋 Quick revision box
+--------------------------------------------------------------------+
| REPLACE METHOD WITH METHOD OBJECT — REVISION CARD |
+--------------------------------------------------------------------+
| Story : The wedding biryani gets its OWN STATION with |
| labelled shelves — fields every step can reach. |
| Signal : Extract Method keeps failing — every fragment |
| needs 3+ locals in and 2+ values out. |
| Recipe : new class -> params + locals become fields -> |
| body moves into compute() -> old method delegates |
| -> NOW extract small steps freely. |
| Checkpoint: full test suite green right after delegation, |
| BEFORE any decomposition. Move, verify, then carve. |
| Naming : Fowler 2nd ed. calls it "Replace Function with |
| Command" — same mechanics, command-object framing. |
| Caution : heavy artillery. Try Split Temporary Variable and |
| Replace Temp with Query first. Keep fields private. |
| Bonus : the new class often matures into Strategy / Command. |
+--------------------------------------------------------------------+✍️ Practice exercise
Set up your own station. Below is a tangled TypeScript function from a school transport app that computes a monthly bus fee. Its three working variables crisscross every block, so plain extraction fails — perfect practice material:
function busFee(distanceKm: number, daysPerWeek: number, isACBus: boolean, siblingCount: number): number {
let fee = 0;
let discount = 0;
let fuelExtra = 0;
fee = distanceKm * 30;
if (distanceKm > 10) {
fuelExtra = (distanceKm - 10) * 12;
if (fee > 600) fuelExtra = fuelExtra * 0.8; // reads fee, writes fuelExtra
}
fee = fee * (daysPerWeek / 5);
if (isACBus) {
fee = fee * 1.4;
fuelExtra = fuelExtra * 1.2;
}
if (siblingCount > 0) {
discount = fee * 0.1 * Math.min(siblingCount, 2);
if (fuelExtra > 200) discount += 50; // reads fuelExtra, writes discount
}
return Math.round(fee + fuelExtra - discount);
}Your tasks:
- Prove the tangle. Try to extract the AC-bus block and the sibling block with your IDE's Extract Method. Write down, for each attempt, how many parameters and return values the IDE demanded. This is the failure that justifies the heavy artillery.
- Write characterization tests. At least five: short distance, long distance with AC, three siblings, two days per week, and one combined everything case. Record the current outputs as the expected values. Green before you move anything.
- Apply the refactoring in the safe order. Create a
BusFeeCalculationclass; makedistanceKm,daysPerWeek,isACBus,siblingCountreadonly constructor fields; makefee,discount,fuelExtraprivate working fields; move the body intocompute(); makebusFeea one-line delegator. Stop and run all tests. - Decompose. Extract
baseFee(),applyFuelExtra(),applyACPremium(),applySiblingDiscount()— or better names if you find them — one extraction per test run. Notice how every extraction now needs zero parameters. - Hunt the temporal coupling. Deliberately swap two calls inside
compute()— say, apply the sibling discount before the AC premium — and watch which test fails. Write one sentence on why it failed. Then put the order back. This is the College corner lesson made physical: the fields gave you freedom, and the price of freedom is owning the order. - Reflect. Look at your final
compute(). Could a new teammate explain the school's bus pricing policy just by reading its four lines? If yes, the station is built, the shelves are labelled, and Salma Khala would hand you a plate of biryani herself.
Frequently asked questions
- When should I use Replace Method with Method Object instead of plain Extract Method?
- Try Extract Method first. Reach for a method object only when extraction keeps failing because every fragment reads and writes the same tangle of local variables, so each extracted piece would need a huge parameter list. The method object turns those locals into fields, and then extraction becomes easy.
- Why did Fowler rename this to Replace Function with Command?
- In the 2nd edition of Refactoring, Martin Fowler renamed it because the resulting object — one that wraps a single action you can configure and then run — is what he calls a command object, similar in spirit to the Command design pattern. The mechanics of the refactoring are the same as in the 1st edition.
- Doesn't creating a whole class for one method add too much code?
- Yes, it adds a class, a constructor, and fields — that is the real cost. Fowler himself says he prefers a plain function the vast majority of the time. The method object earns its keep only when the method is too tangled to decompose any other way, or when you need extra powers like undo or step-by-step execution.
- Should the fields of a method object be private?
- Yes. The fields exist only so the compute method and its private helpers can share state without passing parameters. Nothing outside the class should read or write them. Keep the public surface tiny: a constructor and one method like compute() or run().
- Is a method object the same as the Command design pattern?
- They are close cousins. A method object wraps one computation with its working state; the Command pattern wraps a request as an object so you can queue, log, or undo it. A method object often grows into a genuine Command or Strategy once it exists, which is one of the bonuses of this refactoring.
Further reading
Related Lessons
Extract Method: Turn One Giant Function into Small Named Helpers
Learn Extract Method step by step. Pull a messy block out of a long function, give it a clear name, and make your code read like a clean to-do list.
Split Temporary Variable: One Bucket Cannot Do Two Jobs
Learn Split Temporary Variable with a two-buckets story, TypeScript and C# examples, and safe steps. Give every variable one job and one clear, honest name.
Replace Temp with Query: Ask Fresh, Don't Trust Yesterday's Chit
Learn Replace Temp with Query with a canteen story, TypeScript and C# examples, and safe steps. Turn local variables into reusable methods, one source of truth.
Substitute Algorithm: Take the New Straight Road to School
Learn the Substitute Algorithm refactoring with a cycle-route story, TypeScript and Python examples, and the test-first safety rules every beginner needs.