In short, the principle can be summarized in – a loosely coupled system built upon components that are not responsible for creating their own dependencies. Instead the dependencies are provided from outside the component itself. Components in a loosely coupled system can be replaced with alternative implementations that provide the same services.
The opposite to loosely coupled system is a tight coupled system. Here is an example of a tight coupled dependency. The component VideoService is responsible for creating its own dependency to an implementation of type VideoRepository.
Here is the same example but implemented in a loosely coupled system. The component VideoService is no longer responsible itself to create dependencies to other components in the system. The dependency to an implementation of type VideoRepository has been removed by introducing an interface IVideoRepository. And the responsibility to provide a concrete implementation of that interface has been moved to outside the component itself, and is now injected in the constructor.
So, what are the benefits by implementing the later alternative, the loosely coupled system? Here are a few advantages that I think is the most important to remember.
An obvious drawback with a loosely coupled system is that when your application is getting bigger and more complex, so does the list of dependencies. This could easily end up with a complex and deep dependency graphs. This example is not too complex but might illustrate the problem if the graph is getting longer and deeper in your application.
But if there are side effects, there are also solutions - so it's time to introduce the concept of Inversion of Control (IoC).
To solve the problem with complex and deep dependency chains passed to the constructors in a loosely coupled system we introduce a mechanism called Inversion of Control (IoC). In short, the mechanism can be summarized in – the responsibility to create instances of classes in the system is moved to a so called container and you no longer need to create your instances yourself, you simply ask the container to create the instance for you. The container needs to be configured in the system to work and we can then resolve the instances of the implementations in the system by resolving them from the container, like this:
In the best of worlds, you will not need to resolve instances in this way by allowing the system to handle this in the end-points that executes your code. For example, you can configure a dependency resolver for the controller factory in asp.net that is responsible for creating instances of your controllers and all its dependencies to other components.
Examples of popular IoC frameworks are:
Here are some examples of advantages you will achieve by implementing both DI and IoC in your systems:
Although DI and IoC may seem complex and difficult to understand, do not give up! It is quite easy to implement and understand conceptually when you have tried it out and worked with the mechanisms for a while. This is also often referred to as one of the disadvantages of implementing DI and IoC, but I’m not sure this is actually being the case if you just give it a GO!