Skip to content

Archive

Refactoring

58 articles
Software Engineering 07 Sep 2026 9 min read

Modeling Special Cases as Explicit Behavior

A system often starts with one ordinary case and one exception. A customer has a normal pricing plan unless the account is a guest. A shipment has a delivery date unless it is pickup-only. A report has an owner unless it is generated by the system. The first exception is usually harmless. The problem appears when the same check spreads through the code. Every caller asks whether it has the exceptional case before deciding what to do. Adding a new operation then means finding another place where the condition must be repeated.

Software Engineering 07 Sep 2026 9 min read

Evolving Data Shapes with Expand and Contract

A data change can look trivial in code and still be dangerous in a running system. Renaming a field, splitting one value into two, or changing a message shape may require several application versions, background jobs, and consumers to coexist while the change is in progress. The risky assumption is that the whole system changes at once. In practice, deployments take time, workers may finish old jobs after new code is live, and independently deployed consumers may upgrade later. If one release removes the old shape while something still depends on it, a locally correct change becomes a system failure.

Software Engineering 07 Sep 2026 8 min read

Adding Behavior Safely with Sprout Methods

A small feature can become a risky change when it must be added inside a large method that has weak tests. The obvious edit may be only ten lines, but those lines become mixed with old branches, mutable state, database calls, or other behavior you do not fully understand. Testing the new rule then requires exercising the entire method. One useful response is a sprout method: put the new behavior in a new, focused method and make the existing code call it. The old method changes only enough to connect the new behavior. The new method can often be tested directly with much less setup.

Software Engineering 06 Sep 2026 7 min read

Using Guard Clauses to Keep Control Flow Flat

A function often starts simple and becomes difficult to read as conditions accumulate. One check wraps another, the main work moves several indentation levels to the right, and a developer must remember which conditions are still true while reading the code. A guard clause handles a case that should stop or divert the current operation near the point where that case becomes known. Instead of wrapping the normal path in another conditional, the function deals with the exceptional, invalid, or inapplicable case and exits that path early.

Software Engineering 06 Sep 2026 9 min read

Replacing Branching with Lookup Tables

A chain of conditionals is not automatically a design problem. Sometimes each branch expresses genuinely different behavior, and an if or switch is the clearest way to show it. But another kind of branching appears when the logic is identical in every branch and only a value changes. That distinction matters. If the program is repeatedly asking, “Which constant belongs to this case?”, the conditionals are acting as a hand-written lookup mechanism. A lookup table can make the relationship explicit: keys represent cases, values represent the data associated with them.

Software Engineering 06 Sep 2026 8 min read

Replacing Behavior Switches with Polymorphism

A switch statement is not a design problem by itself. When a program has a small, stable set of cases, one explicit conditional can be easier to read than a hierarchy of types. Trouble starts when the same type distinction controls behavior in several places. Adding one new case then means finding every switch that knows about that type. Missing one produces a system where the new case works in some operations but not others.

Software Engineering 06 Sep 2026 8 min read

Grouping Related Parameters into a Parameter Object

A function can have individually reasonable parameters and still be difficult to use correctly. The problem often appears when several values travel together through many calls and only make sense as a group. Consider code that repeatedly passes startTime, endTime, and timezone. Each value has a clear type, but callers must remember their relationship: the end must not precede the start, and both timestamps are interpreted using the same timezone rule.

Software Engineering 05 Sep 2026 10 min read

Splitting Mixed Work into Explicit Phases

A function often starts with one job and gradually becomes a pipeline hidden inside a block of code. It reads raw input, interprets it, applies business rules, prepares parameters, performs an external action, and formats the result. Each step may be reasonable, but mixing them makes the whole function harder to understand and change. A useful response is Split Phase: separate work that happens for different reasons into explicit stages, and pass a meaningful result from one stage to the next.

Software Engineering 05 Sep 2026 9 min read

Replacing Type Conditionals with Polymorphism

