Skip to content

Commit 4f920a2

Browse files
Document decorator pattern for Rodi (#35)
1 parent c9ea24a commit 4f920a2

File tree

2 files changed

+285
-0
lines changed

2 files changed

+285
-0
lines changed

rodi/docs/decorator-pattern.md

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
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+
```

rodi/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ nav:
1414
- Working with async: async.md
1515
- Context managers: context-managers.md
1616
- Union types: union-types.md
17+
- Decorator pattern: decorator-pattern.md
1718
- Errors: errors.md
1819
- About Rodi: about.md
1920
- Neoteroi docs home: "/"

0 commit comments

Comments
 (0)