Refactoring Catalog
A curated catalog of refactoring techniques from Martin Fowler’s Refactoring (2nd Edition). Each refactoring includes motivation, step-by-step mechanics, and examples.
“A refactoring is defined by its mechanics—the precise sequence of steps that you follow to carry out the change.” — Martin Fowler
How to Use This Catalog
- Identify the smell using the code smells reference
- Find the matching refactoring in this catalog
- Follow the mechanics step by step
- Test after each step to ensure behavior is preserved
Golden Rule: If any step takes more than 10 minutes, break it into smaller steps.
Most Common Refactorings
Extract Method
When to use: Long method, duplicate code, need to name a concept
Motivation: Turn a code fragment into a method whose name explains the purpose.
Mechanics:
- Create a new method named for what it does (not how)
- Copy the code fragment into the new method
- Scan for local variables used in the fragment
- Pass local variables as parameters (or declare in method)
- Handle return values appropriately
- Replace the original fragment with a call to the new method
- Test
Before:
function printOwing(invoice) {
let outstanding = 0;
console.log("***********************");
console.log("**** Customer Owes ****");
console.log("***********************");
// Calculate outstanding
for (const order of invoice.orders) {
outstanding += order.amount;
}
// Print details
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
}After:
function printOwing(invoice) {
printBanner();
const outstanding = calculateOutstanding(invoice);
printDetails(invoice, outstanding);
}
function printBanner() {
console.log("***********************");
console.log("**** Customer Owes ****");
console.log("***********************");
}
function calculateOutstanding(invoice) {
return invoice.orders.reduce((sum, order) => sum + order.amount, 0);
}
function printDetails(invoice, outstanding) {
console.log(`name: ${invoice.customer}`);
console.log(`amount: ${outstanding}`);
}Inline Method
When to use: Method body is as clear as its name, excessive delegation
Motivation: Remove needless indirection when the method doesn’t add value.
Mechanics:
- Check that the method isn’t polymorphic
- Find all calls to the method
- Replace each call with the method body
- Test after each replacement
- Remove the method definition
Before:
function getRating(driver) {
return moreThanFiveLateDeliveries(driver) ? 2 : 1;
}
function moreThanFiveLateDeliveries(driver) {
return driver.numberOfLateDeliveries > 5;
}After:
function getRating(driver) {
return driver.numberOfLateDeliveries > 5 ? 2 : 1;
}Extract Variable
When to use: Complex expression that is hard to understand
Motivation: Give a name to a piece of a complex expression.
Mechanics:
- Ensure the expression has no side effects
- Declare an immutable variable
- Set it to the result of the expression (or part)
- Replace the original expression with the variable
- Test
Before:
return order.quantity * order.itemPrice -
Math.max(0, order.quantity - 500) * order.itemPrice * 0.05 +
Math.min(order.quantity * order.itemPrice * 0.1, 100);After:
const basePrice = order.quantity * order.itemPrice;
const quantityDiscount = Math.max(0, order.quantity - 500) * order.itemPrice * 0.05;
const shipping = Math.min(basePrice * 0.1, 100);
return basePrice - quantityDiscount + shipping;Inline Variable
When to use: Variable name doesn’t communicate more than the expression
Motivation: Remove unnecessary indirection.
Mechanics:
- Check that the right-hand side has no side effects
- If variable isn’t immutable, make it so and test
- Find the first reference and replace with the expression
- Test
- Repeat for all references
- Remove the declaration and assignment
- Test
Rename Variable
When to use: Name doesn’t clearly communicate purpose
Motivation: Good names are crucial for clean code.
Mechanics:
- If variable is widely used, consider encapsulating
- Find all references
- Change each reference
- Test
Tips:
- Use intention-revealing names
- Avoid abbreviations
- Use domain terminology
// Bad
const d = 30;
const x = users.filter(u => u.a);
// Good
const daysSinceLastLogin = 30;
const activeUsers = users.filter(user => user.isActive);Change Function Declaration
When to use: Function name doesn’t explain purpose, parameters need change
Motivation: Good function names make code self-documenting.
Mechanics (Simple):
- Remove parameters not needed
- Change the name
- Add parameters needed
- Test
Mechanics (Migration - for complex changes):
- If removing parameter, make sure it’s not used
- Create new function with desired declaration
- Have old function call new function
- Test
- Change callers to use new function
- Test after each
- Remove old function
Before:
function circum(radius) {
return 2 * Math.PI * radius;
}After:
function circumference(radius) {
return 2 * Math.PI * radius;
}Encapsulate Variable
When to use: Direct access to data from multiple places
Motivation: Provide a clear access point for data manipulation.
Mechanics:
- Create getter and setter functions
- Find all references
- Replace reads with getter
- Replace writes with setter
- Test after each change
- Restrict visibility of the variable
Before:
let defaultOwner = { firstName: "Martin", lastName: "Fowler" };
// Used in many places
spaceship.owner = defaultOwner;After:
let defaultOwnerData = { firstName: "Martin", lastName: "Fowler" };
function defaultOwner() { return defaultOwnerData; }
function setDefaultOwner(arg) { defaultOwnerData = arg; }
spaceship.owner = defaultOwner();Introduce Parameter Object
When to use: Several parameters that frequently go together
Motivation: Group data that naturally belongs together.
Mechanics:
- Create a new class/structure for the grouped parameters
- Test
- Use Change Function Declaration to add the new object
- Test
- For each parameter in the group, remove it from the function and use the new object
- Test after each
Before:
function amountInvoiced(startDate, endDate) { ... }
function amountReceived(startDate, endDate) { ... }
function amountOverdue(startDate, endDate) { ... }After:
class DateRange {
constructor(start, end) {
this.start = start;
this.end = end;
}
}
function amountInvoiced(dateRange) { ... }
function amountReceived(dateRange) { ... }
function amountOverdue(dateRange) { ... }Combine Functions into Class
When to use: Several functions operate on the same data
Motivation: Group functions with the data they operate on.
Mechanics:
- Apply Encapsulate Record to the common data
- Move each function into the class
- Test after each move
- Replace data arguments with uses of class fields
Before:
function base(reading) { ... }
function taxableCharge(reading) { ... }
function calculateBaseCharge(reading) { ... }After:
class Reading {
constructor(data) { this._data = data; }
get base() { ... }
get taxableCharge() { ... }
get calculateBaseCharge() { ... }
}Split Phase
When to use: Code deals with two different things
Motivation: Separate code into distinct phases with clear boundaries.
Mechanics:
- Create a second function for the second phase
- Test
- Introduce an intermediate data structure between phases
- Test
- Extract first phase into its own function
- Test
Before:
function priceOrder(product, quantity, shippingMethod) {
const basePrice = product.basePrice * quantity;
const discount = Math.max(quantity - product.discountThreshold, 0)
* product.basePrice * product.discountRate;
const shippingPerCase = (basePrice > shippingMethod.discountThreshold)
? shippingMethod.discountedFee : shippingMethod.feePerCase;
const shippingCost = quantity * shippingPerCase;
return basePrice - discount + shippingCost;
}After:
function priceOrder(product, quantity, shippingMethod) {
const priceData = calculatePricingData(product, quantity);
return applyShipping(priceData, shippingMethod);
}
function calculatePricingData(product, quantity) {
const basePrice = product.basePrice * quantity;
const discount = Math.max(quantity - product.discountThreshold, 0)
* product.basePrice * product.discountRate;
return { basePrice, quantity, discount };
}
function applyShipping(priceData, shippingMethod) {
const shippingPerCase = (priceData.basePrice > shippingMethod.discountThreshold)
? shippingMethod.discountedFee : shippingMethod.feePerCase;
const shippingCost = priceData.quantity * shippingPerCase;
return priceData.basePrice - priceData.discount + shippingCost;
}Moving Features
Move Method
When to use: Method uses more features of another class than its own
Motivation: Put functions with the data they use most.
Mechanics:
- Examine all program elements used by method in its class
- Check if method is polymorphic
- Copy method to target class
- Adjust for new context
- Make original method delegate to target
- Test
- Consider removing original method
Move Field
When to use: Field is used more by another class
Motivation: Keep data with the functions that use it.
Mechanics:
- Encapsulate the field if not already
- Test
- Create field in target
- Update references to use target field
- Test
- Remove original field
Move Statements into Function
When to use: Same code always appears with a function call
Motivation: Remove duplication by moving repeated code into the function.
Mechanics:
- Extract the repeated code into a function if not already
- Move statements into that function
- Test
- If callers no longer need standalone statements, remove them
Move Statements to Callers
When to use: Common behavior varies between callers
Motivation: When behavior needs to differ, move it out of the function.
Mechanics:
- Use Extract Method on the code to move
- Use Inline Method on the original function
- Remove the now-inlined call
- Move extracted code to each caller
- Test
Organizing Data
Replace Primitive with Object
When to use: Data item needs more behavior than simple value
Motivation: Encapsulate data with its behavior.
Mechanics:
- Apply Encapsulate Variable
- Create a simple value class
- Change the setter to create a new instance
- Change the getter to return the value
- Test
- Add richer behavior to the new class
Before:
class Order {
constructor(data) {
this.priority = data.priority; // string: "high", "rush", etc.
}
}
// Usage
if (order.priority === "high" || order.priority === "rush") { ... }After:
class Priority {
constructor(value) {
if (!Priority.legalValues().includes(value))
throw new Error(`Invalid priority: ${value}`);
this._value = value;
}
static legalValues() { return ['low', 'normal', 'high', 'rush']; }
get value() { return this._value; }
higherThan(other) {
return Priority.legalValues().indexOf(this._value) >
Priority.legalValues().indexOf(other._value);
}
}
// Usage
if (order.priority.higherThan(new Priority("normal"))) { ... }Replace Temp with Query
When to use: Temporary variable holds result of an expression
Motivation: Make the code clearer by extracting the expression into a function.
Mechanics:
- Check that the variable is assigned only once
- Extract the assignment’s right-hand side into a method
- Replace references to the temp with the method call
- Test
- Remove the temp declaration and assignment
Before:
const basePrice = this._quantity * this._itemPrice;
if (basePrice > 1000) {
return basePrice * 0.95;
} else {
return basePrice * 0.98;
}After:
get basePrice() {
return this._quantity * this._itemPrice;
}
// In the method
if (this.basePrice > 1000) {
return this.basePrice * 0.95;
} else {
return this.basePrice * 0.98;
}Simplifying Conditional Logic
Decompose Conditional
When to use: Complex conditional (if-then-else) statement
Motivation: Make the intention clear by extracting conditions and actions.
Mechanics:
- Apply Extract Method on the condition
- Apply Extract Method on the then-branch
- Apply Extract Method on the else-branch (if present)
Before:
if (!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) {
charge = quantity * plan.summerRate;
} else {
charge = quantity * plan.regularRate + plan.regularServiceCharge;
}After:
if (isSummer(aDate, plan)) {
charge = summerCharge(quantity, plan);
} else {
charge = regularCharge(quantity, plan);
}
function isSummer(date, plan) {
return !date.isBefore(plan.summerStart) && !date.isAfter(plan.summerEnd);
}
function summerCharge(quantity, plan) {
return quantity * plan.summerRate;
}
function regularCharge(quantity, plan) {
return quantity * plan.regularRate + plan.regularServiceCharge;
}Consolidate Conditional Expression
When to use: Multiple conditions with the same result
Motivation: Make it clear that conditions are a single check.
Mechanics:
- Verify no side effects in conditions
- Combine conditions using
andoror - Consider Extract Method on the combined condition
Before:
if (employee.seniority < 2) return 0;
if (employee.monthsDisabled > 12) return 0;
if (employee.isPartTime) return 0;After:
if (isNotEligibleForDisability(employee)) return 0;
function isNotEligibleForDisability(employee) {
return employee.seniority < 2 ||
employee.monthsDisabled > 12 ||
employee.isPartTime;
}Replace Nested Conditional with Guard Clauses
When to use: Deeply nested conditionals making flow hard to follow
Motivation: Use guard clauses for special cases, keeping normal flow clear.
Mechanics:
- Find the special case conditions
- Replace them with guard clauses that return early
- Test after each change
Before:
function payAmount(employee) {
let result;
if (employee.isSeparated) {
result = { amount: 0, reasonCode: "SEP" };
} else {
if (employee.isRetired) {
result = { amount: 0, reasonCode: "RET" };
} else {
result = calculateNormalPay(employee);
}
}
return result;
}After:
function payAmount(employee) {
if (employee.isSeparated) return { amount: 0, reasonCode: "SEP" };
if (employee.isRetired) return { amount: 0, reasonCode: "RET" };
return calculateNormalPay(employee);
}Replace Conditional with Polymorphism
When to use: Switch/case based on type, conditional logic varying by type
Motivation: Let objects handle their own behavior.
Mechanics:
- Create class hierarchy (if not exists)
- Use Factory Function for object creation
- Move conditional logic into superclass method
- Create subclass method for each case
- Remove original conditional
Before:
function plumages(birds) {
return birds.map(b => plumage(b));
}
function plumage(bird) {
switch (bird.type) {
case 'EuropeanSwallow':
return "average";
case 'AfricanSwallow':
return (bird.numberOfCoconuts > 2) ? "tired" : "average";
case 'NorwegianBlueParrot':
return (bird.voltage > 100) ? "scorched" : "beautiful";
default:
return "unknown";
}
}After:
class Bird {
get plumage() { return "unknown"; }
}
class EuropeanSwallow extends Bird {
get plumage() { return "average"; }
}
class AfricanSwallow extends Bird {
get plumage() {
return (this.numberOfCoconuts > 2) ? "tired" : "average";
}
}
class NorwegianBlueParrot extends Bird {
get plumage() {
return (this.voltage > 100) ? "scorched" : "beautiful";
}
}
function createBird(data) {
switch (data.type) {
case 'EuropeanSwallow': return new EuropeanSwallow(data);
case 'AfricanSwallow': return new AfricanSwallow(data);
case 'NorwegianBlueParrot': return new NorwegianBlueParrot(data);
default: return new Bird(data);
}
}Introduce Special Case (Null Object)
When to use: Repeated null checks for special cases
Motivation: Return a special object that handles the special case.
Mechanics:
- Create special case class with expected interface
- Add isSpecialCase check
- Introduce factory method
- Replace null checks with special case object usage
- Test
Before:
const customer = site.customer;
// ... many places checking
if (customer === "unknown") {
customerName = "occupant";
} else {
customerName = customer.name;
}After:
class UnknownCustomer {
get name() { return "occupant"; }
get billingPlan() { return registry.defaultPlan; }
}
// Factory method
function customer(site) {
return site.customer === "unknown"
? new UnknownCustomer()
: site.customer;
}
// Usage - no null checks needed
const customerName = customer.name;Refactoring APIs
Separate Query from Modifier
When to use: Function both returns a value and has side effects
Motivation: Make it clear which operations have side effects.
Mechanics:
- Create a new query function
- Copy original function’s return logic
- Modify original to return void
- Replace calls that use return value
- Test
Before:
function alertForMiscreant(people) {
for (const p of people) {
if (p === "Don") {
setOffAlarms();
return "Don";
}
if (p === "John") {
setOffAlarms();
return "John";
}
}
return "";
}After:
function findMiscreant(people) {
for (const p of people) {
if (p === "Don") return "Don";
if (p === "John") return "John";
}
return "";
}
function alertForMiscreant(people) {
if (findMiscreant(people) !== "") setOffAlarms();
}Parameterize Function
When to use: Several functions doing similar things with different values
Motivation: Remove duplication by adding a parameter.
Mechanics:
- Select one function
- Add parameter for the varying literal
- Change body to use the parameter
- Test
- Change callers to use the parameterized version
- Remove now-unused functions
Before:
function tenPercentRaise(person) {
person.salary = person.salary * 1.10;
}
function fivePercentRaise(person) {
person.salary = person.salary * 1.05;
}After:
function raise(person, factor) {
person.salary = person.salary * (1 + factor);
}
// Usage
raise(person, 0.10);
raise(person, 0.05);Remove Flag Argument
When to use: Boolean parameter that changes function behavior
Motivation: Make the behavior explicit through separate functions.
Mechanics:
- Create explicit function for each flag value
- Replace each call with appropriate new function
- Test after each change
- Remove original function
Before:
function bookConcert(customer, isPremium) {
if (isPremium) {
// premium booking logic
} else {
// regular booking logic
}
}
bookConcert(customer, true);
bookConcert(customer, false);After:
function bookPremiumConcert(customer) {
// premium booking logic
}
function bookRegularConcert(customer) {
// regular booking logic
}
bookPremiumConcert(customer);
bookRegularConcert(customer);Dealing with Inheritance
Pull Up Method
When to use: Same method in multiple subclasses
Motivation: Remove duplication in class hierarchy.
Mechanics:
- Inspect methods to ensure they are identical
- Check signatures are the same
- Create new method in superclass
- Copy body from one subclass
- Delete one subclass method, test
- Delete other subclass methods, test each
Push Down Method
When to use: Behavior relevant only to a subset of subclasses
Motivation: Put method where it’s used.
Mechanics:
- Copy method to each subclass that needs it
- Remove method from superclass
- Test
- Remove from subclasses that don’t need it
- Test
Replace Subclass with Delegate
When to use: Inheritance is being used incorrectly, need more flexibility
Motivation: Prefer composition over inheritance when appropriate.
Mechanics:
- Create empty class for delegate
- Add field to host class holding delegate
- Create constructor for delegate, called from host
- Move features to delegate
- Test after each move
- Replace inheritance with delegation
Extract Class
When to use: Large class with multiple responsibilities
Motivation: Split class to maintain single responsibility.
Mechanics:
- Decide how to split responsibilities
- Create new class
- Move field from original to new class
- Test
- Move methods from original to new class
- Test after each move
- Review and rename both classes
- Decide how to expose new class
Before:
class Person {
get name() { return this._name; }
set name(arg) { this._name = arg; }
get officeAreaCode() { return this._officeAreaCode; }
set officeAreaCode(arg) { this._officeAreaCode = arg; }
get officeNumber() { return this._officeNumber; }
set officeNumber(arg) { this._officeNumber = arg; }
get telephoneNumber() {
return `(${this._officeAreaCode}) ${this._officeNumber}`;
}
}After:
class Person {
constructor() {
this._telephoneNumber = new TelephoneNumber();
}
get name() { return this._name; }
set name(arg) { this._name = arg; }
get telephoneNumber() { return this._telephoneNumber.toString(); }
get officeAreaCode() { return this._telephoneNumber.areaCode; }
set officeAreaCode(arg) { this._telephoneNumber.areaCode = arg; }
}
class TelephoneNumber {
get areaCode() { return this._areaCode; }
set areaCode(arg) { this._areaCode = arg; }
get number() { return this._number; }
set number(arg) { this._number = arg; }
toString() { return `(${this._areaCode}) ${this._number}`; }
}Quick Reference: Smell to Refactoring
| Code Smell | Primary Refactoring | Alternative |
|---|---|---|
| Long Method | Extract Method | Replace Temp with Query |
| Duplicate Code | Extract Method | Pull Up Method |
| Large Class | Extract Class | Extract Subclass |
| Long Parameter List | Introduce Parameter Object | Preserve Whole Object |
| Feature Envy | Move Method | Extract Method + Move |
| Data Clumps | Extract Class | Introduce Parameter Object |
| Primitive Obsession | Replace Primitive with Object | Replace Type Code |
| Switch Statements | Replace Conditional with Polymorphism | Replace Type Code |
| Temporary Field | Extract Class | Introduce Null Object |
| Message Chains | Hide Delegate | Extract Method |
| Middle Man | Remove Middle Man | Inline Method |
| Divergent Change | Extract Class | Split Phase |
| Shotgun Surgery | Move Method | Inline Class |
| Dead Code | Remove Dead Code | - |
| Speculative Generality | Collapse Hierarchy | Inline Class |
Further Reading
- Fowler, M. (2018). Refactoring: Improving the Design of Existing Code (2nd ed.)
- Online catalog: https://refactoring.com/catalog/