Dependency injection and inversion of control in Python¶
Originally, dependency injection pattern got popular in languages with static typing, like Java. Dependency injection framework can significantly improve flexibility of the language with static typing. Also, implementation of dependency injection framework for language with static typing is not something that one can do shortly, it could be quite complex thing to be done well.
While Python is very flexible interpreted language with dynamic typing, there is a meaning that dependency injection doesn’t work for it as well, as it does for Java. Also there is a meaning that dependency injection framework is something that Python developer would not ever need, cause dependency injection could be implemented easily using language fundamentals.
It is true.
Dependency injection, as a software design pattern, has number of advantages that are common for each language (including Python):
- Dependency Injection decreases coupling between a class and its dependency.
- Because dependency injection doesn’t require any change in code behavior it can be applied to legacy code as a refactoring. The result is clients that are more independent and that are easier to unit test in isolation using stubs or mock objects that simulate other objects not under test. This ease of testing is often the first benefit noticed when using dependency injection.
- Dependency injection can be used to externalize a system’s configuration details into configuration files allowing the system to be reconfigured without recompilation (rebuilding). Separate configurations can be written for different situations that require different implementations of components. This includes, but is not limited to, testing.
- Reduction of boilerplate code in the application objects since all work to initialize or set up dependencies is handled by a provider component.
- Dependency injection allows a client to remove all knowledge of a concrete implementation that it needs to use. This helps isolate the client from the impact of design changes and defects. It promotes reusability, testability and maintainability.
- Dependency injection allows a client the flexibility of being configurable. Only the client’s behavior is fixed. The client may act on anything that supports the intrinsic interface the client expects.
While improved testability is one the first benefits of using dependency injection, it could be easily overwhelmed by monkey-patching technique, that works absolutely great in Python (you can monkey-patch anything, anytime). At the same time, monkey-patching has nothing similar with other advantages defined above. Also monkey-patching technique is something that could be considered like too dirty to be used in production.
The complexity of dependency injection pattern implementation in Python is definitely quite lower than in other languages (even with dynamic typing).
Low complexity of dependency injection pattern implementation in Python still means that some code should be written, reviewed, tested and supported.
Talking about inversion of control, it is a software design principle that also works for each programming language, not dependending on its typing type.
Inversion of control is used to increase modularity of the program and make it extensible.
Main design purposes of using inversion of control are:
- To decouple the execution of a task from implementation.
- To focus a module on the task it is designed for.
- To free modules from assumptions about how other systems do what they do and instead rely on contracts.
- To prevent side effects when replacing a module.
Let’s go through next example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
"""Dependency injection example, engines module.""" class Engine(object): """Example engine base class. Engine is a heart of every car. Engine is a very common term and could be implemented in very different ways. """ class GasolineEngine(Engine): """Gasoline engine.""" class DieselEngine(Engine): """Diesel engine.""" class ElectroEngine(Engine): """Electro engine."""
1 2 3 4 5 6 7 8 9
"""Dependency injection example, cars module.""" class Car(object): """Example car.""" def __init__(self, engine): """Initializer.""" self._engine = engine # Engine is injected
Next example demonstrates creation of several cars with different engines:
1 2 3 4 5 6 7 8 9 10
"""Dependency injection example, Cars & Engines.""" import example.cars import example.engines if __name__ == '__main__': gasoline_car = example.cars.Car(example.engines.GasolineEngine()) diesel_car = example.cars.Car(example.engines.DieselEngine()) electro_car = example.cars.Car(example.engines.ElectroEngine())
While previous example demonstrates advantages of dependency injection, there is a disadvantage demonstration as well - creation of car requires additional code for specification of dependencies. Nevertheless, this disadvantage could be easily avoided by using a dependency injection framework for creation of inversion of control container (IoC container).
Example of creation of several inversion of control containers (IoC containers) using Dependency Injector:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
"""Dependency injection example, Cars & Engines IoC containers.""" import example.cars import example.engines import dependency_injector.containers as containers import dependency_injector.providers as providers class Engines(containers.DeclarativeContainer): """IoC container of engine providers.""" gasoline = providers.Factory(example.engines.GasolineEngine) diesel = providers.Factory(example.engines.DieselEngine) electro = providers.Factory(example.engines.ElectroEngine) class Cars(containers.DeclarativeContainer): """IoC container of car providers.""" gasoline = providers.Factory(example.cars.Car, engine=Engines.gasoline) diesel = providers.Factory(example.cars.Car, engine=Engines.diesel) electro = providers.Factory(example.cars.Car, engine=Engines.electro) if __name__ == '__main__': gasoline_car = Cars.gasoline() diesel_car = Cars.diesel() electro_car = Cars.electro()