PYTH-270 · Lesson 5 of 12 · 25 min

Structural patterns

▶ Video coming soon A video version of this lesson is on the way.

Structural patterns are about how objects and classes are combined into larger structures, and in particular about joining pieces that were not designed to fit together. Ridgeline meets this problem constantly. The green-coffee importer publishes prices in a format nobody at Ridgeline chose, the new payment provider's library has its own method names, the gift boxes nest inside each other, and the price lookup service is slow enough that calling it on every page view makes the shop crawl.

This lesson covers the structural patterns that come up most in Python: adapter, facade, decorator, composite and proxy, with brief notes on bridge and flyweight. Each one wraps or combines objects so that the rest of the code can stay simple. As in lesson 4, the classic form is shown alongside the Pythonic one, which is often shorter because of duck typing.

Adapter: making incompatible interfaces fit

An adapter converts the interface of one class into the interface that the rest of your code expects. It is the software version of a travel plug adapter. Ridgeline's checkout code expects every payment provider to have a charge(cents) method that returns a receipt string. LumenPay fits. Northwind Payments, the wholesale provider, offers submit_invoice(amount, currency) with the amount as a decimal string in dollars, and returns a dictionary. Rather than teaching checkout about Northwind's quirks, an adapter wraps the Northwind client and presents the expected charge method.

An adapter wraps a third-party client and presents the interface checkout expects
from decimal import Decimal

class NorthwindClient:
    """Third-party library: we cannot change this class."""
    def submit_invoice(self, amount, currency):
        return {"status": "accepted", "invoice": "NW-5531", "amount": amount, "currency": currency}

class NorthwindAdapter:
    """Presents Northwind as a provider with charge(cents)."""
    def __init__(self, client, currency="CAD"):
        self._client = client
        self._currency = currency

    def charge(self, cents):
        dollars = str(Decimal(cents) / 100)
        result = self._client.submit_invoice(dollars, self._currency)
        if result["status"] != "accepted":
            raise RuntimeError("payment refused")
        return f"invoice {result['invoice']} for {result['amount']} {result['currency']}"

def checkout(provider, cents):
    return provider.charge(cents)        # checkout knows only charge(cents)

print(checkout(NorthwindAdapter(NorthwindClient()), 20400))
Output
invoice NW-5531 for 204 CAD

The adapter holds the client as an attribute and translates each call. This is composition, not inheritance: the adapter does not subclass NorthwindClient, so changes to that library's internals cannot leak into checkout, and the translation rules (dollars as strings, the status check) live in exactly one place. Adapters are also the natural place to translate errors, turning the library's exceptions into Ridgeline's own PaymentError so that callers handle one kind of failure.

Try it yourself

Adapt the supplier feed

Ridgeline's reorder code expects a supplier object with a method price_per_kg(origin) that returns a float price in dollars per kilogram. The green-coffee importer's client, ImporterClient, instead offers quote(region_code), which takes an upper-case region code and returns the price per pound in cents as an int.

Write ImporterAdapter(client) with price_per_kg(origin): upper-case the origin, call quote, convert cents per pound to dollars per kilogram (one kilogram is 2.20462 pounds) and round to 2 decimal places.

class ImporterAdapter:
    def __init__(self, client):
        self.client = client

    def price_per_kg(self, origin):
        return self.client.quote(origin)

print(ImporterAdapter(ImporterClient()).price_per_kg("ethiopia"))

Facade: one simple door into a complicated subsystem

A facade provides a simple interface to a group of classes that work together. Placing an online order at Ridgeline involves the inventory, pricing rules, the payment provider, the shipping calculator, the order repository and the notifier, called in the right order with the right error handling. Every caller that places orders, whether the web shop, the kiosk or a wholesale import script, should not have to know that choreography. A CheckoutService facade offers one method, place_order, and handles the rest.

A facade does not hide the subsystem completely; code that needs the details can still use the individual classes. Its job is to make the common case easy and to keep the choreography in one place, so that a change such as reserving stock before charging the card is made once. Lesson 8 will recognise this facade as the application service layer of a layered architecture.

On the left, three callers labelled web shop, kiosk and wholesale import each point to one box labelled CheckoutService.place_order. From that box, arrows fan out to six boxes: inventory, pricing, payments, shipping, order repository and notifier.
A facade gives every caller one simple entry point into the checkout subsystem

Decorator: adding behaviour by wrapping

The decorator pattern adds responsibilities to an object by wrapping it in another object with the same interface. The wrapper forwards each call to the wrapped object and adds something before or after: logging, timing, retries, caching, access checks. Because the wrapper has the same interface, wrappers can be stacked, and callers cannot tell the difference. Ridgeline wraps its payment provider in a LoggingProvider during the first weeks with Northwind, so every charge is recorded for Anita's reconciliation.

The pattern and Python's @decorator syntax share a name and an idea but are not the same thing. The @ syntax wraps a function at definition time; the design pattern wraps an object at run time. Both add behaviour without changing the original code, and both are applications of the open-closed principle. Lesson 7 covers function decorators in detail.

