Dependency Inversion
Dependency Inversion is a no-brainer for fixing tight coupling issues, but why is it named "Dependency Inversion".
Instead of high-level classes depending directly on low-level ones, they depend on interfaces which the low-level classes then implement and thus the “inversion” isn’t about how you pass around the interface - it’s about flipping the usual dependency flow, making everything more flexible and easier to manage.
The key here is the dependency is being flipped from the injected interface/implementer, not the class that performing the composition.
What is Dependency Inversion?
Dependency Inversion is a principle in software design that addresses the direction of dependency within an application. It is part of the SOLID principles and aims to reduce coupling between high-level modules and low-level modules.
Key Concept:
-
Typical Dependency Flow: In a traditional design, if
Class A
usesClass B
, andClass B
usesClass C
, thenClass A
has a direct dependency onClass B
, andClass B
has a direct dependency onClass C
. This creates a direct dependency graph fromClass A
toClass C
. -
Inverted Dependency Flow: By applying the Dependency Inversion Principle,
Class A
interacts withClass B
through an abstraction (interface or abstract class) thatClass B
implements.Class B
will depend on an interface controlled byClass A
, inverting the traditional compile-time dependency. At runtime,Class A
can still call methods onClass B
through the abstraction, but the compile-time dependencies are reversed.
Why is it Called Dependency Inversion?
The term "Dependency Inversion" reflects the change in dependency direction:
- Before Inversion: Dependencies flow from high-level modules to low-level modules directly.
- After Inversion: Dependencies flow from high-level modules to abstractions, and low-level modules implement these abstractions.
This inversion means that instead of high-level modules depending on low-level modules, both depend on abstractions. This inversion enhances modularity, flexibility, and testability.
Example of Dependency Inversion
Without Dependency Inversion:
public class LowLevelService
{
public void DoWork() { /* Implementation */ }
}
public class HighLevelController
{
private LowLevelService _service;
public HighLevelController()
{
_service = new LowLevelService();
}
public void PerformAction()
{
_service.DoWork();
}
}
In this example, HighLevelController directly depends on LowLevelService. This direct dependency makes HighLevelController tightly coupled to LowLevelService.
With Dependency Inversion:
public interface IService
{
void DoWork();
}
public class LowLevelService : IService
{
public void DoWork() { /* Implementation */ }
}
public class HighLevelController
{
private readonly IService _service;
public HighLevelController(IService service)
{
_service = service;
}
public void PerformAction()
{
_service.DoWork();
}
}
In this refactored example:
- IService is an abstraction that LowLevelService implements.
- HighLevelController depends on the IService interface rather than a specific implementation.
- This allows for greater flexibility and easier testing. For instance, you can easily substitute LowLevelService with a mock or another implementation of IService.
Just don't be that person who generates interfaces anytime, everywhere and then every class has another with eyes.
© Jason Song.