What is the dependency inversion principle and why is it important?
If we can take it as a given that a "high level" employee at a corporation is paid for the execution of their plans, and that these plans are delivered by the aggregate execution of many "low level" employee's plans, then we could say it is generally a terrible plan if the high level employee's plan description in any way is coupled to the specific plan of any lower level employee.
If a high level executive has a plan to "improve delivery time", and indicates that an employee in the shipping line must have coffee and do stretches each morning, then that plan is highly coupled and has low cohesion. But if the plan makes no mention of any specific employee, and in fact simply requires "an entity that can perform work is prepared to work", then the plan is loosely coupled and more cohesive: the plans do not overlap and can easily be substituted. Contractors, or robots, can easily replace the employees and the high level's plan remains unchanged.
"High level" in the dependency inversion principle means "more important".
A much clearer way to state the Dependency Inversion Principle is:
Your modules which encapsulate complex business logic should not depend directly on other modules which encapsulate business logic. Instead, they should depend only on interfaces to simple data.
I.e., instead of implementing your class Logic
as people usually do:
class Dependency { ... }
class Logic {
private Dependency dep;
int doSomething() {
// Business logic using dep here
}
}
you should do something like:
class Dependency { ... }
interface Data { ... }
class DataFromDependency implements Data {
private Dependency dep;
...
}
class Logic {
int doSomething(Data data) {
// compute something with data
}
}
Data
and DataFromDependency
should live in the same module as Logic
, not with Dependency
.
Why do this?
Dependency
changes, you don't need to change Logic
. Logic
does is a much simpler task: it operates only on what looks like an ADT.Logic
can now be more easily tested. You can now directly instantiate Data
with fake data and pass it in. No need for mocks or complex test scaffolding.Adding to the flurry of generally good answers, I'd like to add a tiny sample of my own to demonstrate good vs. bad practice. And yes, I'm not one to throw stones!
Say, you want a little program to convert a string into base64 format via console I/O. Here's the naive approach:
class Program
{
static void Main(string[] args)
{
/*
* BadEncoder: High-level class *contains* low-level I/O functionality.
* Hence, you'll have to fiddle with BadEncoder whenever you want to change
* the I/O mode or details. Not good. A good encoder should be I/O-agnostic --
* problems with I/O shouldn't break the encoder!
*/
BadEncoder.Run();
}
}
public static class BadEncoder
{
public static void Run()
{
Console.WriteLine(Convert.ToBase64String(Encoding.UTF8.GetBytes(Console.ReadLine())));
}
}
The DIP basically says that high-level components shouldn't be dependent on low-level implementation, where "level" is the distance from I/O according to Robert C. Martin ("Clean Architecture"). But how do you get out of this predicament? Simply by making the central Encoder dependent only on interfaces without bothering how those are implemented:
class Program
{
static void Main(string[] args)
{
/* Demo of the Dependency Inversion Principle (= "High-level functionality
* should not depend upon low-level implementations"):
* You can easily implement new I/O methods like
* ConsoleReader, ConsoleWriter without ever touching the high-level
* Encoder class!!!
*/
GoodEncoder.Run(new ConsoleReader(), new ConsoleWriter()); }
}
public static class GoodEncoder
{
public static void Run(IReadable input, IWriteable output)
{
output.WriteOutput(Convert.ToBase64String(Encoding.ASCII.GetBytes(input.ReadInput())));
}
}
public interface IReadable
{
string ReadInput();
}
public interface IWriteable
{
void WriteOutput(string txt);
}
public class ConsoleReader : IReadable
{
public string ReadInput()
{
return Console.ReadLine();
}
}
public class ConsoleWriter : IWriteable
{
public void WriteOutput(string txt)
{
Console.WriteLine(txt);
}
}
Note that you don't need to touch GoodEncoder
in order to change the I/O mode — that class is happy with the I/O interfaces it knows; any low-level implementation of IReadable
and IWriteable
won't ever bother it.