Stackable wrappers: logging around a limit check around the real provider
class LumenPay:
    def charge(self, cents):
        return f"LumenPay receipt for {cents} cents"

class LoggingProvider:
    """Same interface as any provider; records each charge, then delegates."""
    def __init__(self, inner, log):
        self._inner = inner
        self._log = log

    def charge(self, cents):
        self._log.append(f"charging {cents}")
        receipt = self._inner.charge(cents)
        self._log.append(f"done: {receipt}")
        return receipt

class LimitProvider:
    """Refuses charges over a limit, then delegates."""
    def __init__(self, inner, max_cents):
        self._inner, self._max = inner, max_cents

    def charge(self, cents):
        if cents > self._max:
            raise ValueError(f"charge of {cents} exceeds limit {self._max}")
        return self._inner.charge(cents)

log = []
provider = LoggingProvider(LimitProvider(LumenPay(), 50000), log)
print(provider.charge(4595))
try:
    provider.charge(90000)
except ValueError as e:
    print("refused:", e)
print(log)
Output
LumenPay receipt for 4595 cents
refused: charge of 90000 exceeds limit 50000
['charging 4595', 'done: LumenPay receipt for 4595 cents', 'charging 90000']

Look at what the log shows for the refused charge: the logging wrapper recorded the attempt, but not a completion, because the limit wrapper raised. The order in which wrappers are stacked changes the behaviour, so choose it deliberately.

Wrapping without boilerplate

Real interfaces have more than one method. A payment provider might offer charge, refund, status and close, and a wrapper that only cares about charge would still need four forwarding methods to keep the same interface. Python offers a shortcut: __getattr__ is called only when normal attribute lookup fails, so a wrapper can define the methods it changes and let __getattr__ forward everything else to the wrapped object automatically.

Forwarding unknown attributes with __getattr__ keeps a wrapper short
class Provider:
    def charge(self, cents):
        return f"charged {cents}"
    def refund(self, cents):
        return f"refunded {cents}"
    def status(self):
        return "online"

class AuditedProvider:
    """Overrides charge; forwards every other attribute to the wrapped provider."""
    def __init__(self, inner):
        self._inner = inner
        self.audit = []

    def charge(self, cents):
        self.audit.append(cents)
        return self._inner.charge(cents)

    def __getattr__(self, name):          # only called for names not found on self
        return getattr(self._inner, name)

p = AuditedProvider(Provider())
print(p.charge(1200))
print(p.refund(300))
print(p.status())
print(p.audit)
Output
charged 1200
refunded 300
online
[1200]

Use this with care. Automatic forwarding hides which methods the wrapper really supports, so tools such as type checkers and editors cannot see them, and a misspelt attribute produces an error from the wrapped object rather than from the wrapper. For small interfaces, writing the forwarding methods out is clearer; for large ones, a typing.Protocol (lesson 7) documents the expected methods while __getattr__ does the forwarding.

Composite: treating groups and items the same way

The composite pattern lets you treat individual objects and groups of objects uniformly. Ridgeline's gift boxes, which you priced recursively with dictionaries in PYTH-201, are a natural composite: a box contains products and other boxes, and both a single bag and a whole box can answer what is your price? and how many items do you contain? Client code asks the question once and never checks whether it is holding a box or a bag.

A tree diagram. The Explorer box at the top contains Sunrise Blend, Ethiopia Guji and a Brewing Kit box. The Brewing Kit box contains a mug, a filter set and a sample bag. Leaves are drawn as rounded rectangles and boxes as folders, and each node shows its price.
A composite: boxes contain items or other boxes, and every node answers price()

Try it yourself

Gift boxes as a composite

Write two classes with the same interface. Item(name, price) represents one product; its price() returns the price and its count() returns 1. Box(name, contents) holds a list of Item and Box objects; its price() is the sum of its contents' prices and its count() is the total number of items inside, at every level.

Boxes themselves are not items, so an empty box has price 0 and count 0.

class Item:
    def __init__(self, name, price):
        self.name, self._price = name, price
    def price(self):
        return self._price
    def count(self):
        return 1

class Box:
    def __init__(self, name, contents):
        self.name, self.contents = name, contents
    def price(self):
        return sum(c._price for c in self.contents)
    def count(self):
        return len(self.contents)

Proxy: a stand-in that controls access

A proxy is an object with the same interface as another object that stands in for it and controls access to it. A caching proxy remembers results so the real object is called less often; a lazy proxy delays creating an expensive object until it is first used; a protection proxy checks permissions; a remote proxy hides the fact that the real object lives on another machine. Structurally a proxy looks like a decorator; the difference is intent. A decorator adds behaviour; a proxy manages access to the real thing.

Ridgeline's web shop asks the pricing service for every coffee on every page view, and each call takes a noticeable fraction of a second. Prices change at most once a day, so a caching proxy in front of the service is a large win with a few lines of code. The proxy must also offer a way to clear the cache when Rosa updates prices.

