DI is a popular design technique implemented in many modern-day frameworks. It is a personal favorite of mine because DI allows us to decouple usage from creation, which in return helps us to follow SOLID’s principles. In this post, we are going to explore a simple example and expand on it to see how we can benefit from DI along with a few SOLID principles.
Let us start with some context. In our sample class below, we have a simple method, accepting a string key as well as applying two if statements. If a condition is met an appropriate string will be returned. We are going to develop this class by adding and expanding requirements while using DI in conjunction with other patterns and principles to solve our requirements.
In our exaggerated example, a business owner recognizes requirements to add three extra keys as well as the ability to return the string in the save casing the key was received in. The business owner also suggested that each key could have different requirements in the future. From the code below we see the two if statements increased with a few lines each. With time customization to this code will be harder to read and maintain. Each if statement will also result in a test to cover all conditions.
Before we get started with DI, let’s refactor by using a simple pattern to isolate the logic for each key and remove the if statement blocks. This will help to prepare your code for DI and increase maintainability.
In the code below we moved each key’s logic into separate classes, each using an interface IStrategy, this is going to help us resolve any strategy without being concerned about its initialization or implementation. The class ToDoTask_v3 now has a Container property of type dictionary containing a list of the Keys and associated strategies (each key’s logic). Using the input key parameter; the Do method will resolve the appropriate strategy from the dictionary, execute it and then return the strategy result.
With this refactoring in place, we avoided creating a long and nested if statement, enabled developers to develop and customize different strategies concurrently and avoided potential code conflicts. New code changes are less likely to break unrelated tests because the separation of strategies can now execute independently in separate tests, specific to the strategy.
Now let’s get to some basic DI
According to Wikipedia: In software engineering, dependency injection is a technique whereby one object supplies the dependencies of another object. A “dependency” is an object that can be used, for example as a service. Instead of a client specifying which service it will use, something tells the client what service to use. The “injection” refers to the passing of a dependency (a service) into the object (a client) that would use it. Passing the service to the client, rather than allowing a client to build or find the service, is the fundamental requirement of the pattern.
At this point, we already decoupled creation from usage, the creation is maintained by our dictionary and the usage by the class ToDoTask_v3. The dictionary supplies the class ToDoTask_v3 with the appropriate initialized object.
In the code below we swapped out the dictionary with a DI framework called Autofac. In a real-world scenario, the container creation found in the constructor of the class ToDoTask_v4 will be done at startup and the container will possibly be static across the application. Notice we are no longer initializing the strategies; we only register them with the container builder, and we use the key to resolve an initialized strategy in the same way we did with the dictionary in the Do method.
We can now expand by registering more classes with the container builder and add them to the constructors of the strategies. This will help us when expanding our requirements in an agile manner giving us the flexibility we need to maintain and develop future requirements. For example, we can introduce a new business rule in an isolated class and inject it into any strategy we wish. To demonstrate this, we isolate the casing logic to a class called SwapCasing and inject it into the constructors of each strategy.
Where are the principles?
The Open-Closed Principle is not very clear in this example however the principle is encouraged through building logic in classes and extending the functionality through DI rather than modification. Notice how DI allowed us to apply the single responsibility principle and take full advantage of the reusability and maintainability the principle has to offer, the casing logic has been reduced to a single place and is now injectable in any class registered with the container. We also see the advantages of dependency inversion where the Do method uses the IStrastegy interface and has no concern over the implementations of the strategies making the method more abstract. Liskov Substitution Principle is also enabled by the DI framework allowing us to interchange between strategies and eliminating the need for if statements complicating the code and tests.
The code is available on Github