Composition over Inheritance
Shelfwise orders vary in two ways: shipping (standard, express, international) and packaging (plain, gift-wrapped). Someone started with subclasses: ExpressOrder, GiftOrder, then ExpressGiftOrder, InternationalGiftOrder… Add same-day shipping and you need two more classes. Add eco-packaging and you need four more. This lesson brings together three principles that stop that explosion.
The rule
Find what varies, hide it behind an abstraction, and plug it in.
It packs three principles into one move:
- Encapsulate what varies. Find the parts of the design that change independently: here, shipping and packaging.
- Program against abstractions. Give each varying part an interface, and let the rest of the code use only the interface.
- Composition over inheritance. Give the object those parts as components (fields), instead of creating a subclass for every combination.
class ShippingPolicy(ABC):
@abstractmethod
def cost(self, order): ...
class Packaging(ABC):
@abstractmethod
def fee(self, order): ...
class Order:
def __init__(self, lines, shipping: ShippingPolicy, packaging: Packaging):
self.lines = lines
self.shipping = shipping
self.packaging = packaging
def total(self):
return (self.subtotal()
+ self.shipping.cost(self)
+ self.packaging.fee(self))
order = Order(lines, shipping=ExpressShipping(), packaging=GiftWrap())Three shipping styles and two packaging styles are now five small classes, not six subclasses, and the gap widens with every option: inheritance multiplies classes, composition adds them. You can even swap a component at run time, say upgrading to express shipping after the order is placed, which a subclass can never do, because an object cannot change its class.
Inheritance is not banned. A true, stable "is a" (like EBook and Product) is still a fine use. Composition wins whenever you are modelling behaviours that vary and combine. It is also easier to test: hand an Order a FakeShipping that always costs 50, and the total can be checked without any real shipping rules.
The shortcut
When you are tempted to name a subclass with two or more adjectives (ExpressGiftOrder), each adjective is a separate thing that varies. Turn each into an interface and give the object one of each.
Spot the adjectives
Proposed:
UrgentSmsNotification,DailyEmailNotification,UrgentEmailNotification.
The trap is composing everything by reflex. A single fixed behaviour does not need an interface and an injected component. Wait until something really varies.
Drills
Answer fast — trust the rule.
Orders vary by 4 shipping styles and 3 packaging styles. How many classes do you need for all combinations with composition (not counting Order)?
What does "program against abstractions" mean for Order.total()?
A customer upgrades to express shipping after placing an order. Why is this easy with composition?
When is inheritance still a good choice?
Remember
- Find what varies, hide it behind an abstraction, and plug it in.
- Two adjectives in a subclass name mean two things that vary: one interface each.
- Trap: inheritance multiplies classes; composition adds them. But do not compose what never varies.
