Skip to main content
CleanCodeMastery

Remove Assignments to Parameters: Never Scribble Over a Borrowed Notebook

Learn the Remove Assignments to Parameters refactoring with a borrowed notebook story, TypeScript and C# examples, and safe steps for beginners to follow.

29 min read Updated June 11, 2026beginner
refactoringremove assignments to parameterscomposing methodsclean codesplit variableparameters

๐Ÿ““ The Story of the Borrowed Notebook

Meera of class eight missed two days of school because of fever. Her best friend Arjun, who sits next to her, lends her his science notebook on Friday. "Take it home," he says, "copy the notes, and return it on Monday. And Meera โ€” my baba checks this notebook, so please keep it neat."

Meera sits at her study table that evening with a cup of Horlicks. Now think about two very different ways she can use that notebook.

The first way is the honest way. She keeps Arjun's notebook open on the left. She keeps her own notebook open on the right. She reads from his pages and writes into hers. When she finds a small mistake in his notes โ€” he wrote that sound travels faster than light, the silly fellow โ€” she corrects it in her notebook, not in his. Maybe she puts a tiny pencil dot in the margin to ask him about it on Monday. On Monday she returns his notebook exactly as he gave it. And here is the quiet superpower of this way: if she ever doubts her copy โ€” "did I copy that circuit diagram correctly?" โ€” she can open his original again and check. The original is always there, untouched, the final judge of every doubt.

The second way is the lazy and dangerous way. Meera is tired, her own notebook is somewhere in her bag, and Arjun's notebook is right there, open. So she starts scribbling directly inside it. She crosses out his lines, squeezes her own words between his sentences, and overwrites his diagrams with her "improved" versions. By page ten, the notebook is a mess. It is no longer Arjun's notes, and it is not fully her notes either. It is a confusing khichdi of both. And here is the worst part: the original is gone. If she made a copying mistake on page three, there is nothing left to compare against. When her mother asks, "is this what the teacher actually taught?", Meera honestly cannot say. On Monday, Arjun's face falls, and his baba's face falls further.

The simple house rule that every grandmother already knows: never overwrite what was given to you. Write your own work in your own notebook.

Now hold that picture, because methods in code receive borrowed notebooks every single day. They are called parameters. A parameter is a value that the caller hands to your method, just like Arjun handing over his notebook. When your method starts assigning new values to that parameter โ€” price = price * 0.9 โ€” it is scribbling over the borrowed notebook. The original input is destroyed, and anyone reading the method later cannot tell what the caller actually sent. The fix is exactly the house rule: copy the value into your own local variable and do all your scribbling there. That fix has a name: Remove Assignments to Parameters.

Figure 1: Meera's two ways with the borrowed notebook โ€” scribbling versus copying

๐Ÿ” What is Remove Assignments to Parameters?

Remove Assignments to Parameters is a small refactoring from the Composing Methods family. The recipe is short:

  1. You find a method that assigns a new value to one of its own parameters somewhere in its body.
  2. You create a local variable, copy the parameter's value into it, and give it a clear name.
  3. From that point on, all the changing and computing happens on the local variable. The parameter itself is never written to again.

The parameter stays what it should always be: a faithful, read-only record of what the caller gave us. The local variable becomes your own notebook, where you are free to scribble.

Why does this matter so much? Because good code gives every name one stable meaning. When a method begins, price means "the price the caller passed in." If line 7 says price = price - 25, then from line 8 onward price secretly means something else โ€” "the discounted price." Same name, two meanings, and the switch happens silently in the middle. A reader at the bottom of the method has no idea which meaning is active without scrolling up and replaying every line in their head. That is a heavy tax to charge every future reader for the sake of saving one variable declaration.

๐Ÿ’ก

One line to remember: a parameter is a borrowed notebook โ€” read it as much as you like, but never write in it. If you need to compute something, copy the value into your own local variable first.

There is a second, sneakier reason. In languages like TypeScript, C#, Java, and Python, objects are passed by "reference value." Two very different actions then look confusingly similar:

  • order = new Order() โ€” this rebinds the parameter. It is local. The caller never notices.
  • order.cancel() โ€” this mutates the object. The caller absolutely notices, because it is the caller's own object.

