Detailed reference for refactorings that redistribute responsibilities between classes. The fundamental question of object-oriented design is: where should this behavior live? These refactorings provide the mechanical steps to move things to the right place.
Move a method to the class it uses most. A method that accesses more features of another class than its own has Feature Envy and belongs somewhere else.
The most common reason for moving a method is Feature Envy -- when a method spends most of its time talking to another object. Moving the method reduces coupling: the method now lives where its data lives, so changes to that data don't ripple outward.
- Examine all features (fields and methods) used by the method. Determine which class has the most features used by the method.
- Check for related methods in the source class. If other methods also use the same target class, consider moving them together.
- Check superclasses and subclasses for overrides or related declarations.
- Declare the method in the target class. Copy the body and adjust references --
thisnow refers to the target; the source object may need to be passed as a parameter. - Turn the source method into a delegating method (call the target).
- Run tests.
- Consider removing the delegating method if no other callers need it.
- Run tests.
Before:
class Account {
overdraftCharge() {
if (this.type.isPremium()) {
let result = 10;
if (this.daysOverdrawn > 7) {
result += (this.daysOverdrawn - 7) * 0.85;
}
return result;
} else {
return this.daysOverdrawn * 1.75;
}
}
}The method depends heavily on this.type (an AccountType object). Move it there.
After:
class AccountType {
overdraftCharge(daysOverdrawn) {
if (this.isPremium()) {
let result = 10;
if (daysOverdrawn > 7) {
result += (daysOverdrawn - 7) * 0.85;
}
return result;
} else {
return daysOverdrawn * 1.75;
}
}
}
class Account {
overdraftCharge() {
return this.type.overdraftCharge(this.daysOverdrawn);
}
}Move a method when:
- It uses more fields/methods of another class than its own
- The target class is likely to change in ways that affect this method
- Related methods already live in the target class
Don't move when:
- The method uses features from multiple classes equally (keep it in the most stable location)
- Polymorphism on the source class is needed
Move a field to the class that uses it more. Similar to Move Method but for data.
A field used more by another class signals that the data model is out of alignment with the behavior model. Moving the field keeps data and behavior together.
- If the field is public, encapsulate it first (Encapsulate Field)
- Create the field in the target class with a getter and setter
- Determine how to reference the target from the source (usually an existing association)
- Update the source getter to delegate to the target
- Run tests
- Remove the field from the source class
- Run tests
Before:
class Customer:
def __init__(self):
self.discount_rate = 0.0
class Order:
def discounted_total(self):
return self.base_total() - (self.base_total() * self.customer.discount_rate)discount_rate is only read by Order through Customer. If most logic involving discount_rate lives in the customer's pricing context, keep it in Customer. But if Order is the primary consumer and discount_rate is really about order pricing policy, consider moving it.
Split a class that does two things into two classes that each do one thing.
A class with too many responsibilities grows too large and becomes hard to understand. If you can identify a coherent subset of fields and methods that relate to each other more than to the rest of the class, that subset deserves its own class.
- Identify the subset of responsibilities to split out
- Create a new class named after the split-out responsibility
- Add a link from the old class to the new class
- Use Move Field for each field in the subset
- Use Move Method for each method in the subset
- Review the interfaces of both classes. Remove unneeded methods, rename as appropriate.
- Decide whether to expose the new class or hide it behind the original
- Run tests
Before:
class Person {
constructor() {
this.name = '';
this.officeAreaCode = '';
this.officeNumber = '';
}
get telephoneNumber() {
return `(${this.officeAreaCode}) ${this.officeNumber}`;
}
}After:
class TelephoneNumber {
constructor() {
this.areaCode = '';
this.number = '';
}
toString() {
return `(${this.areaCode}) ${this.number}`;
}
}
class Person {
constructor() {
this.name = '';
this.telephoneNumber = new TelephoneNumber();
}
get telephone() {
return this.telephoneNumber.toString();
}
}| Signal | What to Extract |
|---|---|
Field name prefix groups (e.g., shippingStreet, shippingCity) |
ShippingAddress class |
| Methods that only use a subset of fields | The subset + its methods = new class |
| Subsets change at different rates | The faster-changing subset deserves its own class |
| Subsets have different collaborators | Each collaborator relationship = potential class boundary |
The inverse of Extract Class. Merge a class that no longer carries its weight back into another class.
A class that does too little -- perhaps after previous refactorings moved its responsibilities elsewhere -- adds complexity without value. Fold it back into the class that uses it.
- For each public method and field of the source class, create a corresponding member in the target class
- Change all references to the source class to use the target class instead
- Run tests
- Delete the source class
- Run tests
- The class has only one or two trivial methods
- The class was created by Extract Class but subsequent refactorings emptied it
- The class adds indirection without any logic, validation, or behavior of its own
Encapsulate the fact that one object delegates to another. Create a method on the server that hides the delegate from the client, enforcing the Law of Demeter.
When a client calls person.getDepartment().getManager(), the client knows about the Department class -- it's coupled to the navigation structure. If Department changes its interface, the client breaks. By adding person.getManager() (which internally calls department.getManager()), the client only knows about Person.
- For each method the client calls on the delegate, create a simple delegating method on the server
- Change the client to call the server method instead
- If no client needs the delegate accessor anymore, remove it
- Run tests
Before:
# Client code:
manager = person.department.managerAfter:
class Person:
@property
def manager(self):
return self.department.manager
# Client code:
manager = person.managerHiding every delegate leads to the Middle Man smell -- a class that does nothing but forward calls. The right balance:
| Situation | Action |
|---|---|
| Delegate's interface is unstable | Hide it (protect callers from change) |
| Client uses many delegate methods | Consider Hide Delegate for each |
| Server is becoming pure forwarding | Remove Middle Man |
| Chain is deep (a.b.c.d) | Definitely hide |
The inverse of Hide Delegate. When a class consists primarily of methods that delegate to another class, let the client call the delegate directly.
As a system evolves, more and more delegating methods accumulate until the "server" class adds no value -- it's just a pass-through. At that point, remove the indirection.
- Create a getter for the delegate on the server (if one doesn't exist)
- For each delegating method that adds no value, redirect the client to call the delegate directly
- Remove the delegating method from the server
- Run tests
Before:
class Person {
get manager() { return this.department.manager; }
get budget() { return this.department.budget; }
get headcount() { return this.department.headcount; }
get location() { return this.department.location; }
// ... 10 more forwarding methods
}After:
class Person {
get department() { return this._department; }
}
// Client:
const manager = person.department.manager;When a server class needs an additional method but you can't modify it (third-party library, frozen module), create the method in the client class and pass the server object as the first argument.
A utility method that "should" be on the server class but can't be added there. The foreign method is a workaround -- mark it as such, so if the server class is ever opened for modification, the method can be moved.
# Server class (third-party, can't modify):
# date = Date(year, month, day)
# Foreign method in client:
def next_day(date):
"""Foreign method -- should be on Date class."""
return Date(date.year, date.month, date.day + 1)When you need several foreign methods on a server class you can't modify, create a new class -- either a subclass or a wrapper -- that adds the missing methods.
| Approach | When to Use |
|---|---|
| Subclass | When you can subclass the server; simplest approach |
| Wrapper (Decorator) | When you can't subclass (final class); forward all original methods |
class EnhancedDate {
constructor(date) {
this._original = date;
}
// Forward original methods
getYear() { return this._original.getYear(); }
getMonth() { return this._original.getMonth(); }
// New methods
nextDay() {
return new EnhancedDate(
new Date(this._original.getTime() + 86400000)
);
}
isWeekend() {
const day = this._original.getDay();
return day === 0 || day === 6;
}
}Use these questions to decide whether and where to move code:
| Question | If Yes | Action |
|---|---|---|
| Does this method use more of another class's features? | Feature Envy | Move Method to that class |
| Is this field used more by another class? | Misplaced data | Move Field to that class |
| Does this class have two groups of fields that don't interact? | Multiple responsibilities | Extract Class |
| Is this class just a thin wrapper with no logic? | Unnecessary indirection | Inline Class |
| Is the client navigating through an object chain? | Tight coupling | Hide Delegate |
| Is this class just forwarding calls? | Middle Man smell | Remove Middle Man |
| Need to add a method to a class you can't modify? | Missing feature | Introduce Foreign Method or Local Extension |
When unsure where to put a method, ask: "If the data this method uses changes, which class should need to be updated?" The method belongs in that class. This keeps data and behavior together, minimizing the ripple effect of change.