Abstract types are a way to define a common interface for a set of classes. This allows
you to write code that works with any class that implements the interface, without
needing to know the details of the implementation. When registering a type in a
Container, you can specify the base interface which is used as key to resolve
concrete types, and the implementation type which is used to create the instance. This
is useful when it is desirable to use the same interface for different implementations,
or when you want to switch to a different implementation in the future without changing
the code that relies on the interface.
Using ABC and abstractmethod
is not strictly necessary, but it is recommended for defining interfaces.
This ensures that any class implementing the interface has the required methods.
If you decide on using a normal class to describe the interface, Rodi requires the
concrete class to be a subclass of the interface.
Otherwise, you can use a Protocol from the
typing module to define the interface. In this case, Rodi allows registering a
protocol as the interface and a normal class that does not inherit it (which aligns with
the original purpose of Python's Protocol).
Rodi raises an exception if we try registering a normal class as interface, with a
concrete class that does not inherit it.
Protocols validation.
Rodi does not validate implementations of Protocols. This means that if you register
a class that does not implement the methods of the Protocol, Rodi will not raise an
exception. Support for Protocols validation might be added in the future, but for now,
you should ensure that the classes you register do implement the methods of the
Protocol.
When working with abstract types, the interface type (or protocol) must always be
used as the key type. The implementation type is used to create the instance, but it
is not used as a key to resolve the type. This is according to the Dependency
Inversion Principle, which states
that high-level modules should not depend on low-level modules, but both should depend
on abstractions.
classMyInterface(ABC):@abstractmethoddefdo_something(self)->str:passclassMyClass(MyInterface):defdo_something(self)->str:return"Hello, world!"defmy_factory()->MyClass:# <-- No. This is a mistake.returnMyClass()container.add_transient_by_factory(my_factory)# <-- MyClass is used as Key.
fromdataclassesimportdataclassfromtypingimportGeneric,List,TypeVarfromrodiimportContainerT=TypeVar("T")classRepository(Generic[T]):"""A generic repository for managing entities of type T."""def__init__(self):self._items:List[T]=[]defadd(self,item:T):"""Add an item to the repository."""self._items.append(item)defget_all(self)->List[T]:"""Retrieve all items from the repository."""returnself._items# Define specific entity classes@dataclassclassProduct:id:intname:str@dataclassclassCustomer:id:intemail:strfirst_name:strlast_name:str# Set up the containercontainer=Container()# Register repositoriescontainer.add_scoped(Repository[Product],Repository)container.add_scoped(Repository[Customer],Repository)# Resolve and use the repositoriesproduct_repo=container.resolve(Repository[Product])customer_repo=container.resolve(Repository[Customer])# Add and retrieve productsproduct_repo.add(Product(1,"Laptop"))product_repo.add(Product(2,"Smartphone"))print(product_repo.get_all())# Add and retrieve customerscustomer_repo.add(Customer(1,"alice@wonderland.it","Alice","WhiteRabbit"))customer_repo.add(Customer(1,"bob@foopower.it","Bob","TheHamster"))print(customer_repo.get_all())
Note how the generics Repository[Product] and Repository[Customer] are both
configured to be resolved using Repository as concrete type. In Python,
instances of GenericAlias are not considered as actual classes. The following
wouldn't work:
The following wouldn't work, because the Container will look exactly for the
key Repository[T] when instantiating the ProductsService, not for
Repository[Product]:
This can be useful for supporting alternative ways to register types. For
example, test code can register a mock type for a class, and the code under
test can check whether an interface is already registered in the container,
skipping the registration if it is.