cconnected.
Understand the pattern. Find your own way.Find your starting point
Your learning feed/Idea 157 of 218
Ideas this builds on
Low-level design157 / 218

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.