diff --git a/README.rst b/README.rst index 33b1b570..11216087 100644 --- a/README.rst +++ b/README.rst @@ -57,14 +57,25 @@ It helps implementing the dependency injection principle. What is dependency injection? ----------------------------- -Dependency injection is a principle that helps to decrease coupling and increase cohesion. Your -code becomes more flexible, clear and it is easier to test it. +Dependency injection is a principle that helps to decrease coupling and increase cohesion. + +What is coupling and cohesion? + +Coupling and cohesion are about how tough the components are tied. + +- **High coupling**. If the coupling is high it's like using a superglue or welding. No easy way + to disassemble. +- **High cohesion**. High cohesion is like using the screws. Very easy to disassemble and + assemble back or assemble a different way. It is an alternative to high coupling. + +When the cohesion is high the coupling is low. + +High cohesion brings the flexibility. Your code becomes easier to change and test. How to implement dependency injection? -------------------------------------- -Objects do not create each other anymore. They provide a way to inject the needed dependencies -instead. +Objects do not create each other anymore. They provide a way to inject the dependencies instead. Before: @@ -76,14 +87,14 @@ Before: class ApiClient: def __init__(self): - self.api_key = os.getenv('API_KEY') - self.timeout = os.getenv('TIMEOUT') + self.api_key = os.getenv('API_KEY') # <-- the dependency + self.timeout = os.getenv('TIMEOUT') # <-- the dependency class Service: def __init__(self): - self.api_client = ApiClient() + self.api_client = ApiClient() # <-- the dependency if __name__ == '__main__': @@ -100,28 +111,36 @@ After: class ApiClient: def __init__(self, api_key: str, timeout: int): - self.api_key = api_key - self.timeout = timeout + self.api_key = api_key # <-- the dependency is injected + self.timeout = timeout # <-- the dependency is injected class Service: def __init__(self, api_client: ApiClient): - self.api_client = api_client + self.api_client = api_client # <-- the dependency is injected if __name__ == '__main__': service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT'))) -Flexibility comes with a price: now you need to assemble your objects like this +``ApiClient`` is decoupled from knowing where the options come from. You can read a key and a +timeout from a configuration file or even get them from a database. + +``Service`` is decoupled from the ``ApiClient``. It does not create it anymore. You can provide a +stub or other compatible object. + +Flexibility comes with a price. + +Now you need to assemble your objects like this ``Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))``. The assembly code might get duplicated and it'll become harder to change the application structure. What does Dependency Injector do? --------------------------------- -``Dependency Injector`` helps you assemble the objects. +``Dependency Injector`` helps to assemble the objects. It provides you the container and the providers that help you describe objects assembly. When you need an object you get it from the container. The rest of the assembly work is done by the @@ -170,8 +189,11 @@ framework: Retrieving of the ``Service`` instance now is done like this ``container.service()``. -Also ``Dependency Injector`` provides a bonus in overriding any of the providers with the -``.override()`` method: +The responsibility of assembling the object is consolidated in the container. When you need to +make a change you do it in one place. + +When doing the testing you call the ``container.api_client.override()`` to replace the real API +client with a mock: .. code-block:: python @@ -180,7 +202,6 @@ Also ``Dependency Injector`` provides a bonus in overriding any of the providers with container.api_client.override(mock.Mock()): service = container.service() - assert isinstance(service.api_client, mock.Mock) It helps in a testing. Also you can use it for configuring project for the different environments: replace an API client with a stub on the dev or stage. @@ -217,7 +238,7 @@ Concept - Explicit is better than implicit (PEP20). - Do no magic to your code. -How does it different from the other frameworks? +How is it different from the other frameworks? - **No autowiring.** The framework does NOT do any autowiring / autoresolving of the dependencies. You need to specify everything explicitly. Because *"Explicit is better than implicit" (PEP20)*. - **Does not pollute your code.** Your application does NOT know and does NOT depend on the framework. No ``@inject`` decorators, annotations, patching or any other magic tricks. diff --git a/docs/introduction/images/coupling-cohesion.png b/docs/introduction/images/coupling-cohesion.png new file mode 100644 index 00000000..4cf4c98c Binary files /dev/null and b/docs/introduction/images/coupling-cohesion.png differ diff --git a/docs/introduction/what_is_di.rst b/docs/introduction/what_is_di.rst index 82392029..1288bfcb 100644 --- a/docs/introduction/what_is_di.rst +++ b/docs/introduction/what_is_di.rst @@ -1,125 +1,190 @@ -What is dependency injection and inversion of control? ------------------------------------------------------- +What is dependency injection? +----------------------------- .. meta:: - :keywords: Python,DI,Dependency injection,IoC,Inversion of Control - :description: This article provides definition of dependency injection, - inversion of control and dependency inversion. It contains - example code in Python that is refactored to be following - inversion of control principle. + :keywords: Python,DI,Dependency injection,Low coupling,High cohesion + :description: This page provides a Python example of what is dependency injection. It tells + about benefits of coupling and high cohesion. -Definition -~~~~~~~~~~ +Dependency injection is a principle that helps to decrease coupling and increase cohesion. -Wikipedia provides quite good definitions of dependency injection pattern -and related principles: +.. image:: images/coupling-cohesion.png -.. glossary:: +What is coupling and cohesion? - `Dependency injection`_ - In software engineering, dependency injection is a software design - pattern that implements inversion of control for resolving - dependencies. A dependency is an object that can be used (a service). - An injection is the passing of a dependency to a dependent object (a - client) that would use it. The service is made part of the client's - state. Passing the service to the client, rather than allowing a - client to build or find the service, is the fundamental requirement of - the pattern. +Coupling and cohesion are about how tough the components are tied. - Dependency injection allows a program design to follow the dependency - inversion principle. The client delegates to external code (the - injector) the responsibility of providing its dependencies. The client - is not allowed to call the injector code. It is the injecting code - that constructs the services and calls the client to inject them. This - means the client code does not need to know about the injecting code. - The client does not need to know how to construct the services. The - client does not need to know which actual services it is using. The - client only needs to know about the intrinsic interfaces of the - services because these define how the client may use the services. - This separates the responsibilities of use and construction. +- **High coupling**. If the coupling is high it's like using a superglue or welding. No easy way + to disassemble. +- **High cohesion**. High cohesion is like using the screws. Very easy to disassemble and + assemble back or assemble a different way. It is an alternative to high coupling. - `Inversion of control`_ - In software engineering, inversion of control (IoC) describes a design - in which custom-written portions of a computer program receive the - flow of control from a generic, reusable library. A software - architecture with this design inverts control as compared to - traditional procedural programming: in traditional programming, the - custom code that expresses the purpose of the program calls into - reusable libraries to take care of generic tasks, but with inversion - of control, it is the reusable code that calls into the custom, or - task-specific, code. +When the cohesion is high the coupling is low. - Inversion of control is used to increase modularity of the program and - make it extensible, and has applications in object-oriented - programming and other programming paradigms. The term was popularized - by Robert C. Martin and Martin Fowler. +High cohesion brings the flexibility. Your code becomes easier to change and test. - The term is related to, but different from, the dependency inversion - principle, which concerns itself with decoupling dependencies between - high-level and low-level layers through shared abstractions. +The example +~~~~~~~~~~~ - `Dependency inversion`_ - In object-oriented programming, the dependency inversion principle - refers to a specific form of decoupling software modules. When - following this principle, the conventional dependency relationships - established from high-level, policy-setting modules to low-level, - dependency modules are reversed, thus rendering high-level modules - independent of the low-level module implementation details. The - principle states: +How does dependency injection helps to achieve high cohesion? - + High-level modules should not depend on low-level modules. - Both should depend on abstractions. - + Abstractions should not depend on details. - Details should depend on abstractions. +Objects do not create each other anymore. They provide a way to inject the dependencies instead. - The principle inverts the way some people may think about - object-oriented design, dictating that both high- and low-level - objects must depend on the same abstraction. +Before: -Example -~~~~~~~ +.. code-block:: python -Let's go through the code of ``example.py``: + import os -.. literalinclude:: ../../examples/di_demo/example.py - :language: python -At some point, things defined above mean, that the code from ``example.py``, -could look different, like in ``example_di.py``: + class ApiClient: -.. literalinclude:: ../../examples/di_demo/example_di.py - :language: python + def __init__(self): + self.api_key = os.getenv('API_KEY') # <-- the dependency + self.timeout = os.getenv('TIMEOUT') # <-- the dependency -Best explanation, ever -~~~~~~~~~~~~~~~~~~~~~~ -Some times ago `user198313`_ posted awesome `question`_ about dependency -injection on `StackOverflow`_: + class Service: -.. note:: + def __init__(self): + self.api_client = ApiClient() # <-- the dependency - How to explain dependency injection to a 5-year-old? -And `John Munsch`_ provided absolutely Great answer: + if __name__ == '__main__': + service = Service() -.. note:: - When you go and get things out of the refrigerator for yourself, you can +After: + +.. code-block:: python + + import os + + + class ApiClient: + + def __init__(self, api_key: str, timeout: int): + self.api_key = api_key # <-- the dependency is injected + self.timeout = timeout # <-- the dependency is injected + + + class Service: + + def __init__(self, api_client: ApiClient): + self.api_client = api_client # <-- the dependency is injected + + + if __name__ == '__main__': + service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT'))) + +``ApiClient`` is decoupled from knowing where the options come from. You can read a key and a +timeout from a configuration file or even get them from a database. + +``Service`` is decoupled from the ``ApiClient``. It does not create it anymore. You can provide a +stub or other compatible object. + +Flexibility comes with a price. + +Now you need to assemble your objects like this +``Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT')))``. The assembly code might get +duplicated and it'll become harder to change the application structure. + +Here comes the ``Dependency Injector``. + +``Dependency Injector`` helps to assemble the objects. + +It provides you the container and the providers that help you describe objects assembly. When you +need an object you get it from the container. The rest of the assembly work is done by the +framework: + +.. code-block:: python + + from dependency_injector import containers, providers + + + class ApiClient: + + def __init__(self, api_key: str, timeout: int): + self.api_key = api_key + self.timeout = timeout + + + class Service: + + def __init__(self, api_client: ApiClient): + self.api_client = api_client + + + class Container(containers.DeclarativeContainer): + + config = providers.Configuration() + + api_client = providers.Singleton( + ApiClient, + api_key=config.api_key, + timeout=config.timeout.as_int(), + ) + + service = providers.Factory( + Service, + api_client=api_client, + ) + + + if __name__ == '__main__': + container = Container() + container.config.api_key.from_env('API_KEY') + container.config.timeout.from_env('TIMEOUT') + + service = container.service() + +Retrieving of the ``Service`` instance now is done like this ``container.service()``. + +Objects assembling is consolidated in the container. When you need to make a change you do it in +one place. + +When doing the testing you call the ``container.api_client.override()`` to replace the real API +client with a mock: + +.. code-block:: python + + from unittest import mock + + + with container.api_client.override(mock.Mock()): + service = container.service() + +How to explain dependency injection to a 5-year-old? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Some time ago `user198313`_ posted this `question`_ on the `StackOverflow`_. + +`John Munsch`_ provided a great answer: + + *When you go and get things out of the refrigerator for yourself, you can cause problems. You might leave the door open, you might get something Mommy or Daddy doesn't want you to have. You might even be looking for - something we don't even have or which has expired. + something we don't even have or which has expired.* - What you should be doing is stating a need, "I need something to drink + *What you should be doing is stating a need, "I need something to drink with lunch," and then we will make sure you have something when you sit - down to eat. + down to eat.* +What's next? +~~~~~~~~~~~~ + +Choose one of the following as a next step: + ++ Pass one of the tutorials: + + :ref:`cli-tutorial` + + :ref:`flask-tutorial` + + :ref:`aiohttp-tutorial` + + :ref:`asyncio-daemon-tutorial` ++ Know more about the :ref:`providers` ++ Go to the :ref:`contents` .. disqus:: - -.. _Dependency injection: http://en.wikipedia.org/wiki/Dependency_injection -.. _Inversion of control: https://en.wikipedia.org/wiki/Inversion_of_control -.. _Dependency inversion: https://en.wikipedia.org/wiki/Dependency_inversion_principle .. _StackOverflow: http://stackoverflow.com/ .. _question: http://stackoverflow.com/questions/1638919/how-to-explain-dependency-injection-to-a-5-year-old/1639186 .. _user198313: http://stackoverflow.com/users/198313/user198313 diff --git a/docs/main/changelog.rst b/docs/main/changelog.rst index 3f5a0db0..552908b7 100644 --- a/docs/main/changelog.rst +++ b/docs/main/changelog.rst @@ -7,6 +7,12 @@ that were made in every particular version. From version 0.7.6 *Dependency Injector* framework strictly follows `Semantic versioning`_ +3.38.0 +------ +- Update "What is What is dependency injection?" documentation page. +- Update README. +- Fix a bunch of typos. + 3.37.0 ------ - Update index documentation page. diff --git a/examples/di_demo2/demo.py b/examples/di_demo/demo.py similarity index 100% rename from examples/di_demo2/demo.py rename to examples/di_demo/demo.py diff --git a/examples/di_demo/example.py b/examples/di_demo/example.py deleted file mode 100644 index 8f4a4234..00000000 --- a/examples/di_demo/example.py +++ /dev/null @@ -1,17 +0,0 @@ -"""The Code.""" - - -class Service: - """The Service.""" - - -class Client: - """The Client that uses the Service.""" - - def __init__(self): - """Initialize the Client.""" - self.service = Service() # The Service is created by the Client - - -if __name__ == '__main__': - client = Client() # Application creates the Client diff --git a/examples/di_demo/example_di.py b/examples/di_demo/example_di.py index ddbbe14b..db85c237 100644 --- a/examples/di_demo/example_di.py +++ b/examples/di_demo/example_di.py @@ -1,18 +1,18 @@ -"""The Code that demonstrates dependency injection pattern.""" +import os + + +class ApiClient: + + def __init__(self, api_key: str, timeout: int): + self.api_key = api_key + self.timeout = timeout class Service: - """The Service.""" - -class Client: - """The Client that uses the Service.""" - - def __init__(self, service): # The Service is injected into the Client - """Initialize the Client.""" - self.service = service + def __init__(self, api_client: ApiClient): + self.api_client = api_client if __name__ == '__main__': - service = Service() # Application creates the Service - client = Client(service) # and inject the Service into the Client + service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT'))) diff --git a/examples/di_demo2/example_no_di.py b/examples/di_demo/example_no_di.py similarity index 100% rename from examples/di_demo2/example_no_di.py rename to examples/di_demo/example_no_di.py diff --git a/examples/di_demo2/test.py b/examples/di_demo/test.py similarity index 100% rename from examples/di_demo2/test.py rename to examples/di_demo/test.py diff --git a/examples/di_demo2/example_di.py b/examples/di_demo2/example_di.py deleted file mode 100644 index db85c237..00000000 --- a/examples/di_demo2/example_di.py +++ /dev/null @@ -1,18 +0,0 @@ -import os - - -class ApiClient: - - def __init__(self, api_key: str, timeout: int): - self.api_key = api_key - self.timeout = timeout - - -class Service: - - def __init__(self, api_client: ApiClient): - self.api_client = api_client - - -if __name__ == '__main__': - service = Service(ApiClient(os.getenv('API_KEY'), os.getenv('TIMEOUT'))) diff --git a/src/dependency_injector/__init__.py b/src/dependency_injector/__init__.py index 177dd55b..2eeec765 100644 --- a/src/dependency_injector/__init__.py +++ b/src/dependency_injector/__init__.py @@ -1,6 +1,6 @@ """Dependency injector top-level package.""" -__version__ = '3.37.0' +__version__ = '3.38.0' """Version number that follows semantic versioning. :type: str