Refactoring techniqueOrganizing Data
Change Unidirectional Association to Bidirectional
Add a back-reference when each of two classes needs to navigate to the other, and keep both sides in sync through one owner.
Problem
A Project knows its Tasks, but a Task does not know its project. Code that holds a task and needs the project has to search all projects, or every caller has to pass the project alongside the task.
Solution
Add a back-reference field on the other class, and make one side responsible for updating both links. Clients change the association only through that controlling side.
Before
class Task {
constructor(readonly id: string, public title: string) {}
}
class Project {
private tasks: Task[] = [];
constructor(readonly id: string, readonly ownerId: string) {}
add(task: Task): void { this.tasks.push(task); }
has(task: Task): boolean { return this.tasks.includes(task); }
}
function canEdit(task: Task, userId: string, projects: Project[]): boolean {
const project = projects.find((p) => p.has(task));
return project?.ownerId === userId;
}After
class Task {
private _project: Project | null = null;
constructor(readonly id: string, public title: string) {}
get project(): Project | null { return this._project; }
/** Only Project calls this. */
_setProject(project: Project | null): void { this._project = project; }
}
class Project {
private tasks = new Set<Task>();
constructor(readonly id: string, readonly ownerId: string) {}
add(task: Task): void {
task.project?.remove(task);
this.tasks.add(task);
task._setProject(this);
}
remove(task: Task): void {
if (this.tasks.delete(task)) task._setProject(null);
}
}
function canEdit(task: Task, userId: string): boolean {
return task.project?.ownerId === userId;
}Why
- Navigation from task to project is O(1) and needs no extra parameters.
canEditno longer takes the whole project list.- One controlling side keeps the two links consistent.
Steps
- Add the back-reference field on the class that lacks it.
- Choose the controlling side. In one-to-many associations it is usually the "one" side (
Project). In many-to-many, pick either. - On the non-controlling side, add a narrow setter for the back-reference that only the controller calls.
- Update the controller's modifying methods to set the back-reference. Handle moving an object from one owner to another. Run the tests.
- Replace lookups and extra parameters in clients with the new navigation.
Watch out
- Bidirectional links double the bookkeeping. A bug in sync leaves the graph inconsistent, which is hard to debug.
- They create reference cycles. Garbage-collected languages cope; reference-counted ones (Swift,
Rcin Rust, C++shared_ptr) need a weak side. - Serializers that walk object graphs (
JSON.stringify, many ORMs) loop forever or fail on cycles. Exclude the back-reference. - Don't add one when the reverse navigation is rare. A lookup or a parameter is cheaper to maintain.
- The inverse is Change Bidirectional Association to Unidirectional.
Related
- Helps with Feature Envy and Message Chains that come from roundabout lookups.
- Inverse: Change Bidirectional Association to Unidirectional.
- Encapsulate Collection keeps clients from bypassing the controller.
Agent checklist
- Both directions of navigation are actually needed.
- Exactly one side controls the association; the other exposes only a narrow setter.
- Moving an item between owners updates all three objects.
- Serializers and reference-counted memory were checked for cycle problems.
- Tests cover add, remove and move.
Treats these smells
See also
- Change Bidirectional Association to UnidirectionalRemove a back-reference that no code needs any more, so two classes stop depending on each other.
- Encapsulate CollectionStop a getter from handing out a mutable internal collection; return a read-only view and add methods to add and remove items.
- Move MethodMove a method to the class whose data it uses most, and leave a delegating call or update the callers.
Original writing and examples. Topic coverage follows Refactoring.Guru, which explains this topic with its own illustrations.