Steven's Knowledge

Refactoring Techniques

A working set of mechanical refactorings, organized by purpose

Refactoring Techniques

A refactoring is a small, mechanical transformation that preserves behavior. The full catalog runs to dozens of named moves; this page covers the working set that returns the most value in day-to-day work.

For each technique: the situation it addresses, the move itself, and the inverse (which is also useful — refactoring often goes back and forth before settling).

Foundation

These five are used in almost every refactoring session.

Rename

Use when a name no longer describes its meaning, or when the codebase has settled on a different convention.

The simplest, safest, highest-return refactoring. Modern IDEs handle the mechanical work; the only real difficulty is choosing the better name.

Extract Function / Inline Function

Extract when a section of a function does something nameable and self-contained. Pull it into its own function; the new function's name documents the section's purpose.

Inline when a function body is as clear as its name, or when an extra layer of indirection adds nothing. The two operations are inverses; both are common.

Extract aggressively when introducing new structure. Inline aggressively when consolidating before re-extracting along different lines.

Extract Variable / Inline Variable

Extract a variable to give a name to an intermediate expression, especially in long conditionals or arithmetic.

Inline a variable when its name adds nothing — when the expression is short and the binding is used in only one place.

Encapsulate Variable

Use when a value is read or written from many places, and you want the option to change its representation, validate it, or trigger logic on access.

Replace direct access with an accessor pair. Once encapsulated, the underlying storage can change without touching call sites.

Change Function Declaration

Use when a function's parameters, return type, or name no longer match its purpose.

Mechanical, but high-impact: the signature is the contract. Splitting this from a behavior change keeps the diff small and reviewable.

Function Shape

Introduce Parameter Object

Use when several parameters consistently appear together. Group them into an object that names the cluster.

Beyond reducing the parameter count, this often reveals a missing domain concept: (startDate, endDate) becomes DateRange; (x, y, z) becomes Point3D.

Combine Functions into Class

Use when several functions share the same arguments and operate on related data. The arguments are an object trying to escape; promote them into one.

Remove Flag Argument

Use when a boolean parameter selects between two behaviors. Replace with two separate functions, each named for the variant it implements.

// Before
book(customer, /* isPremium */ true)

// After
bookPremium(customer)
bookStandard(customer)

Replace Temp with Query

Use when a temporary variable holds the result of a side-effect-free expression. Replacing the temp with a function call lets multiple methods share the computation and prepares the code for further extraction.

Split Phase

Use when a function does two things in sequence — for example, parse-then-process or compute-then-format. Split the function into two, each producing a named intermediate result.

This is one of the most reliable ways to take a long function with mixed concerns and produce two readable, independently testable pieces.

Class Structure

Extract Class

Use when a class describes more than one thing — the surest test is whether you can name it without "and."

Identify the cluster of fields and methods that go together; extract them into a new class with a name that captures the responsibility.

Inline Class

The inverse: when a class has shrunk until it no longer earns its own existence, fold it into its only caller.

Move Function / Move Field

Use when a function or field is more closely related to a class other than the one it currently lives in. Move it to where it belongs.

Often paired with Feature Envy smells: if OrderProcessor.totalForCustomer(customer) reads only customer data, the method belongs on Customer.

Hide Delegate

Use when callers reach through one object to another (order.getCustomer().getEmail()). Add a method on the intermediate object (order.getCustomerEmail()) and let it delegate internally.

Remove Middle Man

The inverse: when an object's interface consists almost entirely of pass-through methods, callers should talk directly to the underlying object.

Conditional Logic

Decompose Conditional

Use when a complex if/else mixes condition logic with branch logic.

// Before
if (date.before(SUMMER_START) || date.after(SUMMER_END)) {
  charge = quantity * winterRate + winterServiceCharge;
} else {
  charge = quantity * summerRate;
}

// After
charge = isSummer(date) ? summerCharge(quantity) : winterCharge(quantity);

The mechanical move is to extract each piece — the predicate and each branch — into a named function.

Replace Conditional with Polymorphism

Use when the same switch-on-type appears in several places. Replace the type code with subclasses (or a strategy interface); each branch becomes a method on the relevant subclass.

Do not apply when there is only one such switch — direct conditional code is more readable than a class hierarchy created for one decision point.

Replace Nested Conditional with Guard Clauses

Use when the body of a function is buried inside several layers of if. Convert the early-exit conditions to guards so the main flow stays at the top level.

// Before
function payAmount(employee) {
  let result;
  if (employee.isSeparated) {
    result = { amount: 0, reasonCode: 'SEP' };
  } else if (employee.isRetired) {
    result = { amount: 0, reasonCode: 'RET' };
  } else {
    // long calculation
    result = ...;
  }
  return result;
}

// After
function payAmount(employee) {
  if (employee.isSeparated) return { amount: 0, reasonCode: 'SEP' };
  if (employee.isRetired)   return { amount: 0, reasonCode: 'RET' };
  // long calculation
  return ...;
}

Introduce Special Case (Null Object)

Use when the same null check is repeated in many places. Replace null with a special-case object that responds sensibly to all the relevant queries.

Inheritance and Composition

Pull Up Method / Push Down Method

Move a method up to a superclass when subclasses share it; move it down when only one subclass needs it. The corresponding Pull Up Field / Push Down Field does the same for state.

Replace Inheritance with Delegation

Use when a subclass uses only part of its parent, or when the inheritance relationship turns out to be "uses" rather than "is-a."

Replace the inheritance with a field holding the former superclass. This loses the implicit interface but gains the freedom to reshape both sides.

Replace Constructor with Factory Function

Use when construction logic is more complex than a constructor should handle, or when callers should be able to receive different concrete types.

A factory function can return subclasses, perform validation, look up cached instances, or dispatch to alternative implementations — all without callers needing to know.

Large-Scale Moves

These do not happen in one sitting; they unfold over days or weeks.

Branch by Abstraction

Use when replacing one implementation with another in a long-lived codebase. Introduce an abstraction over the old implementation, route all callers through it, build the new implementation behind the same abstraction, switch traffic, then retire the old code.

The benefit is that the system is always running and always shippable during the transition.

Strangler Fig

Use when rewriting a system rather than a module. Build the replacement around the existing system, redirect functionality to the replacement piece by piece, and retire the original when nothing is left calling it.

The complementary patterns are documented at length in Refactoring (2nd ed.) and Working Effectively with Legacy Code; both reward careful reading.

Sequencing

A useful order when approaching unfamiliar code:

  1. Rename until the names match what the code does.
  2. Extract Variable to capture intermediate values worth naming.
  3. Extract Function to name sequences of operations.
  4. Split Phase to separate logically distinct stages.
  5. Move Function / Field to relocate behavior to where the data lives.
  6. Extract Class when responsibilities have become visible.
  7. Replace Inheritance with Delegation or other large-scale moves last.

The discipline is to do the cheap, low-risk moves first. Most large refactorings start to suggest themselves naturally after the first three steps clarify the structure.

On this page