Moving Features Between Objects — Middle Level¶
Real-world triggers, the order in which to apply, and the architectural considerations behind moving methods, fields, and classes around.
Table of Contents¶
- The order matters: extract before move
- Real-world triggers
- Move Method vs. Move Field — pick the simpler first
- Extract Class — the responsibility test
- Inline Class — when "lazy" is wrong
- Hide Delegate vs. Demeter's Train Wreck
- Bounded contexts and DDD aggregates
- Foreign Method / Local Extension across languages
- Comparison with Composing Methods
- Review questions
The order matters: extract before move¶
A typical 600-line class has four problems tangled together:
- Some methods belong elsewhere (Move Method).
- Some fields belong elsewhere (Move Field).
- There's a hidden second class trying to escape (Extract Class).
- There's a method chain you keep using (Hide Delegate).
The recommended order:
- Extract Method (from Composing Methods) — break long methods into named, isolated fragments. Each fragment becomes a candidate for Move.
- Move Field for fields that obviously belong elsewhere — small, mechanical wins.
- Move Method on each fragment that's now obviously envious.
- Extract Class when a cohesive set of fields + methods has formed.
- Hide Delegate to clean up the call chains that result.
This is the order of the smallest reversible step at every moment.
Anti-pattern: trying to Extract Class on a 600-line class without first Extract Methods. You'll move giant methods that touch everything, and your "new class" will need 12 collaborators.
Real-world triggers¶
1. PR review feedback¶
A teammate says "this method doesn't belong here." That's Move Method. The cheapest answer is usually right: rename the home, not the method.
2. The "feature touched 5 files" PR¶
You needed to add a small feature, and the diff sprawled across 5 unrelated files. That's Shotgun Surgery — which often signals features need to be moved together into one class. Use Inline Class on a thin wrapper, or Extract Class to consolidate.
3. "Why does this class have 30 methods?"¶
Onboarding question with no satisfying answer. Run a method-clustering analysis (or just read by hand): which methods touch fields A,B,C? Which touch fields D,E? You've found Extract Class candidates.
4. "Class X is in every stack trace"¶
A god class that's always on the call path is doing too much. Extract Class until it isn't.
5. Circular references that won't go away¶
A → B and B → A means the boundary is wrong. One direction must give. Move methods/fields until the circular reference dissolves into a single direction (or use Change Bidirectional to Unidirectional).
Move Method vs. Move Field — pick the simpler first¶
If a method on Account reads interestRate from itself but conceptually belongs on AccountType, two routes exist:
Route A: Move Method first. - Now AccountType.method reads interestRate from... the parameter? Or, if interestRate was on Account, the method needs an Account argument. Awkward.
Route B: Move Field first. - AccountType.interestRate now exists. The original method (still on Account) reads type.interestRate(). It's now Feature Envy. - Then Move Method becomes obvious.
Rule: If the method follows the field, move the field first.
Likewise: when a method drags state with it, that state is often the next Move Field. Iterate.
Extract Class — the responsibility test¶
You're staring at a 30-method class. Should you Extract?
The "two reasons to change" test (SRP)¶
Read the methods. Group them by which business rule they encode. If two groups encode different business rules ("validation" + "pricing"), they belong in two classes.
The "field cluster" test¶
List every field. For each method, mark which fields it reads/writes. If methods M1, M2, M3 only touch fields F1, F2 (and methods M4, M5, M6 only touch F3, F4), the cluster is screaming for Extract.
The "name" test¶
If you can't name the class in 1–2 words, it has too many jobs. OrderService doing pricing, validation, and shipping wants to become OrderValidator, Pricer, and ShippingCalculator.
The "test setup" test¶
Tests that need to set up 12 collaborators are testing too much. Extract until each test sets up 2–3.
When NOT to Extract¶
- The "second responsibility" is too small to live alone. Wait — premature extraction makes you maintain two classes for one purpose.
- The "second responsibility" is one method. That's a function, not a class.
- The class is genuinely an aggregate root (DDD) — the methods orchestrate but don't implement; the implementation is on the leaves.
Inline Class — when "lazy" is wrong¶
Inline Class is the cure for Lazy Class, but be careful — what looks lazy may be load-bearing:
When Inline is right¶
- A wrapper around a single primitive that adds nothing (no validation, no formatting, no behavior).
- A class that's been left over after most of its responsibility was moved away.
- A speculative class that was created "in case we need it later" — and we didn't.
When Inline is wrong¶
- The class is a value object (Email, Money, PhoneNumber) — it encapsulates an invariant. Don't inline.
- The class is a port in hexagonal architecture — even if it's currently a thin pass-through, the port is the abstraction boundary.
- The class is mocked in tests — inlining breaks test setups.
- The class is part of a public API — external callers depend on it.
Heuristic¶
If the class has any non-trivial method (validation, formatting, defaulting), it's not lazy. The fields-only "data class" with getX/setX is the typical Inline candidate.
Hide Delegate vs. Demeter's Train Wreck¶
The Law of Demeter (LoD): a method should call methods on this, its own fields, its parameters, and locals it created — not on objects returned from those.
Train wreck¶
Each . is a step into another object. If any link in the chain changes (Department → Team), every caller breaks. Hide Delegate at each layer trims the chain.
When chains are OK¶
- Fluent APIs / Builders:
Order.builder().withItem(x).withCustomer(c).build()— every.returns the same builder. Not a train wreck. - Stream pipelines:
items.stream().filter(...).map(...).toList()— same object pattern. - Immutable transformations:
string.toLowerCase().trim().replace("a", "b")— each call returns a new String, but the type is consistent.
The train wreck is when each . returns a different concept: john → Department → Manager → Name → String. That's coupling across multiple boundaries.
Pragmatic rule¶
Hide a delegate when ignoring it would force every caller to know about an internal concept. Tolerate the chain when the intermediate types are obviously stable (Builders, Streams, value types).
Bounded contexts and DDD aggregates¶
In Domain-Driven Design, an aggregate is a cluster of objects that change together and have one root that outsiders talk to. Move Method, Move Field, and Extract Class are the day-to-day mechanics of aligning code with aggregates.
Example¶
A Cart aggregate might contain Cart, LineItem, DiscountRule. The root is Cart. Outside callers say cart.applyDiscount(rule), not cart.lineItems().get(0).applyDiscount(rule).
If you find code calling deep inside an aggregate, you have a Move Method opportunity (lift the operation to the root) and a Hide Delegate opportunity (stop exposing the leaf to outsiders).
Bounded context¶
When two parts of a system have inconsistent meanings of "Customer" (the marketing context vs. the billing context), they live in different bounded contexts. Code that conflates them is a candidate for Extract Class — let each context have its own Customer, with translation between.
Foreign Method / Local Extension across languages¶
| Language | Mechanism | Notes |
|---|---|---|
| Java | Static utility class (DateUtils.nextDay(d)) or wrapper class | No extension methods. Lombok's @ExtensionMethod is the closest. |
| Kotlin | fun Date.nextDay() | First-class extension functions; resolution is static. |
| C# | public static class DateExtensions { public static Date NextDay(this Date d) ... } | Extension methods, static dispatch. |
| Swift | extension Date { func nextDay() -> Date { ... } } | First-class extensions; can add protocol conformance. |
| Python | Subclass, monkey-patching, or free function | Monkey-patching is technically possible but discouraged. |
| Go | Method on a named local type (type MfDate time.Time) | No extensions; type aliases + methods cover most cases. |
| Rust | Trait + impl: impl DateExt for Date { fn next_day(&self) -> ... } | Trait extensions; static dispatch. |
| TypeScript | Module augmentation, but usually a free function | Augmentation works but is fragile. |
In modern languages with first-class extensions, Introduce Foreign Method and Introduce Local Extension collapse into the same idiom — you simply add the method.
In Java and Go, the wrapper/utility-class approach is required. The Result type pattern (io.vavr.Try, Java's Optional) is itself a kind of Local Extension.
Comparison with Composing Methods¶
| Aspect | Composing Methods | Moving Features |
|---|---|---|
| Scope | Inside one class | Across classes |
| Risk | Low (mechanical) | Medium (changes APIs) |
| IDE support | Excellent (refactor commands) | Good (but watch test setups) |
| Effect on architecture | Reveals shape | Realigns shape |
| Order | Apply first | Apply after Composing Methods |
The natural progression:
Compose Methods → see the shape → Move Features → realign the shape → Compose Methods again on the new homes → repeat.
This loop is the day-to-day practice of refactoring legacy code into clean architecture.
Review questions¶
- Why should you Extract Method (Composing Methods) before Move Method?
- What's the "two reasons to change" test for Extract Class?
- When is a class genuinely lazy vs. when does it look lazy but isn't?
- How does Hide Delegate relate to the Law of Demeter?
- When are method chains OK and when are they train wrecks?
- What's a DDD aggregate root, and how does it relate to Move Method?
- Compare extension methods in Kotlin vs. wrapper classes in Java for Local Extension.
- What's the typical pair of refactorings you alternate when restructuring legacy code?
- When does Move Field come before Move Method?
- When should you Remove Middle Man rather than Hide Delegate?