Skip to content

Archive

Refactoring

58 articles
Software Engineering 11 Sep 2026 9 min read

Tell, Don’t Ask: Keep Decisions with the Object That Owns the Data

Tell, Don’t Ask: Keep Decisions with the Object That Owns the Data A class can expose perfectly reasonable getters and still make a codebase harder to change. The trouble appears when callers fetch several values, interpret them, make a business decision, and then tell the object how to update itself. The data lives in one place, but the rule that gives the data meaning lives somewhere else. Tell, Don’t Ask is a design guideline for reducing that split. Instead of asking an object for internal state so a caller can decide what should happen, prefer telling the object the meaningful operation you want performed. The object can then apply the rules that belong with its state.

Software Engineering 11 Sep 2026 8 min read

Shotgun Surgery: When One Change Touches Many Files

Shotgun Surgery: When One Change Touches Many Files A pricing rule changes from “free shipping above 50” to “free shipping above 75.” The rule is simple, but the pull request touches checkout, order previews, invoice generation, customer notifications, and tests in several modules. Miss one place and customers see contradictory behavior. That pattern is called shotgun surgery: one logical change requires many small edits scattered through the codebase. The problem isn’t the number of files by itself. The problem is that one decision has many owners.

Software Engineering 11 Sep 2026 10 min read

Shotgun Surgery: Reduce Scattered Change

Shotgun Surgery: Reduce Scattered Change A small requirement can produce a surprisingly large patch. Adding one order state means editing validation, formatting, notification, audit, and reporting code in different modules. Each edit may be simple, yet missing one can leave the system inconsistent. This recurring shape is often called shotgun surgery: one conceptual change forces small edits across many places. The practical problem isn’t the number of files by itself. It is that a single responsibility has been scattered, so developers must reconstruct the full change surface each time that responsibility evolves.

Software Engineering 11 Sep 2026 7 min read

Replacing Type Branches with Polymorphism

Replacing Type Branches with Polymorphism A type-based switch can be perfectly clear. Trouble starts when the same cases appear across several operations. Adding one new type then means editing pricing, validation, formatting, scheduling, and other branches in separate places. The type list has become a change axis, but the code still represents it as scattered conditionals. Replacing type branches with polymorphism moves behavior for each variant behind a shared contract. Callers ask for an operation without selecting the implementation themselves. This can concentrate related rules and reduce repeated branching, but it also introduces more types and indirection. The refactoring pays off only when that trade is useful.

Software Engineering 11 Sep 2026 8 min read

Replacing Inheritance with Delegation

Replacing Inheritance with Delegation A class needs three useful methods from another class, so extending that class seems convenient. Months later, the subclass also inherits methods it shouldn’t expose, depends on initialization details it doesn’t control, and breaks when the superclass changes an internal assumption. The problem isn’t inheritance itself. The problem is using an is-a relationship to obtain code reuse when the real relationship is uses-a. Replacing inheritance with delegation makes that relationship explicit: the object keeps a collaborator and forwards only the behavior it actually needs.

Software Engineering 11 Sep 2026 8 min read

Replace Nested Conditionals with Guard Clauses

Replace Nested Conditionals with Guard Clauses A method often starts simple and becomes deeply nested one condition at a time. A null check wraps a permission check. That wraps a state check. The actual operation ends up several indentation levels from the method boundary, even though it is the main path a reader cares about. Guard clauses handle exceptional or disqualifying conditions near the top of a method and exit immediately. The remaining code can then describe the normal path with less structural noise.

Software Engineering 11 Sep 2026 8 min read

Replace Magic Numbers with Named Domain Values

Replace Magic Numbers with Named Domain Values A condition such as attempts >= 5 is easy to execute and surprisingly hard to review. Why five? Is it a security policy, a technical limit, a temporary experiment, or just an arbitrary value copied from somewhere else? The code contains the number but not the reason it exists. A magic number is a numeric literal whose meaning isn’t clear from its context. Replacing one well means more than moving it into a constant. The goal is to preserve the value’s meaning, units, and ownership so a future change has an obvious place to happen.

Software Engineering 11 Sep 2026 8 min read

Replace Data Clumps with Parameter Objects

