Parallel Inheritance Hierarchies
Before you begin: Base classes, overriding methods, constructors, and decimal division.
Parallel Inheritance Hierarchies occur when adding a subclass to one family repeatedly requires adding a matching subclass to another. The second hierarchy may be repeating a classification that the first already expresses.
A useful question is: does the second subclass contribute genuinely different behavior, or merely choose its matching partner? When it only chooses a partner, composition can often replace the duplication. 1
Every vehicle needs its own trip class
Consider a toy journey calculator. Each vehicle supplies a fixed speed, and a trip calculates hours as distance divided by that speed. These are invented constant speeds, not real travel estimates; there are no stops or changing conditions.
Original version: two families grow together
public abstract class Vehicle
{
public abstract decimal Speed { get; }
}
public sealed class Bicycle : Vehicle
{
public override decimal Speed { get { return 20m; } }
}
public sealed class Train : Vehicle
{
public override decimal Speed { get { return 100m; } }
}
public abstract class Journey
{
public abstract decimal Hours(decimal distance);
}
public sealed class BicycleTrip : Journey
{
public override decimal Hours(decimal distance)
{
return distance / new Bicycle().Speed;
}
}
public sealed class TrainTrip : Journey
{
public override decimal Hours(decimal distance)
{
return distance / new Train().Speed;
}
}BicycleTrip and TrainTrip use the same formula. Their only meaningful difference is which vehicle they construct. Adding a boat would require a Boat and another nearly identical trip class.
class Vehicle:
def speed(self):
raise NotImplementedError
class Bicycle(Vehicle):
def speed(self):
return 20
class Train(Vehicle):
def speed(self):
return 100
class Journey:
def hours(self, distance):
raise NotImplementedError
class BicycleTrip(Journey):
def hours(self, distance):
return distance / Bicycle().speed()
class TrainTrip(Journey):
def hours(self, distance):
return distance / Train().speed()BicycleTrip and TrainTrip use the same formula. Their only meaningful difference is which vehicle they construct. Adding a boat would require a Boat and another nearly identical trip class.
Refactored version: a trip has a vehicle
public abstract class Vehicle
{
public abstract decimal Speed { get; }
}
public sealed class Bicycle : Vehicle
{
public override decimal Speed { get { return 20m; } }
}
public sealed class Train : Vehicle
{
public override decimal Speed { get { return 100m; } }
}
public sealed class Boat : Vehicle
{
public override decimal Speed { get { return 50m; } }
}
public sealed class Trip
{
private readonly Vehicle vehicle;
public Trip(Vehicle vehicle) { this.vehicle = vehicle; }
public decimal Hours(decimal distance)
{
return distance / vehicle.Speed;
}
}Trip trip = new Trip(new Boat());
Console.WriteLine(trip.Hours(40)); // 0.8Trip contains a vehicle reference and one calculation. Adding Boat does not require a BoatTrip. The original bicycle and train calculations still produce the same results through the new caller arrangement.
class Vehicle:
def speed(self):
raise NotImplementedError
class Bicycle(Vehicle):
def speed(self):
return 20
class Train(Vehicle):
def speed(self):
return 100
class Boat(Vehicle):
def speed(self):
return 50
class Trip:
def __init__(self, vehicle):
self.vehicle = vehicle
def hours(self, distance):
return distance / self.vehicle.speed()trip = Trip(Boat())
print(trip.hours(40)) # 0.8Trip contains a vehicle reference and one calculation. Adding Boat does not require a BoatTrip. The original bicycle and train calculations still produce the same results through the new caller arrangement.
Notice the limit of the lesson
For these fixed speeds, even the vehicle subclasses might be unnecessary: a vehicle value containing a speed could suffice. We retain the first family to make the redundant second family easy to see. The example assumes positive vehicle speeds and nonnegative distances.
Two hierarchies are not automatically wrong. A new data type may genuinely require a new serializer, editor, or platform-specific implementation. Those pieces perform different work; their matching names alone do not establish waste.
Look for repeated classes that contain the same behavior and differ only in the partner they select. Passing that partner to one reusable class can let the families evolve independently—or eliminate a family that never had its own reason to exist.
Sources
- Martin Fowler, Refactoring, “Parallel Inheritance Hierarcharchies” in the first-edition code-smells discussion.
- Martin Fowler — Replace Subclass with Delegate, for the related move from subclass selection to contained objects. The journey example is original.