Try it yourself

A caching price proxy

The setup provides SlowPrices, whose price(sku) returns a price and records each call in self.calls. Write CachedPrices(service) with the same price(sku) method: the first request for a SKU asks the service, and later requests for the same SKU return the remembered value.

Add clear(), which forgets every remembered price so the next request asks the service again.

class CachedPrices:
    def __init__(self, service):
        self.service = service

    def price(self, sku):
        return self.service.price(sku)

    def clear(self):
        pass

Bridge and flyweight, briefly

Two more structural patterns are worth recognising. The bridge pattern separates an abstraction from its implementation so the two can vary independently: Ridgeline's reports (daily sales, monthly wholesale) and their output formats (text, CSV, HTML) form two dimensions, and rather than a class for every combination, each report holds a formatter object. You will notice this is composition over inheritance again, applied deliberately to two independent axes of change.

The flyweight pattern shares common data among many small objects to save memory. If the web shop showed hundreds of thousands of product tiles, each could refer to one shared, immutable record for its coffee instead of copying the name, description and image for every tile. In Python, interned strings, small cached integers and immutable value objects shared by reference are everyday flyweights, and __slots__ reduces the memory each object needs.

Worked problem

Adapter, decorator or proxy?

Three wrappers appear in Ridgeline's code review. Sam is unsure what to call each one, since all three wrap another object.

Show solution
  1. TaxServiceWrapper changes the province-code argument and converts the tax service's response into Ridgeline's TaxResult: it changes the interface, so it is an adapter.
  2. TimedProvider has exactly the same charge(cents) interface and records how long each charge took: it adds behaviour, so it is a decorator.
  3. LazyReportEngine has the same interface as the report engine but creates the engine, which loads large templates, only on the first call: it controls access to the real object, so it is a proxy.
  4. The structure is the same in all three (hold an object, forward calls); the intent decides the name.

Answer: An adapter changes the interface, a decorator keeps the interface and adds behaviour, and a proxy keeps the interface and controls access.

Common mistakes

  • MistakeSpreading the translation of a third-party library's interface across the codebase.

    InsteadWrite one adapter that presents the interface your code expects.

  • MistakeSubclassing a third-party class to adapt it.

    InsteadWrap it with composition, so its internals cannot leak into your code.

  • MistakeLetting callers repeat a multi-step choreography of subsystem calls.

    InsteadOffer a facade with one method for the common case.

  • MistakeStacking decorators without thinking about their order.

    InsteadDecide which wrapper should see each call first; the order changes behaviour.

  • MistakeChecking isinstance(x, Box) everywhere in code that handles gift boxes.

    InsteadGive items and boxes the same interface and let each answer for itself (composite).

  • MistakeCaching results that can change without a way to clear the cache.

    InsteadGive a caching proxy an explicit way to invalidate entries.

Summary and key terms

  • Structural patterns combine objects into larger structures, often by wrapping.
  • An adapter translates one interface into the one your code expects, usually by composition.
  • A facade gives a simple entry point to a subsystem and keeps the choreography in one place.
  • A decorator wraps an object with the same interface to add behaviour; wrappers can be stacked, and order matters.
  • A composite lets individual items and groups be treated the same way through a shared interface.
  • A proxy stands in for an object to control access: caching, lazy creation, protection or remote access.
  • Bridge separates two dimensions of variation; flyweight shares immutable data among many objects.
structural pattern
A pattern about how objects and classes are combined into larger structures.
adapter
A wrapper that converts one interface into another that clients expect.
facade
A simple interface to a set of classes that work together.
decorator pattern
Wrapping an object in another with the same interface to add behaviour.
composite
A structure in which individual objects and groups share one interface.
proxy
A stand-in object that controls access to another object.
caching proxy
A proxy that remembers results to avoid repeated expensive calls.
lazy initialization
Delaying the creation of an expensive object until it is first needed.
bridge
Separating an abstraction from its implementation so both can vary.
flyweight
Sharing common immutable data among many small objects.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. What distinguishes an adapter from a decorator?

    Show answer

    An adapter changes the interface; a decorator keeps it and adds behaviour.

  2. Why should an adapter wrap a library object instead of subclassing it?

    Show answer

    Composition keeps the library's internals out of your code and limits coupling to one class.

  3. Which pattern does CheckoutService.place_order represent?

    Show answer

    A facade over inventory, pricing, payments, shipping, storage and notification.

  4. In the composite exercise, why must Box call c.price() rather than read c._price?

    Show answer

    A child might itself be a box, which has no single stored price; calling price() lets each child compute its own.

  5. Give two kinds of proxy.

    Show answer

    For example a caching proxy and a lazy proxy (also protection and remote proxies).

  6. Name an everyday flyweight in Python.

    Show answer

    Interned strings or small cached integers, or one immutable record shared by many objects.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz