Code smellCouplers
Inappropriate Intimacy
Two classes reach into each other's private fields and internals. Cut the back-and-forth by moving members or narrowing what each exposes.
- Also known as
- Inappropriate Closeness
Signs
- Class
Areads or writes fields ofBthat exist only forAto use, andBdoes the same toA. - Fields are made
public,internalor package-private "so the other class can get at them". - Two classes hold references to each other and call back and forth during one operation.
- A subclass depends on protected fields of its parent in ways the parent never documented.
- You cannot change one class's internals without reading the other class first.
Why it happens
- Two concepts started as one class and were split without deciding who owns what.
- A quick fix needed one value, so a field became visible instead of a method being added.
- A bidirectional association was added for convenience and both sides started using it.
- Inheritance was used to share fields, so the child grew dependent on the parent's private layout.
Example
An Order and its Invoice edit each other's state directly.
class Order {
items: { sku: string; price: number; qty: number }[] = [];
invoice?: Invoice;
_discountApplied = false; // only Invoice uses this
}
class Invoice {
total = 0;
constructor(public order: Order) {
order.invoice = this;
}
compute(): void {
let sum = 0;
for (const i of this.order.items) sum += i.price * i.qty;
if (sum > 10000 && !this.order._discountApplied) {
sum *= 0.95;
this.order._discountApplied = true;
}
this.total = sum;
}
}Invoice walks Order's item list and flips a flag that only exists for it. Order keeps a back-reference it never uses.
type LineItem = { sku: string; price: number; qty: number };
class Order {
private readonly items: LineItem[] = [];
add(item: LineItem): void {
this.items.push(item);
}
subtotal(): number {
return this.items.reduce((s, i) => s + i.price * i.qty, 0);
}
}
class Invoice {
readonly total: number;
constructor(order: Order) {
const subtotal = order.subtotal();
this.total = subtotal > 10000 ? subtotal * 0.95 : subtotal;
}
}The discount flag disappears because the invoice computes the total once. The association now runs one way.
Treatment
- If one class uses another's fields to do work, use Move Method and Move Field until each field has one owner.
- If both classes share a cluster of state, use Extract Class to give that state its own home.
- If the link runs both ways but only one side needs it, use Change Bidirectional Association to Unidirectional.
- If a client reaches through one class into another, use Hide Delegate.
- If a subclass is too intimate with its parent, use Replace Inheritance with Delegation.
Payoff
- Each class can change its internals without breaking the other.
- Invariants are enforced in one place, so state stays consistent.
- The classes become testable on their own.
When to ignore
- The two types are one concept split across files for size reasons, such as a builder and the object it builds, or a class and its private nested helper. Close access is intended there.
- Languages offer scoped access for this case: Java package-private, C#
internal, Kotlininternal, Rustpub(crate). Tight coupling inside one module is acceptable when the module boundary is the real API. - Performance-critical code where an accessor layer measurably costs too much. Measure first.
Agent checklist
- List every member each class touches on the other and decide one owner per member.
- Check for a cyclic import between the two modules before and after the change.
- Reduce visibility of each field once no outside class reads it.
- Keep public behavior identical and run the tests after each move.
- Do not break up intentionally co-located types such as builders or nested helpers.
Treatments
- Move MethodMove a method to the class whose data it uses most, and leave a delegating call or update the callers.
- Move FieldMove a field to the class that uses it most, updating every reader and writer to reach it there.
- Extract ClassSplit a class that does two jobs by moving one job's fields and methods into a new class it holds a reference to.
- Hide DelegateAdd a method on the server object that forwards to its delegate, so clients stop navigating through it to reach the delegate.
- Change Bidirectional Association to UnidirectionalRemove a back-reference that no code needs any more, so two classes stop depending on each other.
- Replace Inheritance with DelegationTurn a subclass that uses only part of its superclass into a class that holds an instance of it and forwards just the calls it needs.
See also
- Feature EnvyA method reads and computes on another object's data more than its own. Move the logic to where the data lives.
- Message ChainsClient code walks a chain like a.getB().getC().getD() to reach what it needs, coupling itself to every link in the path.
- Middle ManA class mostly forwards calls to another object and adds nothing. Let clients talk to the real object or fold the wrapper away.
- Move MethodMove a method to the class whose data it uses most, and leave a delegating call or update the callers.
- Extract ClassSplit a class that does two jobs by moving one job's fields and methods into a new class it holds a reference to.
- Hide DelegateAdd a method on the server object that forwards to its delegate, so clients stop navigating through it to reach the delegate.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.