If your team's habit is "we never assign to parameters," then any line that does affect the caller must be a mutation like order.cancel(), which is much easier to spot in review. Removing parameter assignments removes a whole category of "wait, does the caller see this or not?" confusion.

College corner: this rebind-versus-mutate confusion is really a question about parameter passing models, a classic exam topic. In strict pass-by-value, the method receives a copy of the value; reassigning the parameter can never touch the caller. In true pass-by-reference (C++ references, C# ref), the parameter is the caller's variable, so even reassignment is visible outside. Java, Python, JavaScript, and ordinary C# parameters use a third model often called call by sharing or "pass reference by value": the method gets a copy of a reference. Rebinding the copy (order = new Order()) changes only the copy, so the caller is safe; but calling a mutating method through that reference (order.cancel()) reaches the caller's actual object. Once you can state this distinction precisely, you understand why Remove Assignments to Parameters never changes behavior in these languages โ€” it only renames work that was always local. It also explains why the refactoring does not apply to C# ref and out parameters: there the model genuinely is pass-by-reference, and writing to the parameter is the declared contract.

A naming note from Fowler's books. In the first edition of Refactoring, Martin Fowler listed Remove Assignments to Parameters as its own named technique. In the second edition (2018), he merged it into a broader refactoring called Split Variable. The thinking is the same: whenever one name is forced to carry two different meanings, split it into two names. A parameter that gets reused as a working variable is simply the most common and most harmful version of that problem. So if you read the newer book or refactoring.com and cannot find this exact title, look under Split Variable โ€” you will find our notebook rule living happily inside it.

Figure 2: The whole refactoring on one page

๐Ÿ•‘ When do we need it?

You reach for this refactoring the moment you spot a line like param = something inside a method body. But here are the situations where the pain is sharpest:

  1. Long methods. In a Long Method, a reassigned parameter is poison. The reassignment happens on line 12, and the parameter is read again on line 78. Nobody reading line 78 remembers that the input was overwritten sixty lines earlier. If you plan to break the long method apart later with Extract Method, untangling parameter reassignments first makes the extraction far easier, because each piece you pull out will receive a value with one clear meaning.
  2. Debugging sessions. You set a breakpoint at the end of a method and inspect price. Is that the caller's price or the computed price? With a reassigned parameter, you cannot tell, and the original value is lost forever. With a separate local, both values sit side by side in the debugger โ€” Arjun's original on the left, Meera's copy on the right.
  3. Methods with vague names and many branches. When several if blocks each adjust the same parameter, the parameter becomes a running scratchpad. Each branch quietly changes its meaning. Splitting it into a well-named local like finalPrice or adjustedScore instantly documents what the method is building.
  4. Code review confusion. If reviewers keep asking "does this change what the caller passed?", that is your sign. Removing assignments to parameters makes the answer always "no" by construction.
  5. Preparing for stricter rules. Some teams turn on lint rules or compiler keywords (like final in Java) that forbid parameter assignment entirely. Doing this refactoring across the codebase is the path to switching those rules on.

One honest caution: this refactoring is about reassignment, not mutation. If the real problem is that the method changes the caller's object โ€” adds items to a passed-in list, edits fields of a passed-in customer โ€” that is a different issue with different fixes. Keep the two ideas separate in your head.

Figure 3: What actually confuses readers when a parameter is reassigned

To make the rebind-versus-mutate boundary crystal clear, here is a side-by-side table you can paste into your team's wiki:

Line of codeWhat it doesDoes the caller see it?Is it this refactoring's target?
order = new Order()Rebinds the parameter name to a new objectNo โ€” purely localYes โ€” replace with a local variable
price = price * 0.9Rebinds the parameter to a new numberNo โ€” purely localYes โ€” the classic scribble
order.cancel()Mutates the caller's objectYes โ€” the caller's object changedNo โ€” a separate concern entirely
items.push(gift)Mutates the caller's arrayYes โ€” the caller's array grewNo โ€” review it separately
result = 42 (C# out param)Writes the declared outputYes โ€” that is the whole contractNo โ€” excused by design

๐Ÿ‘€ Before and after at a glance

Here is a small but typical example in TypeScript. A function computes the delivery charge for an online order. Watch how the parameter charge gets scribbled over:

// BEFORE: the parameter is overwritten โ€” the borrowed notebook is ruined
function deliveryCharge(charge: number, distanceKm: number, isFestival: boolean): number {
  if (distanceKm > 20) {
    charge = charge + 40;        // scribble 1
  }
  if (isFestival) {
    charge = charge * 1.5;       // scribble 2
  }
  if (charge > 200) {
    charge = 200;                // scribble 3 โ€” cap the charge
  }
  return charge;                 // what does "charge" even mean here?
}

By the return, the name charge has meant three different things at different moments. The caller's original base charge is gone. Now the refactored version:

// AFTER: the parameter stays untouched; the local does the work
function deliveryCharge(charge: number, distanceKm: number, isFestival: boolean): number {
  let finalCharge = charge;      // copy into our own notebook
 
  if (distanceKm > 20) {
    finalCharge = finalCharge + 40;
  }
  if (isFestival) {
    finalCharge = finalCharge * 1.5;
  }
  if (finalCharge > 200) {
    finalCharge = 200;
  }
  return finalCharge;            // clearly the computed result
}

Nothing about the behavior changed. But now charge means "the base charge the caller sent" on every single line, and finalCharge means "the answer we are building." Two names, two meanings, zero confusion. If a bug report comes in โ€” "festival charge looks wrong" โ€” you can log both charge and finalCharge and instantly see the input and the output side by side.

Figure 4: Before, one name carries two meanings; after, each name carries one

It also helps to see the call as a conversation. The caller lends a value; the method copies it, works on the copy, and hands back an answer โ€” the borrowed value itself comes through untouched:

Figure 5: The caller lends a value; the method works on its own copy

๐Ÿชœ Step-by-step, the safe way

This is one of the gentlest refactorings in the whole catalog, but we still do it in tiny verified steps, like good refactoring discipline demands.

โš ๏ธ

Before you touch anything, run your tests and make sure they are green. Refactoring means changing structure without changing behavior, and the only honest proof of "behavior unchanged" is a passing test suite before and after every small step. No tests? Write a couple of quick ones for this method first โ€” even three or four input/output checks are enough of a safety net for this refactoring.

Let us walk through the delivery charge example one careful step at a time.

Step 1 โ€” Find the first assignment to the parameter. Scan the body. The first scribble is charge = charge + 40. Everything before that line still sees the caller's true value; everything after might not.

Step 2 โ€” Declare a local variable initialized from the parameter. Put it right at the top, and name it for the result it will hold, not for the input:

// INTERMEDIATE: local declared, but nothing uses it yet โ€” still compiles, tests still green
function deliveryCharge(charge: number, distanceKm: number, isFestival: boolean): number {
  let finalCharge = charge;      // new line โ€” harmless so far
  if (distanceKm > 20) {
    charge = charge + 40;
  }
  if (isFestival) {
    charge = charge * 1.5;
  }
  if (charge > 200) {
    charge = 200;
  }
  return charge;
}

Run the tests. Green, of course โ€” we added a variable nobody reads yet. That is the point: each step is so small it cannot break anything quietly.

Step 3 โ€” Replace each assignment to the parameter with an assignment to the local, one at a time. Change the first scribble, run tests. Change the second, run tests. Replace reads too, so the chain stays consistent:

// INTERMEDIATE: first branch converted, others still pending
function deliveryCharge(charge: number, distanceKm: number, isFestival: boolean): number {
  let finalCharge = charge;
  if (distanceKm > 20) {
    finalCharge = finalCharge + 40;   // converted
  }
  if (isFestival) {
    finalCharge = finalCharge * 1.5;  // converted
  }
  if (charge > 200) {                 // OOPS โ€” still reads the old name!
    charge = 200;
  }
  return charge;
}

Pause and look at that third branch. This is the classic mistake: we converted the writes but left a read pointing at the stale parameter. If you ran the tests now with a case like charge = 150, distanceKm = 25, isFestival = true, the cap check would test 150 instead of 285 โ€” and a good test catches it instantly. This is exactly why we keep the tests running between steps.

Step 4 โ€” Finish the conversion so the parameter is never written and never read after the copy point (except through the local):

// FINAL
function deliveryCharge(charge: number, distanceKm: number, isFestival: boolean): number {
  let finalCharge = charge;
  if (distanceKm > 20) {
    finalCharge = finalCharge + 40;
  }
  if (isFestival) {
    finalCharge = finalCharge * 1.5;
  }
  if (finalCharge > 200) {
    finalCharge = 200;
  }
  return finalCharge;
}

Run the whole suite. Green.

Step 5 โ€” Lock the door so the scribble cannot return. If your language or tooling supports it, make the rule automatic. In Java, mark the parameter final. In JavaScript and TypeScript projects, enable the ESLint rule no-param-reassign. Now if a future teammate types charge = ..., the tools shout before the code review even starts.

Figure 6: The safe loop โ€” tiny change, test, repeat

The method itself moves through a small set of clear states during this work. Naming the states helps you know exactly where you are if you get interrupted halfway:

Figure 7: States of the method during the refactoring

College corner: there is a deep compiler-theory reason why "one name, one meaning" feels so right. Optimizing compilers internally rewrite your code into SSA form โ€” Static Single Assignment โ€” where every variable is assigned exactly once, and each reassignment in your source becomes a brand-new versioned name (charge1, charge2, charge3). Compilers pay this cost because reasoning about values is dramatically easier when names never change meaning. When you apply Remove Assignments to Parameters, you are doing a tiny human-scale version of the same transformation, for human readers instead of optimizers. The same instinct powers functional languages like Haskell and Elm, where bindings are immutable by default, and the final/const/readonly keywords in mainstream languages. Immutability is not a fashion; it is the cheapest known way to make code easier to reason about.

๐Ÿงบ A bigger real-life example

Let us return to Arjun's notebook and put the whole story into code. Imagine a small study app that Meera uses. A friend shares their notes with her, and the app builds her personal study notes from them: it adds her own headings, fixes spellings, and appends her extra points. Here is a version written by someone who scribbles over borrowed notebooks:

interface NotePage {
  topic: string;
  lines: string[];
}
 
// BEFORE: 'sharedPages' is borrowed, but the function reassigns it freely
function buildMyNotes(sharedPages: NotePage[], myExtraPoints: string[], maxPages: number): NotePage[] {
  // "Trim" the borrowed notes โ€” the original array reference is overwritten
  sharedPages = sharedPages.slice(0, maxPages);
 
  // "Improve" them โ€” overwritten again with a transformed copy
  sharedPages = sharedPages.map((page) => ({
    topic: page.topic.toUpperCase(),
    lines: page.lines.map((line) => line.trim()),
  }));
 
  // Append my own points to the last page
  if (sharedPages.length > 0 && myExtraPoints.length > 0) {
    const last = sharedPages[sharedPages.length - 1];
    sharedPages[sharedPages.length - 1] = {
      topic: last.topic,
      lines: [...last.lines, ...myExtraPoints],
    };
  }
 
  return sharedPages;   // is this the friend's notes or mine? The name lies.
}

Run your eyes down this function. The name sharedPages starts as "the pages my friend shared." Two lines later it means "a trimmed copy." Three lines after that, "a trimmed and cleaned copy." By the end it means "my finished personal notes" โ€” while still wearing the friend's name. If you needed to log a comparison โ€” "show me what the friend sent versus what I produced" โ€” you could not, because the original reference was thrown away on the very first line.

Now the honest version. The borrowed notes stay borrowed; the work happens in a clearly named local:

// AFTER: the parameter keeps its meaning; 'myNotes' is our own notebook
function buildMyNotes(sharedPages: NotePage[], myExtraPoints: string[], maxPages: number): NotePage[] {
  let myNotes = sharedPages.slice(0, maxPages);
 
  myNotes = myNotes.map((page) => ({
    topic: page.topic.toUpperCase(),
    lines: page.lines.map((line) => line.trim()),
  }));
 
  if (myNotes.length > 0 && myExtraPoints.length > 0) {
    const last = myNotes[myNotes.length - 1];
    myNotes[myNotes.length - 1] = {
      topic: last.topic,
      lines: [...last.lines, ...myExtraPoints],
    };
  }
 
  return myNotes;   // honest name: this is my work, built from the shared pages
}

Read it again as a story. sharedPages is Arjun's notebook โ€” referred to once, never touched again. myNotes is Meera's notebook โ€” every change lands there. If a bug report says "the app is losing the friend's last page," you can now print sharedPages.length and myNotes.length next to each other and see the trimming in action. The original survived.

Notice one more subtle thing. The before version looked like it might be modifying the friend's data, because the friend's name was being assigned to over and over. In truth slice and map return new arrays, so the caller was safe all along โ€” but a reader had to know that and verify it. The after version does not need the reader to verify anything. The parameter is never on the left side of =, so the question never even arises. Good refactoring does not just fix code; it deletes whole categories of questions.

Here is the same after-design drawn as a tiny class picture โ€” the borrowed input flows in, the builder works on its own copy, and the two are never the same thing:

Figure 8: The borrowed pages and the built notes are separate things with separate names

๐Ÿ’ป The same refactoring in C#

The same disease and cure appear in C#. Here is a school fee calculator that scribbles over its parameter:

// BEFORE
public decimal MonthlyFee(decimal baseFee, Student student)
{
    if (student.HasSiblingInSchool)
        baseFee = baseFee * 0.85m;        // sibling discount โ€” parameter overwritten
 
    if (student.IsScholarshipHolder)
        baseFee = baseFee - 500m;         // scholarship โ€” overwritten again
 
    if (baseFee < 0m)
        baseFee = 0m;                     // never charge negative fees
 
    return baseFee;                       // 'baseFee' no longer means the base fee
}

The name baseFee is now a lie for most of the method. The refactored version:

// AFTER
public decimal MonthlyFee(decimal baseFee, Student student)
{
    var payableFee = baseFee;             // our own notebook
 
    if (student.HasSiblingInSchool)
        payableFee = payableFee * 0.85m;
 
    if (student.IsScholarshipHolder)
        payableFee = payableFee - 500m;
 
    if (payableFee < 0m)
        payableFee = 0m;
 
    return payableFee;
}

Two C#-specific notes are worth a minute of your attention:

  1. C# has no final keyword for parameters. Java lets you write int gamma(final int inputVal) and the compiler then forbids reassignment. C# has no direct equivalent for ordinary parameters, so the discipline is enforced by team convention and by code analyzers rather than the compiler itself.
  2. out and ref parameters are excused. When a method signature says void TryParse(string text, out int result), assigning to result is not a smell โ€” it is the entire contract. The caller explicitly asked the method to write a value back. This refactoring targets ordinary input parameters that quietly get hijacked as scratch variables, not parameters whose declared job is to carry output.

In Python the rule is the same habit: if a function receives price and you need a computed version, write final_price = price and work on that. Python has no constant parameters at all, so the convention carries all the weight:

# BEFORE: the parameter is hijacked as a scratch variable
def ticket_total(price, age, is_weekend):
    if age < 12:
        price = price * 0.5
    if is_weekend:
        price = price + 50
    return round(price)
 
# AFTER: the parameter keeps its meaning end to end
def ticket_total(price, age, is_weekend):
    payable = price
    if age < 12:
        payable = payable * 0.5
    if is_weekend:
        payable = payable + 50
    return round(payable)

๐Ÿงญ Is it worth the extra line?

Some students ask the fair question: "Sir, it is one extra line. Is it really worth it?" The honest answer is: it depends on how often the parameter is reassigned and how long the method is. A two-line method that reassigns once is a mild case; a sixty-line method that reassigns the same parameter in five branches is an emergency. The quadrant below is a quick way to place any method you meet:

Figure 9: Where does your method sit?

And the payoff is most visible in debugging. When the original input survives in the parameter, tracing a wrong value means reading two clearly named variables. When it does not, tracing means replaying the whole method in your head, line by line, to reconstruct what the caller must have sent:

Figure 10: Typical time to trace a wrong value during debugging

These are not laboratory-precise numbers โ€” they are the everyday experience of anyone who has chased a "festival charge looks wrong" bug at 6 p.m. The pattern is real: keeping the input alive cuts the search roughly in half or better, because half of every such investigation is just answering "what did the caller actually send?"

๐Ÿ› ๏ธ IDE support

There is no single "Remove Assignments to Parameters" button in most IDEs, because the change is mostly typing one new line and renaming a few others. But the tools around the refactoring are strong, and they matter even more for keeping the rule than for applying it:

  • IntelliJ IDEA / Android Studio (Java, Kotlin): there is a built-in inspection called Assignment to method parameter that highlights every offending line across the project. You can fix occurrences with the Extract Variable refactoring, and the quick-fix can declare parameters final for you. Turning the inspection severity up to "Warning" makes the smell visible in the editor gutter forever.
  • ESLint (JavaScript / TypeScript): the rule no-param-reassign flags any assignment to a parameter. With the option props: true it also flags mutation of parameter properties, which is an even stricter notebook rule. Many popular style guides ship with this rule switched on.
  • Visual Studio / Rider / ReSharper (C#): use Introduce Variable (Ctrl+R, V in Visual Studio) to create the local from the parameter expression, then let the rename refactoring update later uses. Roslyn analyzers are available that flag parameter reassignment if your team wants automatic enforcement.
  • All IDEs: the humble Rename refactoring is your best friend in step 3 โ€” once you create the local, renaming guarantees that every read after the copy point uses the new name, so you cannot accidentally leave a stale read behind like the one we caught in the intermediate step earlier.

The big idea: apply the refactoring by hand once, then let a linter or inspection make sure nobody ever has to apply it again.

โš–๏ธ Benefits and risks

BenefitsRisks and limits
Every parameter keeps one stable meaning โ€” "what the caller gave us" โ€” from the first line to the lastAdds one extra variable declaration; in a three-line method this can feel like ceremony
The original input stays available for logging, comparison, and debuggingThe new local must be named well; a lazy name like temp or p2 wastes the whole effort
Removes the confusion between rebinding a reference parameter (invisible to caller) and mutating the object (visible to caller)Does not fix object mutation โ€” if the method edits the caller's object, that problem remains and needs separate attention
Makes the later use of Extract Method easier, since each value has one clear roleDoes not apply to C# out/ref parameters, where assignment is the declared contract
Can be locked in permanently with final (Java) or no-param-reassign (ESLint), turning a habit into a guaranteeIn rare performance-critical numeric kernels, teams sometimes accept parameter reuse deliberately โ€” document it loudly if you do

The risk column is honestly quite mild โ€” this is among the safest refactorings that exist. The behavior of the method cannot change if you follow the steps, because you are only introducing a new name for work that was already happening. The real-world challenge is consistency: one cleaned method in a codebase of scribblers helps a little; a lint rule across the whole project helps enormously.

๐Ÿงช Which smells does it cure?

SmellHow this refactoring helps
Long MethodIn a long body, a reassigned parameter is a meaning-switch hidden in the noise. Splitting it into parameter + named local makes the long method readable and prepares it for Extract Method, because each extracted piece receives values with single, clear meanings
CommentsComments like // price is now the discounted price exist only to apologize for a name that changed meaning. A properly named local such as discountedPrice makes the comment unnecessary
Duplicate CodeWhen the original input survives in the parameter, helper logic that needs "the value as passed in" can simply read it โ€” instead of every caller re-deriving or re-passing the original, which breeds small duplications

It is a supporting refactoring more than a headline act: it rarely fixes a whole smell by itself, but it clears the ground so the bigger refactorings โ€” Extract Method, Replace Method with Method Object, Substitute Algorithm โ€” can be applied safely.

๐Ÿ“‹ Quick revision box

+------------------------------------------------------------------+
|        REMOVE ASSIGNMENTS TO PARAMETERS โ€” REVISION CARD          |
+------------------------------------------------------------------+
| Story    : Arjun's borrowed notebook โ€” read it, never write      |
|            in it; do your work in YOUR notebook (a local).       |
| Smell    : param = something   inside the method body.           |
| Fix      : let result = param;  then change only 'result'.       |
| Why      : one name = one meaning; original input survives;      |
|            no "did the caller see that?" confusion.              |
| Steps    : green tests -> add local copy -> convert one          |
|            assignment at a time -> tests after each -> lock      |
|            with final / no-param-reassign.                       |
| Excused  : C# out/ref params โ€” writing them IS the contract.     |
| Naming   : Fowler 2nd ed. folds this into "Split Variable".      |
| Not for  : object mutation โ€” that is a different problem.        |
+------------------------------------------------------------------+

โœ๏ธ Practice exercise

Time to write in your own notebook. Take this TypeScript function from a movie ticket app and refactor it yourself:

function ticketPrice(price: number, age: number, isWeekend: boolean, couponPercent: number): number {
  if (age < 12) {
    price = price * 0.5;            // child discount
  } else if (age >= 60) {
    price = price * 0.7;            // senior discount
  }
  if (isWeekend) {
    price = price + 50;             // weekend surcharge
  }
  if (couponPercent > 0) {
    price = price - (price * couponPercent) / 100;
  }
  if (price < 0) {
    price = 0;
  }
  return Math.round(price);
}

Your tasks:

  1. Write four or five quick tests first. Cover at least: a child on a weekend, a senior with a coupon, an adult with no extras, and a coupon big enough to push the price below zero. Run them โ€” green.
  2. Apply Remove Assignments to Parameters in tiny steps: introduce a well-named local (think about the name โ€” payablePrice? finalPrice?), convert one assignment at a time, and run the tests after each conversion.
  3. Deliberately make the mistake we showed in the intermediate step โ€” convert the writes but leave one read of price behind โ€” and watch which test catches it. Feeling a test catch a real slip is the best way to believe in the safety net.
  4. Finish the refactoring, run all tests, and then add the ESLint rule no-param-reassign to the project so the scribble can never come back.
  5. Bonus thinking question: the parameter price and the local you created both exist now. Which one would you print in a log line that says "customer was charged X"? Which one in a log line that says "base price for this show was X"? If both answers came to you instantly, you have understood the whole lesson.
  6. College corner challenge: write a one-paragraph answer to this viva question โ€” "Java is pass-by-value, yet a method can empty the list I passed in. Explain how both statements are true at once." If your answer uses the words copy of the reference, rebinding, and mutation correctly, you are ready for any interview version of this question.

Return the notebook the way Arjun gave it to you. Your future teammates โ€” and your future self โ€” will thank you.

Frequently asked questions

Is it an error to assign a new value to a parameter?
No, most languages allow it and the code still runs. It is a readability problem, not a compiler error. After the assignment, the parameter no longer holds what the caller sent, and readers get confused. We refactor to keep every name meaning one thing.
Does this refactoring change what the caller sees?
No. Reassigning a parameter inside a method is local to that method in languages like TypeScript, C#, Java, and Python. The refactoring only renames work inside the method body, so behavior stays exactly the same. Your tests should pass before and after.
What is the difference between reassigning a parameter and mutating an object?
Reassigning means pointing the parameter name at a new value, like order = newOrder, which the caller never sees. Mutating means changing the object itself, like order.cancel(), which the caller does see. This refactoring removes reassignment only; mutation is a separate concern.
What happened to this refactoring in Fowler's second edition?
In the 2nd edition of Refactoring, Martin Fowler folded it into a broader technique called Split Variable. The idea is the same: when one name is forced to hold two meanings, split it into two names. A parameter used as a scratch variable is the most common case.
Does this apply to C# out and ref parameters?
No. With out and ref, assigning to the parameter is the whole point of the contract โ€” the caller expects the method to write a value back. This refactoring targets ordinary input parameters that quietly get overwritten and lose their original meaning.

Further reading

Related Lessons