Replace Data Clumps with Parameter Objects A method takes startDate, endDate, and timezone. Another method takes the same three values. A third passes them unchanged to a lower layer. Soon, changing what “reporting period” means requires editing signatures across the codebase. This is a common design smell called a data clump: several values repeatedly appear together because they are really parts of one concept, but the code still treats them as unrelated pieces. A parameter object gives that concept a name and a boundary.

Software Engineering 11 Sep 2026 7 min read

Middle Man Code Smell: Remove Delegation That Adds No Value

Middle Man Code Smell: Remove Delegation That Adds No Value Delegation is useful when one object asks another object to do work. It can separate responsibilities and keep implementation details behind a boundary. But delegation can become ceremony when an object does little more than forward most calls to another object. That situation is commonly called the Middle Man code smell. The extra layer adds names, files, navigation, and maintenance work without owning a meaningful decision.

Software Engineering 11 Sep 2026 9 min read

Feature Envy: Move Behavior Closer to the Data It Uses

Feature Envy: Move Behavior Closer to the Data It Uses A method can live in one class while spending most of its time inspecting another. It asks that other object for several values, combines them according to rules about that object, and perhaps repeats the same pattern elsewhere. The code works, but changing the data often means hunting down behavior in unrelated places. This is the design smell commonly called feature envy. The name matters less than the question behind it: does this behavior belong closer to the data and rules it depends on?

Software Engineering 11 Sep 2026 9 min read

Composed Method: Keep Each Routine at One Level of Abstraction

Composed Method: Keep Each Routine at One Level of Abstraction A routine becomes hard to read when it mixes two different jobs: describing a process and implementing every mechanical detail of that process. Consider an order checkout function that validates the cart, calculates a total, writes SQL, formats an email, and records metrics in one long block. A reader must move constantly between business intent and low-level mechanics. The code may be correct, yet its structure hides the story of the operation.

Software Engineering 10 Sep 2026 9 min read

Split Phase Refactoring to Separate Computation Stages

Split Phase Refactoring to Separate Computation Stages A function starts by interpreting input, then gradually accumulates validation, business rules, formatting, and output logic. None of those steps is necessarily complicated. The difficulty comes from having them interleaved: changing how input is interpreted can unexpectedly affect code that should only care about the interpreted result. Split Phase is a refactoring that separates one computation into distinct stages. The first phase produces an explicit intermediate result. The next phase consumes that result without needing to know how it was produced.

Software Engineering 10 Sep 2026 10 min read

Replace Primitive Obsession with Domain Types

Replace Primitive Obsession with Domain Types A customer ID, an email address, and a currency code can all be represented as strings. That doesn’t make them interchangeable. When a codebase treats every meaningful value as a generic string, integer, or boolean, callers have to remember rules that the type itself doesn’t express. This problem is often called primitive obsession: using general-purpose primitive values where the domain has a more specific concept. The practical fix isn’t to wrap every string in a class. It’s to introduce a domain type when doing so gives the program a useful place to enforce meaning and rules.

Software Engineering 10 Sep 2026 8 min read

Replace Conditional with Polymorphism When Branches Represent Types

Replace Conditional with Polymorphism When Branches Represent Types A conditional isn’t a design problem just because it has several branches. Sometimes if or switch is the clearest way to express a decision. Trouble starts when the same type-based decision appears in several places and every new variant requires editing all of them. Replace Conditional with Polymorphism is a refactoring that moves variant-specific behavior behind a common operation. Instead of asking an object what kind it is and then deciding what to do, callers ask it to perform the behavior directly.

Software Engineering 10 Sep 2026 7 min read

Reducing Change Amplification in Software Design

A small requirement arrives: add a new delivery status called delayed. The rule itself is simple, but implementing it means editing validation, display labels, notification logic, reporting code, and several tests in unrelated directories. Nothing is individually difficult. The risk comes from having to remember every place that represents the same idea. This is change amplification: one conceptual change requires many coordinated code changes. Some amplification is unavoidable, but repeated scattering is a useful design signal. Learning to recognize it helps you decide where a refactoring can make future changes smaller and less error-prone.

Software Engineering 10 Sep 2026 9 min read

Differential Testing for Behavior-Preserving Changes

