Change Bidirectional Association to Unidirectional
Before you begin: Objects, references, and collections.
Change Bidirectional Association to Unidirectional removes an unnecessary direction of navigation between objects. One object can still contain or reference another without the second object knowing its way back. 1
Original version: each track knows its album
A small export tool reads albums and their tracks. No export operation starts with a track and asks for its album, but the objects maintain that reverse reference anyway.
public sealed class Track
{
public string Title { get; }
public Album Album { get; }
public Track(string title, Album album) { Title = title; Album = album; }
}
public sealed class Album
{
public List<Track> Tracks { get; } = new List<Track>();
public Track Add(string title)
{
var track = new Track(title, this);
Tracks.Add(track);
return track;
}
}class Track:
def __init__(self, title, album):
self.title = title
self.album = album
class Album:
def __init__(self):
self.tracks = []
def add(self, title):
track = Track(title, self)
self.tracks.append(track)
return trackThe album refers to its tracks, and every track refers back. A track cannot even be constructed without supplying an album, despite the track’s export data consisting only of a title.
Refactored version: keep only the navigation that is used
public sealed class Track
{
public string Title { get; }
public Track(string title) { Title = title; }
}
public sealed class Album
{
public List<Track> Tracks { get; } = new List<Track>();
public Track Add(string title)
{
var track = new Track(title);
Tracks.Add(track);
return track;
}
}Album-to-track traversal and titles remain unchanged. Track no longer depends on the Album type.
class Track:
def __init__(self, title):
self.title = title
class Album:
def __init__(self):
self.tracks = []
def add(self, title):
track = Track(title)
self.tracks.append(track)
return trackThe exporter can still iterate over album.tracks. An individual track is simpler to construct and use outside an album.
Verify that “unused” really means unused
Search callers before removing the reverse property. Serialization, ORM mappings, templates, and reflection may use it even when ordinary method calls do not. The refactoring preserves the operations that never needed reverse navigation; it necessarily removes an API for those that did.
If one rare operation needs the parent, passing it explicitly may be sufficient. Repeated expensive searches for a parent can instead be evidence that the reverse association should remain.
This example deliberately leaves the track list mutable, as it was originally. Encapsulating that collection is a separate change. Removing a reverse reference does not automatically enforce ownership or prevent a track from appearing in multiple albums.
Sources
- Martin Fowler — Changes for the second edition of Refactoring, documenting the original association-refactoring pair.