Skip to content
aipatterns.dev

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: computeInvoiceTotal living in ReportService.
  • 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.

before.ts
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.

after.ts
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 this versus 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

See also

Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.