|
| 1 | +This page describes how to use the [_decorator pattern_](https://en.wikipedia.org/wiki/Decorator_pattern) with Rodi's dependency injection container, available since version `2.1.0`. |
| 2 | + |
| 3 | +- [X] What the decorator pattern is. |
| 4 | +- [X] Basic usage with `container.decorate()`. |
| 5 | +- [X] Decorators with additional dependencies. |
| 6 | +- [X] Chaining multiple decorators. |
| 7 | +- [X] Lifetime behaviour. |
| 8 | +- [X] Class-property injection in decorators. |
| 9 | + |
| 10 | +## What is the decorator pattern? |
| 11 | + |
| 12 | +The [decorator pattern](https://en.wikipedia.org/wiki/Decorator_pattern) is a structural |
| 13 | +design pattern that wraps an object with another object that shares the same interface. |
| 14 | +The outer object — the _decorator_ — adds or modifies behaviour before or after |
| 15 | +delegating to the inner object. |
| 16 | + |
| 17 | +Common uses include: |
| 18 | + |
| 19 | +- **Logging** — record calls transparently, without touching business logic. |
| 20 | +- **Caching** — return cached results when available. |
| 21 | +- **Retry / resilience** — retry failed calls automatically. |
| 22 | +- **Authorisation** — gate access without changing the service. |
| 23 | +- **Metrics / tracing** — instrument calls for observability. |
| 24 | + |
| 25 | +Because both the original service and its decorators implement the same interface, |
| 26 | +the rest of the application has no idea decorators exist. |
| 27 | + |
| 28 | +## Basic usage |
| 29 | + |
| 30 | +Use `container.decorate(base_type, decorator_type)` to wrap an already-registered type. |
| 31 | + |
| 32 | +The decorator class must satisfy one rule: its `__init__` must have **at least one |
| 33 | +parameter whose type annotation matches the registered base type** (or a supertype of |
| 34 | +it). That parameter receives the inner service instance. Every other `__init__` parameter |
| 35 | +is resolved from the container as usual. |
| 36 | + |
| 37 | +```python {linenums="1", hl_lines="5 10 15 28-29"} |
| 38 | +from abc import ABC, abstractmethod |
| 39 | +from rodi import Container |
| 40 | + |
| 41 | + |
| 42 | +class MessageSender(ABC): |
| 43 | + @abstractmethod |
| 44 | + def send(self, message: str) -> None: ... |
| 45 | + |
| 46 | + |
| 47 | +class ConsoleSender(MessageSender): |
| 48 | + def send(self, message: str) -> None: |
| 49 | + print(f"[console] {message}") |
| 50 | + |
| 51 | + |
| 52 | +class LoggingMessageSender(MessageSender): |
| 53 | + """Decorator: records every message before delegating to the inner sender.""" |
| 54 | + |
| 55 | + def __init__(self, inner: MessageSender) -> None: |
| 56 | + self.inner = inner |
| 57 | + self.log: list[str] = [] |
| 58 | + |
| 59 | + def send(self, message: str) -> None: |
| 60 | + self.log.append(message) |
| 61 | + self.inner.send(message) |
| 62 | + |
| 63 | + |
| 64 | +container = Container() |
| 65 | +container.add_transient(MessageSender, ConsoleSender) |
| 66 | +container.decorate(MessageSender, LoggingMessageSender) |
| 67 | + |
| 68 | +sender = container.resolve(MessageSender) |
| 69 | + |
| 70 | +assert isinstance(sender, LoggingMessageSender) # outer decorator |
| 71 | +assert isinstance(sender.inner, ConsoleSender) # inner service |
| 72 | + |
| 73 | +sender.send("Hello!") |
| 74 | +assert sender.log == ["Hello!"] |
| 75 | +``` |
| 76 | + |
| 77 | +/// admonition | Order matters. |
| 78 | + type: tip |
| 79 | + |
| 80 | +`decorate()` must be called **after** the base type is registered. An unregistered |
| 81 | +base type raises `DecoratorRegistrationException` immediately. |
| 82 | + |
| 83 | +/// |
| 84 | + |
| 85 | +## Decorators with additional dependencies |
| 86 | + |
| 87 | +The decorator's `__init__` can declare any number of extra parameters alongside |
| 88 | +the decoratee. Rodi resolves them from the container exactly as it would for any |
| 89 | +other type. |
| 90 | + |
| 91 | +```python {linenums="1", hl_lines="4 7 20-23 37-38"} |
| 92 | +from rodi import Container |
| 93 | + |
| 94 | + |
| 95 | +class MessageSender: ... |
| 96 | + |
| 97 | + |
| 98 | +class ConsoleSender(MessageSender): |
| 99 | + def send(self, message: str) -> None: |
| 100 | + print(message) |
| 101 | + |
| 102 | + |
| 103 | +class Logger: |
| 104 | + def __init__(self) -> None: |
| 105 | + self.entries: list[str] = [] |
| 106 | + |
| 107 | + def log(self, text: str) -> None: |
| 108 | + self.entries.append(text) |
| 109 | + |
| 110 | + |
| 111 | +class InstrumentedSender(MessageSender): |
| 112 | + def __init__(self, inner: MessageSender, logger: Logger) -> None: |
| 113 | + self.inner = inner |
| 114 | + self.logger = logger |
| 115 | + |
| 116 | + def send(self, message: str) -> None: |
| 117 | + self.logger.log(f"send({message!r})") |
| 118 | + self.inner.send(message) |
| 119 | + |
| 120 | + |
| 121 | +container = Container() |
| 122 | +container.add_transient(Logger) |
| 123 | +container.add_transient(MessageSender, ConsoleSender) |
| 124 | +container.decorate(MessageSender, InstrumentedSender) |
| 125 | + |
| 126 | +sender = container.resolve(MessageSender) |
| 127 | + |
| 128 | +assert isinstance(sender, InstrumentedSender) |
| 129 | +assert isinstance(sender.logger, Logger) |
| 130 | +sender.send("Hi") |
| 131 | +assert sender.logger.entries == ["send('Hi')"] |
| 132 | +``` |
| 133 | + |
| 134 | +## Chaining multiple decorators |
| 135 | + |
| 136 | +Calling `decorate()` more than once for the same type **chains** the decorators. |
| 137 | +Each call wraps the current registration, so the **last registered decorator is |
| 138 | +the outermost one**. |
| 139 | + |
| 140 | +```python {linenums="1", hl_lines="33-34"} |
| 141 | +from rodi import Container |
| 142 | + |
| 143 | + |
| 144 | +class Greeter: |
| 145 | + def greet(self, name: str) -> str: ... |
| 146 | + |
| 147 | + |
| 148 | +class SimpleGreeter(Greeter): |
| 149 | + def greet(self, name: str) -> str: |
| 150 | + return f"Hello, {name}" |
| 151 | + |
| 152 | + |
| 153 | +class LoggingGreeter(Greeter): |
| 154 | + def __init__(self, inner: Greeter) -> None: |
| 155 | + self.inner = inner |
| 156 | + self.calls: list[str] = [] |
| 157 | + |
| 158 | + def greet(self, name: str) -> str: |
| 159 | + self.calls.append(name) |
| 160 | + return self.inner.greet(name) |
| 161 | + |
| 162 | + |
| 163 | +class ExclamatoryGreeter(Greeter): |
| 164 | + def __init__(self, inner: Greeter) -> None: |
| 165 | + self.inner = inner |
| 166 | + |
| 167 | + def greet(self, name: str) -> str: |
| 168 | + return self.inner.greet(name) + "!" |
| 169 | + |
| 170 | + |
| 171 | +container = Container() |
| 172 | +container.add_transient(Greeter, SimpleGreeter) |
| 173 | +container.decorate(Greeter, LoggingGreeter) # wraps SimpleGreeter |
| 174 | +container.decorate(Greeter, ExclamatoryGreeter) # wraps LoggingGreeter |
| 175 | + |
| 176 | +greeter = container.resolve(Greeter) |
| 177 | + |
| 178 | +# ExclamatoryGreeter → LoggingGreeter → SimpleGreeter |
| 179 | +assert isinstance(greeter, ExclamatoryGreeter) |
| 180 | +assert isinstance(greeter.inner, LoggingGreeter) |
| 181 | +assert isinstance(greeter.inner.inner, SimpleGreeter) |
| 182 | + |
| 183 | +assert greeter.greet("World") == "Hello, World!" |
| 184 | +``` |
| 185 | + |
| 186 | +## Lifetime behaviour |
| 187 | + |
| 188 | +A decorator **inherits the lifetime of the service it wraps**. If the inner service |
| 189 | +is a singleton, the whole decorated chain is a singleton; if it is scoped, the chain |
| 190 | +is scoped; if transient, the chain is transient. |
| 191 | + |
| 192 | +=== "Singleton" |
| 193 | + |
| 194 | + ```python {linenums="1", hl_lines=""} |
| 195 | + container = Container() |
| 196 | + container.add_singleton(MessageSender, ConsoleSender) |
| 197 | + container.decorate(MessageSender, LoggingMessageSender) |
| 198 | + |
| 199 | + provider = container.build_provider() |
| 200 | + |
| 201 | + a = provider.get(MessageSender) |
| 202 | + b = provider.get(MessageSender) |
| 203 | + assert a is b # same instance every time |
| 204 | + ``` |
| 205 | + |
| 206 | +=== "Scoped" |
| 207 | + |
| 208 | + ```python {linenums="1", hl_lines=""} |
| 209 | + container = Container() |
| 210 | + container.add_scoped(MessageSender, ConsoleSender) |
| 211 | + container.decorate(MessageSender, LoggingMessageSender) |
| 212 | + |
| 213 | + provider = container.build_provider() |
| 214 | + |
| 215 | + with provider.create_scope() as scope: |
| 216 | + a = provider.get(MessageSender, scope) |
| 217 | + b = provider.get(MessageSender, scope) |
| 218 | + assert a is b # same instance within the scope |
| 219 | + |
| 220 | + with provider.create_scope() as scope2: |
| 221 | + c = provider.get(MessageSender, scope2) |
| 222 | + assert c is not a # new instance in a new scope |
| 223 | + ``` |
| 224 | + |
| 225 | +=== "Transient" |
| 226 | + |
| 227 | + ```python {linenums="1", hl_lines=""} |
| 228 | + container = Container() |
| 229 | + container.add_transient(MessageSender, ConsoleSender) |
| 230 | + container.decorate(MessageSender, LoggingMessageSender) |
| 231 | + |
| 232 | + provider = container.build_provider() |
| 233 | + |
| 234 | + a = provider.get(MessageSender) |
| 235 | + b = provider.get(MessageSender) |
| 236 | + assert a is not b # fresh instance each time |
| 237 | + ``` |
| 238 | + |
| 239 | +## Class-property injection in decorators |
| 240 | + |
| 241 | +Decorators support the same [mixed injection](./getting-started.md) as any other |
| 242 | +registered type. If the decorator class has **class-level type annotations** in addition |
| 243 | +to its `__init__` parameters, Rodi injects those properties via `setattr` after |
| 244 | +construction — exactly as it does for regular services. |
| 245 | + |
| 246 | +```python {linenums="1", hl_lines="19 21 37"} |
| 247 | +from rodi import Container |
| 248 | + |
| 249 | + |
| 250 | +class Greeter: |
| 251 | + def greet(self, name: str) -> str: ... |
| 252 | + |
| 253 | + |
| 254 | +class SimpleGreeter(Greeter): |
| 255 | + def greet(self, name: str) -> str: |
| 256 | + return f"Hello, {name}" |
| 257 | + |
| 258 | + |
| 259 | +class Logger: |
| 260 | + def __init__(self) -> None: |
| 261 | + self.entries: list[str] = [] |
| 262 | + |
| 263 | + |
| 264 | +class LoggingGreeter(Greeter): |
| 265 | + logger: Logger # injected via setattr after __init__ |
| 266 | + |
| 267 | + def __init__(self, inner: Greeter) -> None: |
| 268 | + self.inner = inner |
| 269 | + |
| 270 | + def greet(self, name: str) -> str: |
| 271 | + self.logger.log(f"greet({name!r})") |
| 272 | + return self.inner.greet(name) |
| 273 | + |
| 274 | + |
| 275 | +container = Container() |
| 276 | +container.add_transient(Greeter, SimpleGreeter) |
| 277 | +container.add_transient(Logger) |
| 278 | +container.decorate(Greeter, LoggingGreeter) |
| 279 | + |
| 280 | +greeter = container.resolve(Greeter) |
| 281 | + |
| 282 | +assert isinstance(greeter, LoggingGreeter) |
| 283 | +assert isinstance(greeter.logger, Logger) # injected as class property |
| 284 | +``` |
0 commit comments