Card image cap

Understanding SOLID Principles - The Dependency Inversion Principle

The last principle of the SOLID principles is the Dependency Inversion Principle. The principle states that

"High level modules should not depend on low-level modules. Both should communicate by means of abstractions."

It means that we should avoid direct dependencies of components during communication, and rather have their abstractions be used for the same. The principle also talks about high-level and low-level modules, let's understand what the high and low levels of modules mean.

Depdencies : High-Level vs Low-Level:

Its a common sighting in a very large application to have a user interface for interactions, an n-tier levels of business logic and finally a data store for persisting of data. The user interface also consists of various modules like view models, forms and so on. On such a typical application, a query or a command would originate from the user interface and finally ends at the data store. In such an architecture, all the components which are near to the outside world; meaning which are visible to the user are considered as high-level components. Since the commands or the flow of execution originates from these components, they are treated as high-level. And the modules which reside close to the I/O are all considered as low-level modules. Since these modules involve the actual business logic that is executed within a flow of control, these are called low-level. Coming back to the principle, the high-level components such as the User Interface or the View Model components must not have a direct dependency on the low-level components during a flow of execution, but rather should have a communication flow via abstractions of the low-level components.

A typical High-Level module:

  1. Tend to be more abstract
  2. Contain business rules
  3. Process-oriented than detail
  4. located far away from I/O

And a typical Low-level module:

  1. Lies close to I/O
  2. Is connected to the High-level business logic by means of Plumbing code
  3. has interaction with external systems or hardware

Examples of Low-level dependencies include:

  1. Database components
  2. File I/O
  3. Email sending (SMTP) clients
  4. Web API calls
  5. Configuration Managers
  6. System DateTime Maintainance

Take an example of a typical area calculator client, which internally makes use of a library for calculating the area of a given shape. A typical program can look like this:


public class Client
{
    SquareLogic square;
    
    public Client()
    {
        // SquareLogic is fully substitutable for ISquare
        this.square = new SqaureLogic();
    }

    public void Invoke() {
        Console.WriteLine(square.calculateArea(2));
    }
}

public class SqaureLogic
{
    public int calculateArea(int side)
    {
        return side * side;
    }
}

Here the Client class is directly dependent on the internal logic class SquareLogic for the calculation functionality. We can consider the Client class to be at higher-level in the flow of control (since the execution probably starts from here) and the SquareLogic class is at the lower-level. Let's consider the scenario of having a requirement wherein the SquareLogic class needed to be replaced by another logic class for say, efficiency sake. But the above situation doesn't allow for any change, since we have a high-level class directly calling a low-level class for functionality, which the principle advocates should be avoided.

Points to be noted here:

  1. Using new to create objects is not always bad, but when we are creating an object by using a new keyword, we are subconsciously creating a bond between the caller class and the dependency class together. This needs to be avoided if possible.
  2. Another example of a dependency is the use of Static methods within a class for any sake. These are dangerous because we create a link between the caller class and the class which holds the static method. This can create problems if the caller class is exported into another component for any future expansion.
  3. Using new keyword also makes the class harder to test, because we end up adding more setup logic to our test methods, which make our tests brittle.
  4. Instead of creating new objects at the point of necessity, prefer having the instantiation section moved to the class constructor, since it is the first point where the execution within the class begins and also helps maintaining the classes better.

The principle of dependency inversion gives rise to dependency injection, which is a phenomenon where the lower-level dependencies of a high-level class are injected when required via a parameterized constructor, instead of using the new keyword explicitly. The dependency injection container or the DI container takes care of the object instantiation and maintenance.

Coming back to the above example, we can restructure the above classes to have an abstract way of having dependencies, as shown below:


public class Client
{
    ISquare square;
    public Client()
    {
        // SquareLogic is fully substitutable for ISquare
        this.square = new SqaureLogic();
    }

    public void Invoke() {
        Console.WriteLine(square.calculateArea(2));
    }
}

public class SqaureLogic : ISquare
{
    public int calculateArea(int side)
    {
        return side * side;
    }
}

In the above case, the SquareLogic class gets an interface which abstracts the actual detail for external purposes. The Client class then uses the interface type instead of the implementation type and invokes the functionality within. We still are using the new keyword here which isn't desirable. Further restructuring the code can lead to the below result:


public class Client
{
    ISquare square;
    public Client(ISquare square)
    {
        this.square = square;
    }

    public void Invoke() {
        Console.WriteLine(square.calculateArea(2));
    }
}

Now we have a parameterized constructor which has the ISquare interface passed onto it, assigned to the local variable square. With this, we completely remove the dependency of lower-level SquareLogic class and instead have communication via its interface ISquare for functionality. The required dependency for this class can be injected at the first point of execution (such as a Main method) or can be tagged to a DI container for object management. In either way, we have a configurable dependency management and the classes comply to dependency inversion.

Published 2 months ago

Sponsored Links
We use cookies to improve user experience. Learn More