tech ramblings

Deconstructing Dependency Principles: Inversion and Injection

In software engineering, the word "dependency" gets thrown around a lot, and so I want to explain two interesting and related topics - dependency inversion principle and dependency injection. Let's look at what these are and why we might care about them.

Dependency Inversion

Dependency inversion is the "D" in SOLID principles. The idea is simple. If you're a higher level module, and you depend on a lower level module, the natural tendency is to make the higher level module depend on the lower level module. This is the natural direction of dependency. The "inversion" aims to reverse this direction.

Example 1 - Your application depends on a database. One option is to depend on a MySQL client directly. But, we can invert the dependency by depending on a higher-level abstraction of a DB Client, instead. What this does is introduces an element of flexibility. If you decide to swap out MySQL for Postgres later on, you just have to write a Postgres client that conforms to the DB Client abstraction, and you're good to go. (I know I know YAGNI. It's very rare that you're going to change databases like that, but the example is easy to grasp)

Example 2 - Your e-commerce store application depends on third-party payment gateway API, say Stripe, for accepting payments. Instead of calling Stripe APIs directly, your application could instead depend on a PaymentProcessor interface. You can then have multiple payment gateway APIs implement this interface. Now, you have the flexibility to swap out one payment gateway for another, let's say, if one of them is down.

class EcommerceApplicationCoupledToStripe:
    def __init__(self, stripeProcessor):
        pass

class EcommerceApplicationDeCoupledToPaymentProcessor:
    def __init__(self, paymentProcessorInterface):
        pass

In the above example, you want to use the second class implementation instead of the first. The core idea is that you're depending on an abstraction, rather than a concrete instance. As a result, your code is more extensible and less tightly coupled!

Dependency Injection

Dependency injection is one of the mechanisms by which you can apply the dependency inversion principle. Fundamentally, it's the practice of "injecting" the dependencies of a class(i.e. the other services that it depends on) into the class via the constructor or setter methods. This is opposed to the practice of merely creating the dependencies within the class. Now, why would you want to do this? Flexibility. You want to separate the concern of creating the dependency from using it. This allows you to control how you create the dependency object independent of the classes that use it.

For example, maybe you want to swap out the stripeProcessor with a paypalProcessor, or while writing tests, you create a mock processor class to fake the payment processing operations. Injecting the dependencies makes it very easy to accomplish these tasks. You just have to create another class, and inject an instance into the EcommerceApplication class. On the other hand, if the stripeProcessor was instantiated within the EcommerceApplication class, it would take much more effort to do the same. Chances are you would have probably either messed up the code, or arrived at a solution involving injecting the dependencies.

To sum up..

These two concepts are fundamentally about different things but are intricately related. To make your code more flexible, you want to inject your dependencies, not create them within the class. And to make your code more extensible, you want to depend not on lower level concrete implementations, but rather on higher-level (i.e. inversion) abstractions.