A conditional is often the clearest way to express a small decision. Problems begin when the same type question spreads through a codebase. One function asks whether a notification is an email or SMS to format it. Another asks the same question to validate it. A third asks again to calculate delivery cost. Adding a new notification type then means finding and changing several unrelated switches. This is a useful signal for polymorphism: different implementations respond to the same operation according to their own behavior. The goal is not to remove every if or switch. The goal is to stop many callers from repeatedly deciding what an object is before they can decide what it does.

Software Engineering 05 Sep 2026 9 min read

Replacing Systems in Slices with the Strangler Pattern

A system can be difficult to change without being possible to replace in one safe step. The old application may serve real traffic, contain years of business rules, and depend on behavior that nobody has fully documented. A complete rewrite asks the team to reproduce all of that correctly before users receive any value from the new system. The strangler pattern takes a different approach: replace the system one well-defined slice at a time while the old and new implementations coexist.

Software Engineering 05 Sep 2026 7 min read

Replacing Repeated Type Conditionals with Polymorphism

A type-based switch can be the simplest way to express a small rule. The trouble starts when the same distinction appears in several places. Pricing checks whether an order is standard or express. Delivery estimates check the same thing. Cancellation rules do too. Adding a new order type then means finding every branch that knows the list of types. The problem is not the switch syntax itself. The problem is distributed knowledge: several callers know which variants exist and which behavior belongs to each one.

Software Engineering 05 Sep 2026 7 min read

Replacing Repeated Null Checks with Null Objects

Optional collaborators often begin harmlessly. A service may accept an optional audit recorder, notification sink, or metrics collector. Then every operation that uses the collaborator grows the same check: if audit != null: audit.record("order_created", order.id) One check is easy to understand. Dozens of checks create a different problem: knowledge that the collaborator may be absent is scattered through code that should be focused on other work. A missed check can fail at runtime, while slightly different checks can produce inconsistent behavior.

Software Engineering 05 Sep 2026 10 min read

Replacing Primitive Values with Domain Types

Many programs represent important concepts with ordinary strings, numbers, and booleans. That is convenient at first. A customer ID is a string, an email address is a string, and a quantity is an integer, so using those primitive types seems sufficient. The trouble starts when values that have different meanings share the same representation. A function can receive a product ID where it expected a customer ID. Validation rules spread across callers. A number that means cents can be confused with one that means whole currency units. The compiler or runtime may see perfectly valid primitives even though the program has made a domain mistake.

Software Engineering 05 Sep 2026 10 min read

Replacing Primitive Data with Domain Types

Many bugs begin with a value that is technically valid for its programming-language type but invalid for the job it represents. A quantity is negative. A percentage is passed as 20 where another function expects 0.20. Two string arguments are swapped because both look identical to the type system. The problem is not that strings and numbers are poor types. They are useful building blocks. The problem appears when a primitive value has accumulated rules or meaning that the primitive cannot express.

Software Engineering 05 Sep 2026 8 min read

Replacing Magic Values with Named Concepts

A literal value can be perfectly clear when it describes the mechanics of a calculation. index + 1 usually needs no explanation. But the same syntax becomes harder to understand when a value carries a business rule or engineering decision: if retries > 3, price * 0.85, or timeout = 30. The problem is not that numbers or strings appear in code. The problem is that some literals have meaning that the code does not name. These are often called magic values.

Software Engineering 05 Sep 2026 9 min read

Replacing Boolean Flag Parameters with Explicit Operations

A boolean parameter looks harmless because it carries only two values. The problem is that those values often control two different behaviours while saying almost nothing at the call site. Consider set_access(user, true). Does true mean grant access, require approval, make access permanent, or enable logging? A developer has to remember the parameter name or inspect the function before the call becomes clear. This problem becomes more expensive when the flag selects different validation, side effects, or failure rules. One function then behaves like two operations hidden behind a small parameter.

Software Engineering 05 Sep 2026 8 min read

