Structural Patterns
Adapt incompatible interfaces, add behavior with decorators, and control access with a proxy without hiding the real object.
By the end, you’ll be able to…
- Wrap a third-party interface with an adapter.
- Use decorators to add behavior around a call.
- Implement a proxy for lazy loading or access control.
- Choose the smallest structure that solves the problem.
01An adapter translates an interface
An adapter wraps an existing object and exposes the interface your code expects. It is useful at a third-party boundary, where the vendor's method names and error types should not leak through the application. The adapter translates both data and failures into your vocabulary.
Keep the adapter thin: validate assumptions, call the dependency, and translate the result. Do not put business rules in the adapter, or every backend will need a copy. If the third-party API changes, the adapter bounds the blast radius to one file.
02Decorators add behavior around a call
A Python decorator is a function that receives a function and returns a replacement. Use functools.wraps to preserve the original name and docstring. Decorators are a natural fit for logging, timing, caching, and authorization checks that wrap a call without changing its core logic.
A decorator should be transparent about what it changes. If it can raise a new exception, document that. If it caches, document the key and invalidation policy. Avoid stacking many decorators that each mutate arguments; the call becomes difficult to reason about and debug.
03A proxy controls access
A proxy implements the same interface as the real object and decides when to delegate. A lazy proxy creates the expensive object on first use. A protective proxy checks permission before forwarding. A caching proxy can return a stored result without contacting the origin.
The proxy must preserve the real object's contract. If the real repository raises NotFoundError, the proxy should not convert it to None. Keep the proxy's additional behavior visible in its name and documentation; a proxy that silently changes semantics is a trap.
04Choose the smallest structure
Patterns are not a checklist. A one-method adapter that only renames a method may be unnecessary; a direct call with a comment can be clearer. A decorator that hides a network request inside an attribute access is surprising. The structure should reduce coupling or make a boundary testable, not merely look architectural.
Write the caller first. If the caller can depend on a simple protocol and the existing object already satisfies it, no adapter is needed. If a decorator would need to know about five unrelated concerns, split the concerns instead. The best pattern is the one another developer can explain without opening the catalog.
The ideas worth keeping.
- Adapters translate a dependency into your application's vocabulary.
- Decorators wrap behavior and should preserve metadata.
- Proxies control access while preserving the underlying contract.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Adapt a temperature sensor
Wrap a sensor that returns Celsius so callers see a temperature() method in Fahrenheit. Verify a reading of 25 becomes 77.0.
Explore a worked solution
class CelsiusSensor: def reading(self): return 25class FahrenheitAdapter: def __init__(self, sensor): self.sensor = sensor def temperature(self): return self.sensor.reading() * 9 / 5 + 32assert FahrenheitAdapter(CelsiusSensor()).temperature() == 77.0print("adapter verified")The adapter owns the unit conversion and exposes the interface the application expects. The sensor remains unchanged and can be replaced by any object with a reading() method.
Add a caching proxy
Create a proxy around a slow report that renders only once and returns the cached string on subsequent calls.
Explore a worked solution
class Report: def render(self): return "report"class CachedReport: def __init__(self, report): self.report = report self._value = None def render(self): if self._value is None: self._value = self.report.render() return self._valuereport = CachedReport(Report())assert report.render() == report.render() == "report"print("cache verified")The proxy preserves the render() contract while adding a cache. The first call populates the value; later calls return it without touching the wrapped object.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Adapt a temperature sensor
Wrap a sensor that returns Celsius so callers see a temperature() method in Fahrenheit. Verify a reading of 25 becomes 77.0.
adapter verified
Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
One lesson closer.
Reading is a start. Trying it yourself makes the connection.