Differential Testing for Behavior-Preserving Changes Replacing working code is risky when the requirement is “change the implementation, not the behavior.” A rewritten parser may accept a different edge case. A faster pricing engine may round one value differently. A new library may return the same records in a different order. Ordinary tests help, but they only cover cases and assertions someone thought to write. Differential testing adds another source of evidence: run the old and new implementations on the same inputs, compare their observable results, and investigate differences.

Software Engineering 10 Sep 2026 9 min read

Creating Seams to Test Hard-to-Change Code

Creating Seams to Test Hard-to-Change Code A method can contain a simple business rule and still be difficult to test. The difficulty often comes from everything attached to the rule: the current clock, a network client, a filesystem call, a global configuration object, or a constructor that creates its own dependencies. When rewriting the surrounding code would be risky, a seam can give you a smaller move. A seam is a place where you can change which behavior the code uses without changing the code that makes the business decision. In tests, that lets you replace an awkward dependency with controlled behavior.

Software Engineering 10 Sep 2026 8 min read

Composed Method: Keep Code at One Level of Abstraction

Composed Method: Keep Code at One Level of Abstraction A method can be only thirty lines long and still be difficult to read. The problem often isn’t its length. It is that the method keeps changing altitude: one line describes a business step, the next manipulates a collection, then another formats a storage key, and then the code returns to business logic. The Composed Method pattern addresses that problem by making a method read as a sequence of operations at roughly the same level of abstraction. The top-level method explains what happens. Smaller methods hold the details of how each step happens.

Software Engineering 09 Sep 2026 9 min read

Replacing Boolean Parameters with Explicit Choices

A call such as sendReport(report, true) may be perfectly valid code, yet it makes the reader stop. What does true mean? Send immediately? Include attachments? Compress the report? The answer exists somewhere in the called function’s contract, but it is not visible at the call site. Boolean parameters become a design problem when they represent an important choice between behaviors. They compress that choice into true or false, so callers must remember what each value means and the implementation often grows branches around the flag.

Software Engineering 09 Sep 2026 9 min read

Moving Behavior Toward the Data It Uses

A method can live in one class while doing most of its work with another class’s data. At first this may seem harmless: the code runs, the names are clear, and the calculation is short. Over time, however, the method often becomes a second place that knows how the other object works. That design smell is commonly called feature envy. A piece of behavior appears to “envy” another object’s features because it reads that object’s state or calls its methods much more than it uses its own.

Software Engineering 09 Sep 2026 8 min read

Finding Seams for Safer Code Changes

Sometimes a small code change feels much larger than the requirement. You want to test one decision, replace one dependency, or alter one behavior, but the code gives you no place to do that without executing or editing a large surrounding block. A useful way to reason about this problem is to look for a seam: a place where you can change the behavior of a program without editing the code that uses that behavior. A seam might be a function parameter, an object boundary, a configurable callback, or another point where one implementation can be substituted for another.

Software Engineering 09 Sep 2026 8 min read

Characterization Tests Before Changing Legacy Code

Changing unfamiliar code creates a difficult question: how do you know a refactor preserved behavior when nobody can state exactly what the current behavior is? Existing unit tests may be sparse. Documentation may describe the intended rules but not the edge cases the system actually implements. Some odd behavior may even have become a dependency for callers. A characterization test helps in this situation. Instead of starting from what the code ought to do, it records what the code does now for a carefully chosen input. That gives you a behavioral reference point before you change the implementation.

Software Engineering 08 Sep 2026 9 min read

Refactoring Feature Envy by Moving Behavior

A method belongs to InvoiceService, but most of its code asks an Invoice for fields, combines those fields according to invoice rules, and barely uses the service’s own state. Every time the invoice model changes, the service changes with it. This is a common design smell called feature envy: behavior lives in one place but depends heavily on the data or rules owned by another place. The name is less important than the maintenance problem. Knowledge that belongs together is split across a boundary, so one conceptual change requires coordinated edits.

Software Engineering 08 Sep 2026 8 min read

Keeping Behavior Close to the Data It Uses

A method can live in the wrong place even when its code is correct. One common sign is a method that repeatedly reads another object’s fields, interprets those values, and makes a decision that is really about that other object. This creates a maintenance problem. The data and the rules governing that data change for related reasons, but the code is stored in different places. A small change to the meaning of the data can then require developers to find and update distant callers.