Code smellCouplers
Feature Envy
A method reads and computes on another object's data more than its own. Move the logic to where the data lives.
Signs
- A method calls several getters on one other object and few or none on
this. - The method's result depends almost entirely on another type's fields.
- You see the same chain of reads on that other object in several places.
- The method's name mentions the other type:
computeInvoiceTotalliving inReportService. - Changing the other type's fields forces edits in this method, never the reverse.
Why it happens
- Data and behavior started in different places. Someone made a plain record type and wrote the logic in a service next to the code that first needed it.
- Moving a field to a new class left behind the methods that used it.
- A layered architecture rule ("models hold data, services hold logic") was applied mechanically, so every calculation sits away from its inputs.
- Each new caller copied the calculation instead of asking the data owner for it.
Example
A shipping service computes everything about a parcel from the parcel's own fields.
class Parcel {
constructor(
public lengthCm: number,
public widthCm: number,
public heightCm: number,
public weightKg: number,
) {}
}
class ShippingService {
billableWeight(p: Parcel): number {
const volumetric = (p.lengthCm * p.widthCm * p.heightCm) / 5000;
return Math.max(p.weightKg, volumetric);
}
isOversized(p: Parcel): boolean {
return Math.max(p.lengthCm, p.widthCm, p.heightCm) > 120;
}
quote(p: Parcel, ratePerKg: number): number {
return Math.ceil(this.billableWeight(p)) * ratePerKg;
}
}billableWeight and isOversized touch only Parcel. They belong there.
class Parcel {
constructor(
readonly lengthCm: number,
readonly widthCm: number,
readonly heightCm: number,
readonly weightKg: number,
) {}
billableWeight(): number {
const volumetric = (this.lengthCm * this.widthCm * this.heightCm) / 5000;
return Math.max(this.weightKg, volumetric);
}
isOversized(): boolean {
return Math.max(this.lengthCm, this.widthCm, this.heightCm) > 120;
}
}
class ShippingService {
quote(p: Parcel, ratePerKg: number): number {
return Math.ceil(p.billableWeight()) * ratePerKg;
}
}quote stays in the service because the rate belongs to the service, not the parcel.
Treatment
- If the whole method works on the other object's data, use Move Method.
- If only part of the method envies the other object, first use Extract Method on that part, then move the extracted piece.
- If the method uses data from several objects, move it to the object whose data it uses most. Pass the rest in as arguments.
- If a field is read mostly by another class, consider Move Field instead of moving the methods.
Payoff
- Behavior sits next to the data it depends on, so a change to that data touches one class.
- Fewer getters leak internal fields; some can become private.
- Duplicate calculations across callers collapse into one method.
When to ignore
- The method deliberately sits apart from the data: Strategy and Visitor put behavior outside the data so it can vary independently.
- The data type is a DTO or wire format shared across services, and adding logic would couple consumers to it.
- The other type is generated code or a third-party class you cannot edit. See Incomplete Library Class.
- In a functional codebase, plain data plus free functions is the convention. Keep the functions in the module that owns the type rather than moving them into the type.
Agent checklist
- Count reads on
thisversus reads on the other object before moving anything. - Confirm the target class is editable and is not a shared DTO or generated type.
- Extract the envious fragment first if only part of the method qualifies.
- Leave a delegating method in the old place if public callers depend on it, and mark it for removal.
- Run the tests after the move and check no new circular import appeared.
Treatments
- Move MethodMove a method to the class whose data it uses most, and leave a delegating call or update the callers.
- Extract MethodMove a coherent fragment of a long function into its own well-named function and call it from the original place.
- Move FieldMove a field to the class that uses it most, updating every reader and writer to reach it there.
See also
- Inappropriate IntimacyTwo classes reach into each other's private fields and internals. Cut the back-and-forth by moving members or narrowing what each exposes.
- Data ClassA class holds fields and accessors but no behavior, while other classes reach in to do the work that belongs to it.
- Message ChainsClient code walks a chain like a.getB().getC().getD() to reach what it needs, coupling itself to every link in the path.
- Move MethodMove a method to the class whose data it uses most, and leave a delegating call or update the callers.
- Extract MethodMove a coherent fragment of a long function into its own well-named function and call it from the original place.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.