loosely coupled python code with dependency injection
The software needs to be flexible to respond to change. Dependency injection is a technique for managing dependencies between software components. It is a design pattern that uses inversion of control to provide the ability to swap components for testing or other purposes without changing any other part of the system.
The main idea behind Dependency Injection is to pass the responsibility of providing services from constructor functions or direct instantiation methods to other objects. In other words, the dependencies of an object are given rather than being created. That way, if we want to add new features, we don’t have to change the existing code, but instead add new dependencies to it.
Creating abstractions with interfaces
There are three steps in writing loosely coupled Python code with dependency injection. The first step is to identify which dependencies the code needs, the second step is to create interfaces for each dependency, and the third step is to pass them to the dependent object through its constructor or method parameters.
Recently, I implemented functionality that requires a byte encoder and decoder to manipulate business data (I’ve simplified its implementation for readability purposes) I ended his responsibilities for the interface. Python doesn’t have an interface keyword like golang, but you can replicate interface functionality with the abc library .
Focusing on the IPacketEncoder interface, the concrete Encoder class must implement it. Initial business requirements required all strings to be encoded and padded with their length. Fortunately, Python provides struct libraries to do most of the hard work.
implement dependency injection
The encode_name function is a simple implementation to create an encoded payload with a specified name. It takes an IPacketEncoder object and a string as parameters.
Two major implementation details are using the IPacketEncoder interface instead of the PaddedPacketEncoder concrete class and having an argument instead of being initialized inside the encoder function. This is dependency injection. Because IPacketEncoder is passed, it can be easily extended, replaced, or provided with a stub, thus making the system loosely coupled.
A few weeks later, there was a new requirement for an encoder to handle null terminated encoding. Fortunately, due to the existing implementation, it can be easily implemented.
NullTerminatingPacketEncoder essentially adds NULL_BYTE after each encoded string. Returning to the encode_name function, an instance of NullTerminatingPacketEncoder is a valid encoder argument because NullTerminatingPacketEncoder implements the same IPacketEncoder interface as PaddedPacketEncoder.
If dependency injection was not used here, it could result in code that is tightly coupled and difficult to change, for example:
If a new encoder is needed, the encode_name function will have to be replaced almost entirely with its tests as well, increasing the delivery time. What’s worse is that it can have hidden side effects, especially if the original developer is no longer available.
To conclude, I will provide a less simple example using this technique. The SocketService is responsible for decoding an operation’s code, calling its corresponding handler, and returning its result. All its argument types including its constructor are interface abstractions. The CLI is responsible for determining which encoder to use, i.e. null terminated or padded, and passing an instance of it to the SocketService class.
As projects continue to grow, it has been recommended to “inject” these dependencies using a dependency injection framework, such as Dependency Injector, to automatically inject dependency arguments.
Hopefully, it shows how to write loosely coupled Python code with dependency injection. Many Python developers dislike dependency injection because they think it is not “Pythonic”, but I would argue that its benefits cannot be discredited, especially as it is present in many other programming languages.
Thanks for reading If you want to connect with me, you can find me on LinkedIn or Github.