As a software engineer, your objective is to built software in a way it is modular and easy to extend. There are a few general factors that one should take into consideration:
- separation of concerns
- low coupling
- high cohesion
Dependency Injection is a technique that favours these factors and in this blog post, I will try to explain why this is the case and how to use dependency injection with Python to improve your daily life and produced software.
It is a known fact DI is not widely used inside Python mostly because of its scripting nature but you as an experienced developer should already know that Python is not only a scripting language but is widely used in professional software development as well. So give me a chance to convince you π
Dependency injection is a technique built on the top of the Inversion of Control. The main idea is to separate the construction and usage of objects.
Let’s consider the following example:
|
|
S3FileUploader
is an implementation of FileUploader
interface and is using boto s3 sdk client
.
This relationship is called a “dependency”.
In the given example boto3.client("s3")
construction is hardcoded inside S3FileUploader
initialiser.
Which might be an indication of a bad design.
The solution here is to delegate the responsibility related to initialising an object and inject the object initialised this way as our dependency.
Global state
Letβs be honest, the example above is not the worst-case scenario. Many times I’ve seen examples where a class depended on something from a global state (been there, done that, learnt my lesson π ).
|
|
Please do not rely on a global state β
There are plenty of conversations on the internet talking about why this is bad. Allow me to explain why this is not a good idea:
- it breaks encapsulation - any other object can change the state of it
- testing is much harder - requires a lot of mocks flying around
Dependency injection library
Let’s revisit our previous example and apply the dependency injection technique.
|
|
Isn’t it better? Now it’s clearly visible what is required for this class to work.
To achieve this we will need some dependency injection library. In all of my projects I use kink (https://github.com/kodemore/kink) - it’s a library created by my friend π
In my opinion, it’s a very flexible, friendly, and easy-to-use Python library. I encourage you to check out the GitHub page.
Examples
Setup
I always follow the convention where all my DI definitions are inside a file called bootstrap.py
This is something that I came up with during a conversation with one of my friends, so if you have any better approaches, please let me know, I’m open to discussion.
|
|
Then in the main project directory inside __init__.py
I invoke the above function.
|
|
So right now I’m sure that all of my defined dependencies will be registered inside the dependency injection container.
In the case of kink, the dependency injection container is like a Python dict object. So you can add new dependencies as you are adding new values to the Python dict which is a cool feature in my opinion.
Also, you can register your dependencies in two ways:
- by type
- by name
So you can do both:
|
|
Mainly I’m using the second one because I’m a fan of typing in Python π
As you have seen some of my definitions inside bootstrap_di
function are using the lambda function.
It’s because kink supports on-demand service creation.
It means that the creation of our dependency will not be executed until it is requested.
Ok, that’s all when it comes to setting up our DIC, it’s pretty simple, isn’t it?
Usage
Ok, all of our services/dependencies are defined and waiting for usage. Let see how we can apply this to our code!
|
|
We used the @inject
decorator which is doing auto wiring of our dependencies.
Generally speaking, auto wiring is a functionality that checks what’s inside the DIC,
and then if the type or name matches with what is defined inside the object’s initialiser
then the kink will do the job and will inject exactly what is needed.
Simple, right? This is called constructor injection
but with kink, we can also do the same with functions.
Let’s consider another example.
|
|
Again the rules are the same as for constructor injection
, kink will do the job and will resolve our dependencies automatically.
Benefits of using DI
- It’s much easier to follow SRP (Single Responsibility Principle)
- The code is more reusable - you can inject your services in many places
- It’s much easier to test - you can inject mocks or test doubles of your dependencies
- The code is more readable - you are looking only at behaviors of your components
- It can improve the modularity of your application
And much more …
Problems with DI
DI will not resolve all the problems automatically for you. As a developer, you have to be aware of the responsibilities and roles of your components.
There are far too many dependencies.
The main problem is the greed of our components. So with an easy way to inject dependencies we are injecting “almost everything” into our component. What do you think, is this component doing only one thing? I will tell you - if it needs to be aware of so many dependencies then it’s definitely not doing one thing, this is against SRP. That’s another indicator of bad design but we don’t see it at first glance because we are happy with the ease of use of our DIC.
Greedy components should be refactored!
Consider the following example:
|
|
It’s obvious, this controller does not have one responsibility, this is typical DI abuse. The above controller needs to be aware of many dependencies and has to handle many aspects of the business logic. Such example should be considered a bad design and DI misuse.
As it is with everything in life if you misuse the DI you can get your project in trouble. So in the end, you will end up with less readable code, it will be more difficult to manage, and you will lose all the benefits that I mentioned above. The final result will be counterproductive.
I would like to mention Uncle Bob’s tweet, pretty old, but I think it explains it better than everything that I can bring you to the table.
IoC is a good idea. Like all good ideas, it should be used with moderation.
— Uncle Bob Martin (@unclebobmartin) March 5, 2013
Original link: https://twitter.com/unclebobmartin/status/308980513929035776
Other libraries
On the Python market, there are few other libraries for DI which look promising.
Links
- https://github.com/kodemore/kink
- https://en.wikipedia.org/wiki/Dependency_injection
- https://martinfowler.com/articles/injection.html
Summary
I hope the main idea behind DI is more clear for you after this blog post. And I hope you see the added value of this. I encourage you to try kink and DI π
If you have any question/thoughts/comments please don’t hesitate to ping me π
Most of the code examples above I took either from my personal or commercial projects. But if you would like to check the DI usage in a wider context, and within some real problem, you can visit my project on GitHub https://github.com/szymon6927/surebets-finder/tree/master - I used DI with kink there.
PS. This post has been also published on my company’s blog (https://www.netguru.com/blog/dependency-injection-with-python-make-it-easy)