Dependency Inversion Principle
One of the most common interview questions I have been asked is some version of “What does SOLID stand for and what does each principle mean?” I know there are thousands of articles already covering this subject with dozens more probably being created each week, but I’m going to add to the pile anyway! We’re going to take a deeper look at what each of these principles mean, how they should be used, and why they are important.
What is Solid
- S stands for Single Responsibility Principle (SRP)
- O stands for Open Closed Principle (OCP)
- L stands for Liskov Substitution Principle (LSP)
- I stands for Interface Segregation Principle (ISP)
- D stands for Dependency Inversion Principle (DIP)
Background
You should read my posts on the other SOLID principles for a good understanding
- Single Responsibility Principle
- Open/Closed Principle
- Liskov Substitution Principle
- Dependency Inversion Principle
Understanding the Dependency Inversion Principle
High level modules shouldn’t depend on low-level modules. They should depend on abstractions
The primary goal of this principle is to reduce dependencies in the code. When a module has a direct dependency on the implementation of the calls being made it’s kind of like me needing to know exactly how my Dr. Pepper was made in order to drink it. I don’t need or even want to know how to make Dr. Pepper myself, or what distributor to call to order one. I just want to be able to walk up to a vending machine, put my money in, and enjoy my refreshing beverage. Just as the vending machine is a common interface for me to obtain my favorite drink, your code should have some abstraction which it can rely on in order to separate it from the irrelevant details of the call it needs to make.
A Simple Example
In the following code I have provided an example of a pattern I have seen in several projects. In this section we will talk about why this solution isn’t ideal and how we can improve it.
public enum LogType {
Console,
Window
}
public class Logger {
private LogType type;
public Logger(LotType logType) =>
type = logType;
public void Write(string msg) =>
switch(type) {
case LogType.Console:
Console.WriteLine($"{DateTime.Now.ToString("MMddyyyyhhmmss")}: {msg}"));
break;
case LogType.Window:
MessageBox.Show($"{DateTime.Now.ToString("MMddyyyyhhmmss")}: {msg}");
break;
default:
throw new Exception("Log type not found.");
}
}
public class Looper {
private readonly Logger logger;
public Looper() =>
logger = new Logger(LogType.Console);
public void Loop() =>
for (var i = 0; i < 10; i++)
logger.Write(i.ToString());
}
static void Main(string[] args) {
var looper = new Looper();
looper.Loop();
}
The Issues
Keeping all the SOLID principles in mind, there are a few issues that we can spot with this code.
Firstly, the Logger class has 2 responsibilities which violates the Single Responsibility Principle. It logs a message AND decides how to log it. This will need to be addressed.
Secondly, when the need arises to add a logging type such as writing to a file then we will have to modify both the Logger class and the LogType enum. this violates the Open/Closed Principle*.
Thirdly, the Looper class has intimate knowledge of how the Logger class is performing its operation and has a hard coded dependency on the Logger class. This means that the high level module (Looper) has a direct dependency on a lower level module (Logger).
The Solutions
How do we solve these problems? Well, aligning with SRP is as simple as implementing the strategy pattern. The strategy pattern is a conversion from an iterative substitution to a polymorphic substitution. We can do this by breaking the different logging types into their own classes. Then, in order to resolve our violation of OCP we can have each of these logger classes implement a common interface: ILogger. Now when we need to add a new logging method we can just create a new class which implements ILogger.
public interface ILogger {
void Write(string message);
}
public class ConsoleLogger : ILogger {
public void Write(string message) =>
Console.WriteLine($"{DateTime.Now.ToString("MMddyyyyhhmmss")}: {message}");
}
public class FormLogger : ILogger {
public void Write(string message) =>
MessageBox.Show($"{DateTime.Now.ToString("MMddyyyyhhmmss")}: {message}");
}
So now our code follows SRP and OCP, but how does this fix our issue with DIP? Since we’ve broken the logging logic into multiple classes which implement a common interface we can leverage Polymorphism and the Liskov Substitution Principle to code the Looper class to the ILogger interface. Now we can substitute the implementation for the interface at runtime. What I’m talking about here is known as Dependency Injection; This is where the dependency is injected directly into a class via its public members. In this example I will be injecting the dependency through the constructor.
public class Looper {
private readonly ILogger logger;
public Looper(ILogger logger) =>
this.logger = logger;
public void Loop(int limit) =>
for (var i = 0; i < limit; i++)
logger.Write(i.ToString());
}
Now our Looper class has no knowledge of which class is being used or how the operation is performed. All it knows is that it requires something which understands how to log. It is now able to hand the job off to whatever implementation is provided; this makes our Looper class cleaner by reducing dependencies and responsibilities.
This can also be described as “pushing fragility to the boundaries” of our application. The boundary in our example is where a Looper instance is being initialized and the dependencies are gathered, which happens to be the entry point of our console application.
static void Main(string[] args) {
var consoleLogger = new ConsoleLogger();
var looper = new Looper(consoleLogger);
looper.Loop(10);
Console.WriteLine("...................................\n");
var formLogger = new FormLogger();
looper = new Looper(formLogger);
looper.Loop(5);
}
By making these changes our code now follows SRP, OCP, LSP, and DIP. That’s 4 of the 5 SOLID principles in a single example! Pretty productive refactoring session!