DEV Community

sashamathews
sashamathews

Posted on • Originally published at programmingstuff.Medium

How Dependency Inversion and Inversion of Control principles help to build maintainable software systems

An object is one of the fundamental concepts in object-oriented programming. Object encapsulates a set of some data (properties), as well as the behavior (methods) of the application code. Theoretically, any application can be implemented within the single method of single object of single class. In terms of a console application, all logic can be placed in the Main method. In terms of ASP.NET Core web app, all incoming requests can be processed by Index action of HomeController class. Therefore, if you decided to use Single/God Object Development “principle”, you do not need to worry about dependency injection.

However, putting all application logic in a single object can work well only for very simple cases. This is a well-known anti-pattern called God object. As the number of features growths, the maintainability of your application will tend to zero.

0_zb70q4Oyc8mlxQN1

This graph represents the following idea: when your application has 10 lines of code, it takes almost no time to find some bug or to integrate new feature into existing code. However, when application size is 5000 lines of code, coding new feature will take 5% of your time. The remaining 95% of the time will be spent on integrating new feature into existing mess.

You can contradict me that no one who understands object-oriented programming just a bit develops a complex software in such an ugly way. You absolutely right.

Start using many classes

We definitely need much more than one class to develop a more complex application than HelloWorld. Every single class will encapsulate data and behavior needed to perform one particular task (Single Responsibility Principle). Objects of such "narrow" classes will use each other, work together, making our application work. How exactly some object can use another one? Easy: one object has to instantiate another object and, for example, call it's method. Let's take a quick look at simple code example:

public class UserReportGenerator
{
    private readonly Logger _logger = new Logger();
    public Report Generate()
    {
        _logger.Log("Started");
        //Repor generation logic
        _logger.Log("Completed");
    }
}
public class Logger
{
    public void Log(string message)
    {
        string logMessage = $"Message: { message }, Time: { DateTime.Now }";
        File.WriteAllText("app_log.txt", logMessage);
    }
}
Enter fullscreen mode Exit fullscreen mode

This is not a God object. We have 2 separate classes. Each class is responsible for the only (almost) task.

UserReportGenerator and Logger classes do not follow Single Responsibility Principle 100% percent. Besides their direct responsibilities they both also responsible for instantiating their dependencies. These "instantiating" responsibilities also have to be moved out of UserReportGenerator and Logger classes. We will see how to deal with it later and why it's super important.

Class Logger is a low-level component in this small chain. It knows how to create log messages in appropriate format before saving them to the text file. The high-level component is UserReportGenerator class which knows how to generate user reports according to business rules.

As you can see, the logic of our application is split into separate classes. This separation simplifies code-readability and allows us to reuse classes again and again for the new features in different parts of the system (now we can inject Logger to many different classes which require to log something).

Our new approach definitely will allow us to develop complex application that even can be sold to the end users. However, I am afraid that after a while we may experience serious maintainability problem again. Do you remember the graph Maintainability & Lines of Code? Here is how it looks now:

222

This looks much better, but for some reason the line still goes down despite the better code design. Why we got stuck again? The reason of that is …

Tight Coupling

As you may already noticed UserReportGenerator class is tightly coupled to Logger. We use "new" keyword followed by class name to create the instance of Logger. This is a glue. The relationship between UserReportGenerator and Logger is defined at compile time. UserReportGenerator always has to work only with one concrete Logger implementation. You may be wondered why do we might need to break this relationship? Why this is a problem? There are few reasons to do it:

  • Code reusability. It may not be possible to reuse some class in a new context if it is tightly coupled to another class. Look at Logger class first. Any other classes in our system can quickly reuse Logger as many times as needed. They have just instance of Logger and call it's method. But now imagine that someone else wants to reuse UserRerportGenerator. He will also reuse Logger functionality however logging feature may not be needed at all!

  • Code flexibility. Code cannot be quickly adapted to new requirements which are regularly changing. Today it's enough to write logs in simple file. Tomorrow we will have to write logs in database. The day after tomorrow? Message Broker? Yes, you can modify Logger class and redeploy your application. But what if there are thousands of classes already use your Logger. You may spend quite long time just to compile your solution after making changes. And don't forget about the risk of regression defects.

  • Unit testing. Tightly coupled classes cannot be unit testable. Logic of Generate method in UserReportGenerator class cannot be tested in isolation from the external environment, because each run of such test will touch the file system. "Unit" test will fail when file system errors occur even if report generation logic is perfect.

