How do you carry out Extract Class, Move Function and Move Field across a large, actively-developed codebase without a risky big-bang change — and how do you know the extraction boundary is right?
answer
- Extract Class = repeated Move Field + Move Function
- Old class delegates while internals migrate
- Parallel change: expand → migrate → contract
- Seam evidence: field clustering, feature envy, co-change
- Beware reflection, serialisation, ORM and metric names
basics
~20 sDo it in small, always-green steps: create the new class, move one field or method at a time while the old class delegates to it, run the tests each time, and only remove the delegation once every caller has migrated. Never move everything at once.
solid answer
~60 sExtract Class is not an atomic operation; it is a sequence of Move Field and Move Function steps, each individually behaviour-preserving. Practical sequence: (1) pick the seam by looking at which fields and methods actually cluster — methods that use only subset A, fields touched only by subset A; (2) create the empty new class and give the old one a reference to an instance of it; (3) move one field, leaving an accessor on the old class that delegates, so all existing callers keep working; (4) move the methods that use it, again leaving delegating methods behind; (5) run tests after each move; (6) once nothing calls the delegating members, delete them and decide the new class's visibility — internal detail, or exposed and callers updated. Delegation is what makes each step safe and each commit revertible. Boundary evidence: field-usage clustering, methods that never touch the other cluster's state, Feature Envy (a method using another object's data more than its own), differing rates and reasons for change, and separate reasons to test. If you cannot name the extracted class in domain terms, the seam is probably wrong.
code
pseudocode · 13 lines// Extract Class via a delegation ladder — every step compiles and passes tests
class Person {
private phone = new TelephoneNumber() // 1. new class, held internally
// 2. field moved; old accessors delegate, so no caller changes yet
get officeAreaCode() { return this.phone.areaCode }
set officeAreaCode(v) { this.phone.areaCode = v }
// 3. method moved; stub delegates
telephoneNumber() { return this.phone.toString() }
}
// 4. migrate callers to use person.phone directly, one at a time
// 5. delete the delegating stubs only when nothing calls themgo deeper
Say that you move one field or method at a time, run the tests after each, and keep the old class working by having it call through to the new one.
Describe the delegation ladder concretely (new class held by the old, accessors delegate, methods move with stubs left behind, delete stubs last) and name Feature Envy as the trigger for Move Function.
Add how you choose the seam with evidence — field-usage clustering, differing reasons to change, testing pressure — plus parallel change for public APIs, separating refactoring from behaviour commits, and the hazards of reflection/serialisation for renames.
Frame it economically and organisationally: target co-change hotspots, keep every commit shippable so the work survives reprioritisation, use Branch by Abstraction at subsystem scale, add characterisation tests before touching thinly-covered code, watch identity/locking semantics when splitting state, and treat Inline Class as the normal reverse move when a split proves wrong.
### Vocabulary - **Move Function (Move Method)** — relocate a function to the class/module whose data it mostly uses. Trigger: **Feature Envy** — a method that calls another object's getters more than it touches its own state. - **Move Field** — relocate a data field to the class that uses it most, or that owns its invariants. - **Extract Class** — split one class doing two jobs into two classes. In practice it is *Move Field* + *Move Function* applied repeatedly, plus a new empty class to move things into. - **Inline Class** — the inverse: a class that no longer carries its weight is folded back into its only user. Extractions that turn out wrong are undone with this, which is why an over-eager extraction is recoverable rather than fatal. ### Finding the seam — evidence, not intuition 1. **Field-usage clustering.** For each method, list the fields it touches. Draw the bipartite graph. Real classes-in-waiting show up as connected components with few edges between them. This is mechanical and reviewable; cohesion metrics such as LCOM are a rough automated proxy — treat them as a hint, never a verdict. 2. **Feature Envy.** A method reaching repeatedly into another object's data belongs over there. 3. **Different reasons to change.** If persistence details change on a database upgrade while pricing rules change when marketing changes policy, they are separate responsibilities with separate change drivers — the practical reading of the Single Responsibility Principle. 4. **Different rates of change / different authors.** Version history is real evidence: run a co-change analysis over commits. Members that never change together are candidates for separation; members from different modules that always change together are a *missing* abstraction (and may indicate the split should go elsewhere). 5. **Naming test.** If the extracted part has an obvious domain name (`Address`, `PricingPolicy`, `RetryPolicy`), the seam is likely real. If the best you can do is `XxxHelper`, `XxxManager`, or `XxxUtils`, you have found a bag, not a concept, and the split will not pay off. 6. **Testing pressure.** If you keep wanting to test half the class without constructing the other half, that half wants to be its own class. ### Doing it safely — the delegation ladder The key technique is that **the old class keeps its interface and delegates** while internals migrate. Concretely: ``` class Person { private officeAreaCode, officeNumber // step 0: two fields that clump // step 1: new class, held by the old one private telephone = new TelephoneNumber() // step 2: move a field; keep the old accessor delegating -> nothing breaks get officeAreaCode() { return this.telephone.areaCode } set officeAreaCode(v) { this.telephone.areaCode = v } // step 3: move methods that use it, leaving delegating stubs behind telephoneNumber() { return this.telephone.toString() } } ``` At every point the system compiles and all tests pass. Callers migrate at their own pace. Only when a delegating member has no callers left do you remove it. In a shared or externally-consumed codebase, this is exactly **Parallel Change** (expand → migrate → contract), and the contract phase may be a separate release with a deprecation window. For cases where the extraction spans a subsystem rather than a class, the same idea scales up as **Branch by Abstraction**: introduce an abstraction over the current implementation, build the new implementation behind it, migrate consumers, then retire the old one — all on the mainline, no long-lived branch. Long-lived refactoring branches are the failure mode this exists to avoid: they rot against an actively-developed mainline and produce exactly the merge risk you were trying to prevent. ### Practical discipline - **Separate refactoring commits from behaviour commits.** A commit is either behaviour-preserving or behaviour-changing, never both. This keeps review honest, keeps `bisect` meaningful, and lets a risky behaviour change be reverted without losing the structural work. - **Use the tool's automated refactorings where they exist.** Automated Rename, Move, Extract Method and Change Signature are verified transformations across the whole project, including call sites the human eye misses. Where automation is unavailable (dynamic languages, reflection, string-based dependency injection, serialised field names, database column mappings, log-parsing consumers), rename and move become *risky* — a Rename Field that silently changes a serialised JSON key or an ORM column is a production incident, not a refactoring. - **Tests are the safety net, so check the net first.** Before a large extraction, verify coverage of the code being moved; if it is thin, add characterisation tests (tests that pin down current behaviour, whatever it is, without judging whether it is correct) first. - **Mind the invisible callers.** Reflection, dependency-injection by name, serialisation, public APIs, database mappings, dashboards and alerts that parse log/metric names. A refactoring is only behaviour-preserving with respect to observers you know about. - **Watch the concurrency and identity semantics.** Moving a field into a separate object changes object identity and can change locking granularity, `equals`/`hashCode` behaviour, and what a synchronised block actually protects. Splitting fields that were mutated together under one lock into two objects can introduce a race that no unit test will show. - **Know when to stop.** Extraction has a cost: more indirection, more navigation, more constructor wiring. If the extracted class has one method, no state and one caller, Inline Class it back. ### The strategic frame At principal level the interesting part is not the mechanics but sequencing and economics: refactor **where change is happening** (co-change hotspots × complexity), not uniformly; take **preparatory refactorings** immediately before a feature so the value is realised now and the risk is bounded by the feature's own testing; keep every step shippable so the work survives reprioritisation. "Make the change easy, then make the easy change" is the whole strategy in one line — and the corollary is that a refactoring programme that cannot be interrupted at any commit is badly structured.
- Why not do the whole extraction on a long-lived branch and merge once?Because the mainline keeps moving. A wide structural change touching many files conflicts with almost every other change, so the branch rots and the merge concentrates all the risk into one irreversible event. Delegation and parallel change keep the work on the mainline, shippable at every commit, and revertible in slices. Branch by Abstraction is the same idea at subsystem scale.
- What evidence from version-control history helps locate a good extraction boundary?Co-change analysis. Members that consistently change in the same commits belong together; members within one class that never co-change are candidates for splitting. Crossed with complexity and defect density it also tells you where refactoring pays off — hot, complex, frequently-edited code — rather than spending effort on stable code nobody touches.
- Which environments make Rename and Move genuinely risky rather than routine?Anywhere the name is data: reflection, string-keyed dependency injection, serialised field names in stored documents or wire formats, ORM column mappings, template/expression languages, and dashboards or alerts that match on log or metric names. In those cases the compiler and the IDE cannot see the call site, so you need a search-plus-migration strategy — often expand/contract with both names supported for a window.
- How do you decide an extraction has gone too far?When the extracted class has no state, one method and one caller, or when its name is a suffix like Helper/Manager rather than a domain concept, or when reading a single behaviour now requires opening four files. Inline Class is the sanctioned reverse move; the catalog runs both directions and reversing a bad split is normal engineering, not an admission of failure.
Rewiring a building floor by floor while people keep working, with temporary patch cables from the old sockets to the new panel. Nobody loses power at any moment, and you pull each patch cable only after everyone on that floor is on the new circuit.
saying these in an interview costs you the question
- Treating Extract Class as an atomic edit rather than a sequence of Move Field / Move Function steps each leaving the build green.
- Doing large structural refactoring on a long-lived branch and merging in one shot.
- Mixing behaviour changes into refactoring commits, which destroys reviewability and bisectability.
- Assuming automated Rename is always safe, ignoring reflection, DI-by-name, serialised keys, ORM mappings and log/metric-name consumers.
- Refactoring uniformly across the codebase instead of targeting change hotspots where the payoff is realised.
- Naming the extracted class `SomethingManager`/`SomethingHelper` and treating the split as done — a bag is not a concept.
- Ignoring that splitting fields that were mutated together under one lock can change locking granularity and introduce races.