Refactoring Tangled Methods with Method Objects

A long method is not automatically a design problem. Sometimes a calculation is easiest to understand when its steps stay together. Trouble starts when one method accumulates many temporary values, later steps depend on several earlier results, and extracting any part requires passing a long list of arguments. At that point, ordinary Extract Method refactoring can feel blocked by the method’s local state. A method object is one way through that problem: move the computation into a short-lived object, turn the important local variables into fields, and then extract parts of the computation into small methods on that object.

Software Engineering 05 Sep 2026 10 min read

Reducing Change Amplification by Keeping Decisions Together

A small requirement can produce a surprisingly large patch. Changing one pricing rule might require edits in an API handler, a validator, a report formatter, three tests, and a scheduled job. None of the edits is difficult by itself, yet missing one can leave the system inconsistent. This is change amplification: one conceptual change requires modifications in many places. The practical problem is not the number of files alone. It is that knowledge about one decision is scattered, so developers must rediscover every place that encodes it whenever the decision changes.

Software Engineering 05 Sep 2026 9 min read

Moving Behavior Closer to the Data It Uses

A function often starts in a reasonable place and becomes awkward as the system grows. It reads several fields from another object, interprets those fields, applies rules to them, and repeats the same pattern whenever a new requirement appears. The problem is not simply that the function is long. The deeper problem is responsibility placement: one part of the system owns the data, while another part knows too much about what that data means.

Software Engineering 05 Sep 2026 8 min read

Flattening Nested Conditionals with Guard Clauses

A function can be correct and still make its reader carry too much context. One common cause is deeply nested conditionals: before understanding the line in front of you, you must remember every condition that surrounds it. A guard clause handles a condition that prevents the main work from continuing, then exits the current operation early. Used carefully, guard clauses turn exceptional, invalid, or already-finished cases into short branches and leave the normal path at a shallower indentation level.

Software Engineering 05 Sep 2026 9 min read

Characterization Tests for Legacy Code

Refactoring unfamiliar code creates an uncomfortable problem: you want to improve the implementation, but you may not know which parts of its current behavior callers depend on. Existing documentation can be incomplete, and a thin test suite may describe only the obvious cases. A characterization test helps by recording what the software does now. Instead of starting from a specification of what the code should do, you exercise existing behavior, observe the result, and turn that observation into a test. The test then warns you when a later change alters that behavior.

Software Engineering 04 Sep 2026 8 min read

Replacing Long Parameter Lists with Parameter Objects

A function can become difficult to call correctly even when its implementation is simple. The problem often appears as a growing parameter list: several values travel together, callers must remember their order and meaning, and the same group is passed through multiple layers. One useful refactoring is a parameter object: a small type that groups parameters which belong to one concept. The goal is not to make a function signature shorter at any cost. The goal is to give related data a name, make invalid combinations harder to create, and give future changes a natural home.

Software Engineering 04 Sep 2026 8 min read

Removing Hidden Temporal Coupling

Some code works only when its methods are called in the right order. The individual methods may look valid, yet swapping two calls or forgetting an earlier call produces an error much later. This is temporal coupling: one operation depends on another operation having happened earlier. The coupling is not automatically a design flaw. Opening a transaction before committing it is a real lifecycle constraint. The problem is hidden temporal coupling, where the API allows an invalid sequence and leaves callers to remember an undocumented rule.

Software Engineering 04 Sep 2026 9 min read

Remove Duplicated Knowledge, Not Similar Code

Developers often learn the DRY principle as “do not repeat yourself.” That shorthand can lead to the wrong refactoring: two blocks look similar, so they are merged into one abstraction. Later, the two cases evolve for different reasons, and the shared abstraction fills with flags and exceptions. The more useful idea is narrower: avoid having the same piece of knowledge represented independently in multiple places. That distinction changes how you refactor. Similar code is only a clue. The real question is whether the copies encode one decision that must stay consistent, or different decisions that merely happen to look alike today.