Now we can say with confidence that UserReportGenerator does not have to instantiate its dependencies. But even when UserReportGenerator will receive dependency via constructors like that:

public class UserReportGenerator
{
    private Logger _logger;
    public UserReportGenerator(Logger logger) => logger = _logger;
}
Enter fullscreen mode Exit fullscreen mode

But still none of 3 drawbacks listed above are eliminated. Tight coupling is still in place despite the fact that UserReportGenerator is not instantiating its dependency anymore. UserReportGenerator is still dependent on concrete implementation.

There are two key principles in Object-Oriented Design which can really help us. I am talking about Dependency Inversion principle:

“High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces). Abstractions should not depend on details (concrete implementations). Details should depend on abstractions.”

and Inversion of Control principle:

“Inversion of Control is a principle in software engineering by which the control of objects or portions of a program is transferred to a container or framework.”

Let’s apply Dependency Inversion Principle to our classes:

public class UserReportGenerator
{
    private readonly ILogger _logger;
    public UserReportGenerator(ILogger logger) => _logger = logger;

    ...
}
public interface ILogger
{
    void Log(string message);
}
public class Logger : ILogger
{
    ...
}
Enter fullscreen mode Exit fullscreen mode

We introduced abstraction ILogger. Now UserReportGenerator class knows only about ILogger abstraction. No dependency on a concrete implementation anymore. But please note that DI Principle only suggests a solution to the problem but does not suggest the technique to implement it. Who and how should instantiate Logger instance and inject it into constructor of UserReportGenerator class? Dependency Inversion Principle does not answer that questions but Inversion of Control Principle does.

Typically Inversion of Control principle is achieved by using special frameworks called IoC or DI containers (Autofac, SimpleInjector, Light Inject…). They are responsible for instantiating dependencies, injecting them into appropriate objects and controlling their lifetime.

var builder = new ContainerBuilder();
builder.RegisterType<Logger>().As<ILogger>();
Enter fullscreen mode Exit fullscreen mode

So now everything is in right place. Our classes are responsible only for doing their job. They do not create dependencies they needed anymore. This separate responsibility is moved to IoC container.

Using Dependency Inversion Principle in conjunction with Inversion of Control makes your system flexible. Our classes can be reused any number of times and there is no need to reuse their hardcoded dependencies anymore. When new requirement comes to write logs to the database, we simply need to provide the UserReportGenerator class with the new implementation of ILogger interface and make a simple change in Composition Root of our application:

public class DatabaseLogger : ILogger
{
    public void Log(string message)
    {
        //Write log into database
    }
}

//builder.RegisterType<Logger>().As<ILogger>();
builder.RegisterType<DatabaseLogger>().As<ILogger>();
Enter fullscreen mode Exit fullscreen mode

When we need to unit test UserReportGenerator in isolation from external environment it will be enough to use Null Object pattern or to generate a stub by using unit testing frameworks.

Null object Pattern:

[Fact]
public void Report_Generated_Successfully()
{
    var urg = new UserReportGenerator(new NullLogger());
    Report report = urg.Generate();
    Assert.True(report.Success);
}
Enter fullscreen mode Exit fullscreen mode

Stub:

[Fact]
public void Report_Generated_Successfully()
{
    var loggerStub = new Mock<ILogger>();
    var urg = new UserReportGenerator(loggerStub.Object);
    Report report = urg.Generate();
    Assert.True(report.Success);
}
Enter fullscreen mode Exit fullscreen mode

Dependency Inversion and Inversion of Control principles can ultimately help us to keep the maintainability of our system in a good shape quite long time, potentially forever. Of course there are many more other principles, pattern, techniques to learn and practice in order to build complex maintainable software systems. However, DI and IoC are fundamental ones.
333p_

Thanks for reading!

Top comments (0)