Code smellCouplers
Middle Man
A class mostly forwards calls to another object and adds nothing. Let clients talk to the real object or fold the wrapper away.
Signs
- Most public methods of a class are one line that calls the same field:
return this.repo.find(id). - Adding a method to the delegate forces a matching forwarding method on the wrapper.
- The wrapper has no state of its own beyond the delegate reference.
- Reviewers ask what the class is for, and the answer is "it wraps X".
Why it happens
- Hide Delegate was applied to cure Message Chains, then kept being applied until the class became pure forwarding.
- Logic that once lived in the wrapper moved elsewhere, leaving the shell behind.
- An abstraction layer was added "in case we switch implementations", and the switch never came.
Example
A CustomerService exists only to pass calls to the repository.
interface Customer {
id: string;
email: string;
}
class CustomerRepository {
private rows = new Map<string, Customer>();
find(id: string): Customer | undefined {
return this.rows.get(id);
}
save(c: Customer): void {
this.rows.set(c.id, c);
}
delete(id: string): void {
this.rows.delete(id);
}
}
class CustomerService {
constructor(private readonly repo: CustomerRepository) {}
find(id: string) {
return this.repo.find(id);
}
save(c: Customer) {
this.repo.save(c);
}
delete(id: string) {
this.repo.delete(id);
}
}
// controller
const service = new CustomerService(new CustomerRepository());
service.save({ id: "c1", email: "a@example.com" });Every method forwards. Callers gain nothing by going through the service.
const customers = new CustomerRepository();
customers.save({ id: "c1", email: "a@example.com" });If one method in the service had real logic, such as normalizing the email before saving, keep that method and remove the rest. Callers then use the repository for plain reads and writes.
Treatment
- If most methods forward, use Remove Middle Man and let clients call the delegate.
- If only a few forwarding methods exist on an otherwise useful class, use Inline Method on those forwarders at their call sites.
- If the wrapper adds a little behavior and forwards the entire delegate interface, consider Replace Delegation with Inheritance, but only when the wrapper truly is a kind of the delegate.
Payoff
- Fewer types and fewer hops when reading the code.
- New delegate methods are usable at once, without wrapper edits.
- The remaining classes each have a real reason to exist.
When to ignore
- The wrapper is a deliberate seam: Proxy, Decorator, Adapter or Facade. These forward on purpose to add access control, caching, translation or simplification.
- The wrapper isolates a third-party library so the rest of the code does not import it. That boundary has value even when thin.
- The wrapper narrows a wide interface to the few methods clients may use.
- Removing it would reintroduce long Message Chains in client code.
Agent checklist
- Count forwarding methods versus methods with real logic before removing the class.
- Confirm the wrapper is not a named pattern or a dependency boundary that the team relies on.
- Check dependency injection wiring and mocks in tests that reference the wrapper type.
- Update every caller to the delegate, then delete the wrapper in a separate step.
- Run the tests and confirm no layering or lint rule forbids the new direct dependency.
Treatments
- Remove Middle ManLet clients call the delegate directly when a class does little more than forward calls to it.
- Inline MethodReplace calls to a function whose body is as clear as its name with the body itself, then delete the function.
- Replace Delegation with InheritanceWhen a class forwards nearly every call to one delegate it truly is a kind of, make it a subclass of that delegate and delete the forwarding.
See also
- Message ChainsClient code walks a chain like a.getB().getC().getD() to reach what it needs, coupling itself to every link in the path.
- Lazy ClassA class does too little to justify the file, name and indirection it costs; fold it into its caller or delete it.
- 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.
- Remove Middle ManLet clients call the delegate directly when a class does little more than forward calls to it.
- Inline MethodReplace calls to a function whose body is as clear as its name with the body itself, then delete the function.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.