Card 04/ 08
Decorator Example: Logging and Retrying an HTTP Client
Wrap the client in a retry layer, then in a logging layer, without editing either.
A network client is a natural Decorator, and this example maps the template onto it. The client's interface is one method, get(url). Retrying and logging are each a decorator that implements get and holds another client, and the example builds the same two layers in both orders to show that the order changes what is logged.
| Template | HTTP example |
|---|---|
| Component | HttpClient, with get(url) |
| ConcreteComponent | FlakyClient, which times out a set number of times before it answers |
| ConcreteDecorator | RetryingClient, which tries again after a timeout, and LoggingClient, which prints each call |
from abc import ABC, abstractmethod
class HttpClient(ABC):
@abstractmethod
def get(self, url: str) -> str: ...
class FlakyClient(HttpClient):
def __init__(self, failures: int) -> None:
self._failures = failures
def get(self, url: str) -> str:
if self._failures > 0:
self._failures -= 1
raise ConnectionError("timed out")
return "200 OK"
class RetryingClient(HttpClient):
def __init__(self, inner: HttpClient, attempts: int) -> None:
self._inner = inner
self._attempts = attempts
def get(self, url: str) -> str:
for attempt in range(1, self._attempts + 1):
try:
return self._inner.get(url)
except ConnectionError:
if attempt == self._attempts:
raise
raise AssertionError("unreachable")
class LoggingClient(HttpClient):
def __init__(self, inner: HttpClient) -> None:
self._inner = inner
def get(self, url: str) -> str:
print(f" log: GET {url}")
try:
result = self._inner.get(url)
except ConnectionError as error:
print(f" log: failed, {error}")
raise
print(f" log: {result}")
return result
print("logging outside retry:")
outside = LoggingClient(RetryingClient(FlakyClient(failures=2), attempts=3))
print(outside.get("/orders"))
print("retry outside logging:")
inside = RetryingClient(LoggingClient(FlakyClient(failures=2)), attempts=3)
print(inside.get("/orders"))logging outside retry:
log: GET /orders
log: 200 OK
200 OK
retry outside logging:
log: GET /orders
log: failed, timed out
log: GET /orders
log: failed, timed out
log: GET /orders
log: 200 OK
200 OKBoth stacks return "200 OK", and they log different things. With logging outside, the logger sees one call that succeeded, because the retries happen inside the stack where it cannot see them. With logging inside, the retry layer calls the logger three times, and every failed attempt is printed.
| Step | Layer | What happens |
|---|---|---|
| 1 | RetryingClient | attempt 1 calls the logging layer |
| 2 | LoggingClient | prints GET /orders and calls the flaky client |
| 3 | FlakyClient | raises timed out; the logger prints the failure and re-raises |
| 4 | RetryingClient | catches the timeout; attempt 2 repeats steps 2 and 3 |
| 5 | RetryingClient | attempt 3 calls the logging layer again |
| 6 | FlakyClient | returns 200 OK; the logger prints it and returns it |
Neither order is wrong; they answer different questions. Logging outside records what the caller experienced, one slow request. Logging inside records what the network did, three attempts. You choose the order by which of those you need, and only the line that builds the stack changes.
← → move through sections and cards