Let small objects work together
Picture
An order calculates its items’ total. A receipt renderer formats that total. A checkout coordinates the two. Changing receipt wording should not change how the order adds prices.
Predict
Should checkout inherit from the receipt renderer just because it needs receipt output?
Walk through the small example
No. Checkout uses a renderer; it is not a substitutable kind of renderer. Holding a collaborator expresses the relationship directly.
Mental model and complete Python solution
Composition means building a behavior from cooperating objects. Order owns validated line items, TextReceipt owns presentation, and Checkout delegates to both. The purpose is to isolate independent changes, not to create one class per noun. A simpler function can remain a function when no useful state or boundary needs an object.
What the function promises: Prices use integer minor currency units, and quantities are positive integers. Checkout.receipt returns text. These examples do not process payments or calculate taxes.
class Order:
def __init__(self, items):
self._items = tuple(items)
if any(type(price) is not int or price < 0
or type(quantity) is not int or quantity <= 0
for price, quantity in self._items):
raise ValueError("Need nonnegative integer prices and positive quantities")
def total(self):
return sum(price * quantity for price, quantity in self._items)
class TextReceipt:
def render(self, order):
return f"Total: {order.total()} minor units"
class Checkout:
def __init__(self, order, renderer):
self.order = order
self.renderer = renderer
def receipt(self):
return self.renderer.render(self.order)
order = Order([(30, 2), (10, 1)])
assert order.total() == 70
assert Checkout(order, TextReceipt()).receipt() == "Total: 70 minor units"
assert Order([]).total() == 0
Work and storage: Total calculation takes O(number of line items). Stored items take linear space. Separating formatting does not change that complexity.
When to recognize it: Two responsibilities change for different reasons. Let one object use another through a small boundary.
Check before applying it: Composition is not automatically better than inheritance. Use inheritance when a genuine substitutable relationship makes the contract clearer.
Change one rule
Every order is immutable and total is requested thousands of times. What design choice becomes available?
See what changes
Calculate and store the total once during construction. If items later become editable, either update the cached total through controlled operations or invalidate it. Caching introduces a consistency responsibility.
Connect
Objects and responsibilities: Give each object a small promise first.
Swap the rule, not the whole feature: Swap one behavior while keeping its owner unchanged.