The Dependency Inversion Principle
Last time we discovered that
- Stable code should not depend on volatile, frequently changing code. If it does, then it will no longer be stable, and
- Abstractions are more stable than concretions/details.
Putting these ideas together, we get:
- Abstractions should not depend on concretions, and
- Concretions should depend on abstractions
These two rules are the basis of the Dependency Inversion Principle (DIP). Formally, DIP states:
- High-level modules should not depend on low-level modules. Both should depend on abstractions (e.g. interfaces).
- Abstractions should not depend on details. Details (concrete implementations) should depend on abstractions.
The second part of DIP we have already derived from first principles.
The first part is worth exploring more.
High-level modules are oriented towards being about policy.
They concern themselves with what happens. These are stable modules. On the other hand, lower-level modules involve themselves with the detail; i.e. how it works. These are subject to more frequent changes – they are more volatile.
Since stable things should not depend on unstable things, it makes sense for high-level modules to remain ignorant or independent of detail.
OK, time for an example:
Assume a high-level workflow module method, GetCustomerOrdersUseCase.GetActiveOrders() needs access to the system’s SQL Server database. Currently, database access is achieved by directly instantiating the data access class:
public IEnumerable<Order> GetActiveOrders(Customer customer) { // ... var dataAccess = new SqlAdoDataAccess(ConnectionString); var orders = dataAccess.GetActiveOrders(customer.Id); // ... }
Why is this problematic?
Well, we have coupled ourselves to an instance of the SqlAdoDataAccess class. If we wanted to change how we access data, say, switch from SqlAdoDataAccess to SqlEfDataAccess, then we will be forced to make the change inline. Unit testing will also be difficult since SqlAdoDataAccess will try to establish a connection to a database. It’s not a unit test when input/output (IO) is involved.
How do we enable easy changing of how data is accessed?
What if we break the dependency? Instead of our high-level class GetCustomerOrdersUseCase depending on a volatile concrete data access class (SqlAdoDataAccess) it could rely on a stable data access abstraction (IDataAccess) instead?
In this way, we have inverted the direction of dependency: SqlAdoDataAccess will implement IDataAccess, a dependency that did not exist before. GetCustomerOrdersUseCase will no longer directly instantiate SqlAdoDataAccess but will instead depend on an interface, IDataAccess. GetCustomerOrdersUseCase can then receive an instance of SqlAdoDataAccess, or any IDataAccess implementer, via its constructor:
public GetCustomerOrdersUseCase(IDataAccess dataAccess) { DataAccess = dataAccess; } public IEnumerable<Order> GetActiveOrders(Customer customer) { // ... var orders = DataAccess.GetActiveOrders(customer.Id); // ... }
Somewhere in our program, we still need to connect our abstractions and concretions – we will always need that. But at least we can reduce the dependency to a single line binding the interface to the concrete class.
This connection is usually configured via an Inversion of Control (IoC) container and may look like this:
container.For<IDataAccess>.Use<SqlAdoDataAccess>();
To recap, the Dependency Inversion Principle helps us decouple our software modules (i.e. classes). It states that high-level, policy-oriented modules ought to depend on abstractions rather than concretions.
In turn, abstractions should not depend on concretions or detail.
Leave a Reply
Want to join the discussion?Feel free to contribute!