Skip to content

Commit a576e37

Browse files
Improve the documentation for Generics (#5)
1 parent 4bbbe46 commit a576e37

File tree

2 files changed

+179
-43
lines changed

2 files changed

+179
-43
lines changed

rodi/docs/dependency-inversion.md

Lines changed: 176 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -234,69 +234,206 @@ container.add_transient_by_factory(my_factory) # <-- MyClass is used as Key.
234234

235235
## Working with generics
236236

237-
Generic types are supported.
237+
Generic types are supported. The following example provides a meaningful
238+
demonstration of generics with `TypeVar` in a real-world scenario.
238239

239-
```python {linenums="1", hl_lines="1 6 9 29 34 40-41 44-45"}
240-
from typing import Generic, TypeVar
240+
```python {linenums="1", hl_lines="9 43-44 47-48"}
241+
from dataclasses import dataclass
242+
from typing import Generic, List, TypeVar
241243

242244
from rodi import Container
243245

244-
245246
T = TypeVar("T")
246247

247248

248-
class LoggedVar(Generic[T]):
249-
def __init__(self, value: T, name: str):
250-
self.name = name
251-
self.value = value
249+
class Repository(Generic[T]): # interface
250+
"""A generic repository for managing entities of type T."""
251+
252+
def __init__(self):
253+
self._items: List[T] = []
254+
255+
def add(self, item: T):
256+
"""Add an item to the repository."""
257+
self._items.append(item)
252258

253-
def set(self, new: T):
254-
self.log("Set " + repr(self.value))
255-
self.value = new
259+
def get_all(self) -> List[T]:
260+
"""Retrieve all items from the repository."""
261+
return self._items
256262

257-
def get(self) -> T:
258-
self.log("Get " + repr(self.value))
259-
return self.value
260263

261-
def log(self, message: str):
262-
print(self.name, message)
264+
# Define specific entity classes
265+
@dataclass
266+
class Product:
267+
id: int
268+
name: str
263269

264270

271+
@dataclass
272+
class Customer:
273+
id: int
274+
email: str
275+
first_name: str
276+
last_name: str
277+
278+
279+
# Set up the container
265280
container = Container()
266281

282+
# Register repositories
283+
container.add_scoped(Repository[Product], Repository)
284+
container.add_scoped(Repository[Customer], Repository)
267285

268-
class A(LoggedVar[int]):
269-
def __init__(self):
270-
super().__init__(10, "example")
286+
# Resolve and use the repositories
287+
product_repo = container.resolve(Repository[Product])
288+
customer_repo = container.resolve(Repository[Customer])
271289

290+
# Add and retrieve products
291+
product_repo.add(Product(1, "Laptop"))
292+
product_repo.add(Product(2, "Smartphone"))
293+
print(product_repo.get_all())
294+
295+
# Add and retrieve customers
296+
customer_repo.add(Customer(1, "alice@wonderland.it", "Alice", "WhiteRabbit"))
297+
customer_repo.add(Customer(1, "bob@foopower.it", "Bob", "TheHamster"))
298+
print(customer_repo.get_all())
299+
```
300+
301+
The above prints to screen:
302+
303+
```bash
304+
[Product(id=1, name='Laptop'), Product(id=2, name='Smartphone')]
305+
[Customer(id=1, email='alice@wonderland.it', first_name='Alice', last_name='WhiteRabbit'), Customer(id=1, email='bob@foopower.it', first_name='Bob', last_name='TheHamster')]
306+
```
307+
308+
/// admonition | GenericAlias in Python is not considered a class.
309+
type: warning
310+
311+
Note how the generics `Repository[Product]` and `Repository[Customer]` are both
312+
configured to be resolved using `Repository` as concrete type. `GenericAlias`
313+
in Python is not considered an actual class. The following wouldn't work:
314+
315+
```python
316+
container.add_scoped(Repository[Product]) # No. 💥
317+
container.add_scoped(Repository[Customer]) # No. 💥
318+
```
319+
///
320+
321+
### Nested generics
322+
323+
When working with nested generics, ensure that the *same type* used to describe
324+
a dependency is registered in the container.
325+
326+
```python {linenums="1", hl_lines="12 16-17 26 33"}
327+
from dataclasses import dataclass
328+
from typing import Generic, List, TypeVar
329+
330+
from rodi import Container
331+
332+
T = TypeVar("T")
333+
334+
335+
class DBConnection: ...
336+
337+
338+
class Repository(Generic[T]):
339+
db_connection: DBConnection
340+
341+
342+
class Service(Generic[T]):
343+
repository: Repository[T]
272344

273-
class B(LoggedVar[str]):
274-
def __init__(self):
275-
super().__init__("Foo", "example")
276345

346+
@dataclass
347+
class Product:
348+
id: int
349+
name: str
277350

278-
class C:
279-
a: LoggedVar[int]
280-
b: LoggedVar[str]
281351

352+
class ProductsService(Service[Product]):
353+
...
282354

283-
container.add_scoped(LoggedVar[int], A)
284-
container.add_scoped(LoggedVar[str], B)
285-
container.add_scoped(C)
286355

287-
instance = container.resolve(C)
356+
container = Container()
357+
358+
container.add_scoped(DBConnection)
359+
container.add_scoped(Repository[T], Repository)
360+
container.add_scoped(ProductsService)
288361

289-
assert isinstance(instance.a, A)
290-
assert isinstance(instance.b, B)
362+
service = container.resolve(ProductsService)
363+
assert isinstance(service.repository, Repository)
364+
assert isinstance(service.repository.db_connection, DBConnection)
291365
```
292366

293-
As described above, use the *most* abstract class as the key to resolve more
294-
*concrete* types, in accordance with the Dependency Inversion Principle (DIP). Generics are the **most** abstract
295-
type, so use them as keys like in the example above at lines _44-45_.
367+
---
368+
369+
The following wouldn't work, because the `Container` will look exactly for the
370+
key `Repository[T]` when instantiating the `ProductsService`, not for
371+
`Repository[Product]`:
372+
373+
```python
374+
container.add_scoped(Repository[Product], Repository) # No. 💥
375+
```
376+
377+
Note that, in practice, this does not cause any issues at runtime, because of
378+
**type erasure**. For more information, refer to [_Instantiating generic classes and type erasure_](https://typing.python.org/en/latest/spec/generics.html#instantiating-generic-classes-and-type-erasure).
379+
380+
If you need to define a more specialized class for `Repository[Product]`,
381+
because for example you need to define products-specific methods, you can:
382+
383+
- Define a `ProductsRepository(Repository[Product])`.
384+
- Override the annotation for `repository` in `ProductsService`.
385+
- Register `ProductsRepository` in the container.
386+
387+
```python {linenums="1", hl_lines="26 29-30 37"}
388+
from dataclasses import dataclass
389+
from typing import Generic, TypeVar
390+
391+
from rodi import Container
392+
393+
T = TypeVar("T")
394+
395+
396+
class DBConnection: ...
397+
398+
399+
class Repository(Generic[T]):
400+
db_connection: DBConnection
401+
402+
403+
class Service(Generic[T]):
404+
repository: Repository[T]
405+
406+
407+
@dataclass
408+
class Product:
409+
id: int
410+
name: str
411+
412+
413+
class ProductsRepository(Repository[Product]): ...
414+
415+
416+
class ProductsService(Service[Product]):
417+
repository: ProductsRepository
418+
419+
420+
container = Container()
421+
422+
container.add_scoped(DBConnection)
423+
container.add_scoped(Repository[T], Repository)
424+
container.add_scoped(ProductsRepository)
425+
container.add_scoped(ProductsService)
426+
427+
service = container.resolve(ProductsService)
428+
assert isinstance(service.repository, Repository)
429+
assert isinstance(service.repository, ProductsRepository)
430+
assert isinstance(service.repository.db_connection, DBConnection)
431+
```
296432

297433
## Checking if a type is registered
298434

299-
To check if a type is registered in the container, use the `__contains__` interface:
435+
To check if a type is registered in the container, use the `__contains__`
436+
interface:
300437

301438
```python {linenums="1", hl_lines="11-12"}
302439
from rodi import Container
@@ -313,8 +450,9 @@ assert A in container # True
313450
assert B not in container # True
314451
```
315452

316-
This can be useful to support alternative ways to register types. For example, tests
317-
code can register a mock type for a class, and the code under test can check if any
318-
interface is already registered in the container, and skip the registration if it is.
453+
This can be useful for supporting alternative ways to register types. For
454+
example, test code can register a mock type for a class, and the code under
455+
test can check whether an interface is already registered in the container,
456+
skipping the registration if it is.
319457

320458
The next page explains how to work with [async](./async.md).

rodi/docs/getting-started.md

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,7 @@ However, this approach has several limitations.
7878

7979
- **Scalability Issues**: As the application grows, managing dependencies
8080
manually within classes becomes cumbersome. It can lead to duplicated code
81-
and make the system harder to maintain. As dependencies are likely to require
82-
their own set of parameters passed to their constructors, the parent
83-
constructor would become more and more complex.
81+
and make the system harder to maintain.
8482
- **Tight Coupling**: The `ProductsService` class is tightly coupled to
8583
_concrete_ implementations of its dependencies. This makes it less convenient
8684
to replace `ProductsRepository` and `EmailHandler` with different
@@ -165,7 +163,7 @@ container.add_transient(B)
165163
# resolve B
166164
example = container.resolve(B)
167165

168-
# the container automatically resolves
166+
# the container automatically resolves dependencies
169167
assert isinstance(example, B)
170168
assert isinstance(example.dependency, A)
171169
```
@@ -309,7 +307,7 @@ class SQLProductsRepository(ProductsRepository):
309307
- The **high-level class (`ProductsService`)** implements business logic and
310308
depends on the `ProductsRepository` abstraction.
311309
- `ProductsService` does not depend on the details of how data is stored or
312-
retrieved.
310+
retrieved, and it is not _concerned_ with those details.
313311
- The low-level class (`SQLProductsRepository`) implements the
314312
`ProductsRepository` interface using an SQL database.
315313
- It can be swapped out for another implementation (e.g.,

0 commit comments

Comments
 (0)