Card 01/ 05

ComparisonDifficulty: Intermediate3 min

Adapter vs Decorator vs Proxy vs Facade

Four wrappers that look alike in a diagram and differ in what they promise the caller.

Adapter, Decorator, Proxy and Facade all place an object between a caller and something else, and they are told apart by comparing two interfaces: the one the wrapper offers the caller, and the one it wraps. Adapter changes the interface. Decorator and Proxy keep it, and differ in whether they add behaviour or control access. Facade offers a smaller interface over several objects. This card sets the four side by side, then wraps one service each way.

The interface each wrapper offers, what it wraps, and why
PatternInterface offeredWhat it wrapsIts purpose
AdapterA different one, the one the client expectsOne objectMake an existing object fit
DecoratorThe same oneOne object, and decorators stackAdd behaviour
ProxyThe same oneOne object, often created by the proxyControl access: when, whether, or from where
FacadeA new and simpler oneSeveral objects in a subsystemHide the subsystem's complexity

Two questions settle most cases. First, does the wrapper offer the same interface as what it wraps? If not, it is an adapter for one object or a facade for several. Second, if it does, is it adding something the caller wants, such as logging or retries, or deciding whether and when the call happens? Adding is Decorator, deciding is Proxy.

The listing below wraps one WeatherService four ways. The service is the same each time; only the wrapper changes.

py
from typing import Protocol


class Weather(Protocol):
    def temperature(self, city: str) -> float: ...


class WeatherService:
    def __init__(self) -> None:
        self.calls = 0

    def temperature(self, city: str) -> float:
        self.calls += 1
        return {"Oslo": 4.0, "Lisbon": 19.5}[city]


class FahrenheitAdapter:
    def __init__(self, service: Weather) -> None:
        self._service = service

    def reading_f(self, city: str) -> float:
        return self._service.temperature(city) * 9 / 5 + 32


class LoggingWeather:
    def __init__(self, inner: Weather) -> None:
        self._inner = inner

    def temperature(self, city: str) -> float:
        result = self._inner.temperature(city)
        print(f"  log: temperature({city}) = {result}")
        return result


class CachingWeather:
    def __init__(self, real: Weather) -> None:
        self._real = real
        self._cache: dict[str, float] = {}

    def temperature(self, city: str) -> float:
        if city not in self._cache:
            self._cache[city] = self._real.temperature(city)
        return self._cache[city]


class Flights:
    def book(self, city: str) -> str:
        return f"flight to {city} FL-208"


class Hotels:
    def book(self, city: str) -> str:
        return f"hotel in {city} HT-51"


class TripPlanner:
    def __init__(self, weather: Weather, flights: Flights, hotels: Hotels) -> None:
        self._weather = weather
        self._flights = flights
        self._hotels = hotels

    def plan(self, city: str) -> str:
        celsius = self._weather.temperature(city)
        packing = "a coat" if celsius < 10 else "sunglasses"
        return f"{self._flights.book(city)}, {self._hotels.book(city)}, pack {packing}"


service = WeatherService()
print("Adapter, a different interface:")
print(f"  reading_f(Oslo) = {FahrenheitAdapter(service).reading_f('Oslo')}")
print("Decorator, the same interface and added behaviour:")
LoggingWeather(service).temperature("Lisbon")
print("Proxy, the same interface and controlled access:")
cached = CachingWeather(service)
before = service.calls
cached.temperature("Oslo")
cached.temperature("Oslo")
print(f"  two requests, {service.calls - before} call to the service")
print("Facade, a simpler interface over several services:")
print(f"  {TripPlanner(service, Flights(), Hotels()).plan("Oslo")}")
OutputRan with python:3.12-slim (Python 3.12.13)
Adapter, a different interface:
  reading_f(Oslo) = 39.2
Decorator, the same interface and added behaviour:
  log: temperature(Lisbon) = 19.5
Proxy, the same interface and controlled access:
  two requests, 1 call to the service
Facade, a simpler interface over several services:
  flight to Oslo FL-208, hotel in Oslo HT-51, pack a coat
`reading_f` is a new interface, `temperature` is kept twice, and `plan` sits over three services.

The adapter offers reading_f, which the service does not have, and converts to Fahrenheit. The logging decorator offers temperature and adds a line of output. The caching proxy offers temperature too, and one call to the service for two requests shows it deciding whether to forward. The facade offers plan, one call over the weather, flight and hotel services.

Decorator and Proxy are the closest pair, and some code is both: a caching wrapper adds a cache and controls access. Name it by its main purpose, and by how it is built. Decorators are handed their object and expected to stack; a proxy often creates or locates its subject and stands alone.

← → move through sections and cards