In this post, we will delve into a popular software design principle referred to as the Dependency Inversion Principle and, more specifically, the practice of Inversion of Control, or "IoC". We will touch lightly on how we can achieve Inversion of Control using a few common implementation techniques while introducing an interesting hybrid solution which may prove useful in your next application development project.
Inversion of Control refers to the “control flow”, or “flow of control”, for an application – how the application executes, what gets executed, when does it execute, and how these decisions are made. Inversion of Control is the separation of these concerns from the application logic into a construct responsible for managing decisions for the application.
Using Dependency Injection, we can achieve Inversion of Control by allowing dependency references to be defined externally and provided to the application logic rather than letting the application take control over the designation of dependencies internally.
Figure 1 (below) shows an example implementation for an application service that disregards the design principle behind Inversion of Control:
<p> CODE: https://gist.github.com/thec2group-blog/ded30930414de759f12bf1037e54ac31.js</p>
Figure 1: UserCalendarService not using dependency injection.
The UserCalendarService will be responsible for retrieving calendar data for any user for display on a shared calendar application. To fulfill these requirements, the UserCalendarService declares references to two dependency services. Access to user information is provided by the UserService implementation of the IUserService contract and access to calendar data is provided by the CalendarRepository implementation of the ICalendarRepository contract.
With respect to the topic of this post, our focus will be on how these references are obtained by the UserCalendarService rather than the actual implementation of these services.
Notice that the class definition in Figure 1 not only declares its dependencies as a requirement, but then also decides on the implementations for these dependencies by instantiating references for them directly in the class constructor.
Figure 2 (below) shows an example implementation of our application service that takes advantage of Dependency Injection to obtain the required dependency references:
<p> CODE: https://gist.github.com/thec2group-blog/8180f3d0a441b32871fbb335bda532fe.js</p>
Figure 2: UserCalendarService using dependency injection.
The technique used in the implementation shown in Figure 2 (above) is a common Dependency Injection technique referred to as constructor injection. By refactoring our application service to accept dependency references as constructor parameters, rather than defining the dependency references ourselves, we are removing the knowledge of how the dependencies are defined entirely from our class definition.
Now let’s look at a common alternative to using Dependency Injection to achieve Inversion of Control known as the Service Locator Pattern.
Like Dependency Injection, the Service Locator Pattern allows dependencies to be defined externally and apart from the application logic. But rather than injecting, these dependencies are made available upon request by the application.
Figure 3 (below) shows an example implementation of our application service that takes advantage of the Service Locator Pattern to obtain the required dependency references:
<p> CODE: https://gist.github.com/thec2group-blog/6b5402608a6473dd3412a5bfc30804f5.js</p>
Figure 3: UserCalendarService using a service locator.
The ServiceLocator shown in Figure 3 (above), provides a GetService method which accepts a Type as a parameter and, in return, responds with an appropriate Object reference of the specified Type. In each instance, we cast the Object reference given to us by the ServiceLocator to the Type we’ve requested, and then assign each reference to our private member variables, respectively.
Preference over the Inversion of Control strategy selected, whether it be Dependency Injection, a Service Locator Pattern, or by some other means may depend on the situation and nature of the application or component. There are many implementations available for use depending on the development platform and environment, and, although each implementation is striving for a common goal, each comes with varied sets of features, configuration options, syntaxes, behaviors and caveats. Additionally, it is not uncommon for development frameworks to be biased toward one strategy over another. In some cases, they may provide a prepackaged integrated means of enforcing Inversion of Control that may dictate the strategy for use with for your application. We can see this happening in our implementation for our UserCalendarService.
How we’ve implemented our UserCalendarService looks straightforward. However, if we look at our previous example, which claims to have achieved Inversion of Control by implementing the Service Locator Pattern, you might notice that the ServiceLocator itself is in fact a dependency in our application!
<p> CODE: https://gist.github.com/thec2group-blog/2e28f1f918e9c12552373ad6377a8f87.js</p>
This introduces an interesting problem. The design pattern, as we’ve implemented it, to decouple our application logic from our dependencies, in a sense, contradicts itself.
What we need is a service contract: an interface that specifies the requirements for our service locator. The service contract's sole responsibility is providing references for our UserCalendarService’s dependency requirements as well as for any other services in our application.
The service contract will simply be an abstraction of the methods required for a service locator. We need the ability to specify a Type for any dependency we need and, in return, receive a concrete dependency reference that implements the Type we’ve specified. The interface for our service locator might look something like the following:
<p> CODE: https://gist.github.com/thec2group-blog/4d093c47b9bf5c3f8cc16a948001b190.js</p>
Figure 4: IServiceProvider defining the requirements for our service locator.
Now that we’re able to treat our service locator just like any other dependency, let’s look at how our UserCalendarService can be implemented to decouple our application from our ServiceLocator dependency:
<p> CODE: https://gist.github.com/thec2group-blog/eb275271e60dbfe27e0bdf7f0e843df9.js</p>
Figure 5: UserCalendarService using IServiceProvider as an injectable service locator.
We did it! Our UserCalendarService now uses a hybrid approach that combines constructor Dependency Injection with the Service Locator Pattern to achieve Inversion of Control.
In situations where you may have ended up with bloated service constructors using constructor injection, or with hard-coded ServiceLocator references scattered throughout your application, you now have a standardized service constructor pattern. Move forward with confidence knowing that if your service locator implementation needs to change for any reason, all that will be needed from a development perspective is a revised implementation for your IServiceProvider interface. The rest of your application shouldn’t know the difference!
As a bonus, we can implement the following extension method to help simplify our constructor logic and make our code easier to work with:
<p> CODE: https://gist.github.com/thec2group-blog/752591171262201246485e2db3da3509.js</p>
Figure 6: To help simplify the code required to obtain dependency references, we create a Get<T> extension method for IServiceProvider as a shorthand for IServiceProvider.GetService.
Another option we can take advantage of, depending on the situation (or entirely on personal preference), is to further simplify our constructor logic. By only keeping a reference to the IServiceProvider that was passed into our service constructor, any additional dependency references may be obtained on demand throughout the lifetime of the service, rather than obtaining our dependency references entirely up front:
<p> CODE: https://gist.github.com/thec2group-blog/db9c5cb684f117cb42d56c1f0e080c44.js</p>
Figure 7: UserCalendarService implemented to keep a reference to IServiceProvider so that it may be used as needed throughout the lifetime of the service.
While the Dependency Inversion Principle and Inversion of Control are not new ideas, my hope is that you'll have gleaned something of value for use in your next application development project.