This lesson was translated automatically – it may contain errors.
High-level modules should not depend on low-level ones. Both kinds should depend on abstractions.
Abstractions should not depend on details – details should depend on abstractions.
The point: instead of depending on concrete implementations, aim at interfaces.
What a violation looks like
The order service creates its own storage – and gets a particular database along with it:
// The order logic knows about Postgres: the class, the driver, the connection string
public class OrderService
{
private readonly SqlOrderRepository _repository = new(Config.ConnectionString);
public void Place(Order order)
{
order.MarkPlaced();
_repository.Save(order);
}
}
// The order logic knows about Postgres: the class, the driver, the connection string
class OrderService {
public:
void Place(Order& order) {
order.MarkPlaced();
repository_.Save(order);
}
private:
SqlOrderRepository repository_{Config::ConnectionString()};
};
// The order logic knows about Postgres: the type, the driver, the connection string
type OrderService struct {
repository *SQLOrderRepository
}
func NewOrderService() *OrderService {
return &OrderService{repository: NewSQLOrderRepository(config.ConnectionString)}
}
func (s *OrderService) Place(order *Order) error {
order.MarkPlaced()
return s.repository.Save(order)
}
# The order logic knows about Postgres: the class, the driver, the connection string
class OrderService:
def __init__(self) -> None:
self._repository = SqlOrderRepository(config.connection_string)
def place(self, order: Order) -> None:
order.mark_placed()
self._repository.save(order)
// The order logic knows about Postgres: the class, the driver, the connection string
class OrderService {
private readonly repository = new SqlOrderRepository(config.connectionString);
place(order: Order): void {
order.markPlaced();
this.repository.save(order);
}
}
// The order logic knows about Postgres: the class, the driver, the connection string
public class OrderService {
private final SqlOrderRepository repository = new SqlOrderRepository(Config.CONNECTION_STRING);
public void place(Order order) {
order.markPlaced();
repository.save(order);
}
}
// The order logic knows about Postgres: the class, the driver, the connection string
class OrderService {
private val repository = SqlOrderRepository(Config.connectionString)
fun place(order: Order) {
order.markPlaced()
repository.save(order)
}
}
The arrow of dependency points the wrong way: the main thing in the system – the rules of placing an order – depends on the secondary one, on the way things are stored. Here is what that turns into:
- the test needs a database – there is nothing to replace the storage with, it is created inside;
- changing the storage means editing the logic – moving to another database or adding a
cache goes through
OrderService, even though the rules of placing an order have not changed; - the logic cannot be built on its own – the module with the business rules drags the database driver along;
- you have to write bottom-up – until the storage exists, the service will not compile.
How to invert the dependency
The interface is declared by the higher-level module – for its own needs and in its own words. The storage implements it:
// The contract belongs to the order logic, not to the database
public interface IOrderRepository
{
void Save(Order order);
}
public class OrderService
{
private readonly IOrderRepository _repository;
public OrderService(IOrderRepository repository) => _repository = repository;
public void Place(Order order)
{
order.MarkPlaced();
_repository.Save(order);
}
}
// The detail implements the contract instead of dictating it
public class SqlOrderRepository : IOrderRepository
{
public void Save(Order order) { }
}
// The contract belongs to the order logic, not to the database
class IOrderRepository {
public:
virtual void Save(const Order& order) = 0;
virtual ~IOrderRepository() = default;
};
class OrderService {
public:
explicit OrderService(IOrderRepository& repository) : repository_(repository) {}
void Place(Order& order) {
order.MarkPlaced();
repository_.Save(order);
}
private:
IOrderRepository& repository_;
};
// The detail implements the contract instead of dictating it
class SqlOrderRepository : public IOrderRepository {
public:
void Save(const Order& order) override {}
};
// The contract belongs to the order logic, not to the database
type OrderRepository interface {
Save(order *Order) error
}
type OrderService struct {
repository OrderRepository
}
func NewOrderService(repository OrderRepository) *OrderService {
return &OrderService{repository: repository}
}
func (s *OrderService) Place(order *Order) error {
order.MarkPlaced()
return s.repository.Save(order)
}
// The detail implements the contract instead of dictating it
type SQLOrderRepository struct{}
func (SQLOrderRepository) Save(order *Order) error { return nil }
# The contract belongs to the order logic, not to the database
class OrderRepository(Protocol):
def save(self, order: Order) -> None: ...
class OrderService:
def __init__(self, repository: OrderRepository) -> None:
self._repository = repository
def place(self, order: Order) -> None:
order.mark_placed()
self._repository.save(order)
# The detail implements the contract instead of dictating it
class SqlOrderRepository:
def save(self, order: Order) -> None:
pass
// The contract belongs to the order logic, not to the database
interface OrderRepository {
save(order: Order): void;
}
class OrderService {
constructor(private readonly repository: OrderRepository) {}
place(order: Order): void {
order.markPlaced();
this.repository.save(order);
}
}
// The detail implements the contract instead of dictating it
class SqlOrderRepository implements OrderRepository {
save(order: Order): void {}
}
// The contract belongs to the order logic, not to the database
public interface OrderRepository {
void save(Order order);
}
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) {
this.repository = repository;
}
public void place(Order order) {
order.markPlaced();
repository.save(order);
}
}
// The detail implements the contract instead of dictating it
public class SqlOrderRepository implements OrderRepository {
public void save(Order order) { }
}
// The contract belongs to the order logic, not to the database
interface OrderRepository {
fun save(order: Order)
}
class OrderService(private val repository: OrderRepository) {
fun place(order: Order) {
order.markPlaced()
repository.save(order)
}
}
// The detail implements the contract instead of dictating it
class SqlOrderRepository : OrderRepository {
override fun save(order: Order) = Unit
}
The key here is who owns the interface. If IOrderRepository is declared next to the order
logic and written in its words, the dependency really is inverted. If it lives in the
package with the database and repeats its API – it is just one more file, and the arrow
points exactly where it pointed before.
What following it gives
- The module is tested without a database, network or queues. Put an in-memory implementation in and the test runs in milliseconds rather than seconds.
- The implementation can be replaced without touching the logic. Moving from one database to another is a new class next to the old one, not edits in the business code.
- Module boundaries become explicit. An interface is a written contract: it shows what exactly the higher-level module requires of the lower-level one.
- The order of development stops being rigid. The order logic can be written before the storage is ready: the contract is already there.
Where the «inversion» is
Normally the higher-level module depends on the lower-level one: the service knows about the database. After the principle is applied, the interface belongs to the higher-level module and the lower-level one implements it – the arrow of dependency turns around. Hence the name.
Do not confuse the principle with dependency injection: DI is a way to pass an implementation inside, while DIP is the decision about what the dependency points at.