Высокоуровневые модули не должны зависеть от низкоуровневых. Оба типа должны зависеть от абстракций.
Абстракции не должны зависеть от деталей – детали должны зависеть от абстракций.
Смысл: вместо зависимости от конкретных реализаций нужно ориентироваться на интерфейсы.
Как выглядит нарушение
Сервис заказов сам создаёт себе хранилище – и вместе с ним получает конкретную СУБД:
// Логика заказа знает про Postgres: класс, драйвер, строку подключения
public class OrderService
{
private readonly SqlOrderRepository _repository = new(Config.ConnectionString);
public void Place(Order order)
{
order.MarkPlaced();
_repository.Save(order);
}
}
// Логика заказа знает про Postgres: класс, драйвер, строку подключения
class OrderService {
public:
void Place(Order& order) {
order.MarkPlaced();
repository_.Save(order);
}
private:
SqlOrderRepository repository_{Config::ConnectionString()};
};
// Логика заказа знает про Postgres: тип, драйвер, строку подключения
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)
}
# Логика заказа знает про Postgres: класс, драйвер, строку подключения
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)
// Логика заказа знает про Postgres: класс, драйвер, строку подключения
class OrderService {
private readonly repository = new SqlOrderRepository(config.connectionString);
place(order: Order): void {
order.markPlaced();
this.repository.save(order);
}
}
// Логика заказа знает про Postgres: класс, драйвер, строку подключения
public class OrderService {
private final SqlOrderRepository repository = new SqlOrderRepository(Config.CONNECTION_STRING);
public void place(Order order) {
order.markPlaced();
repository.save(order);
}
}
// Логика заказа знает про Postgres: класс, драйвер, строку подключения
class OrderService {
private val repository = SqlOrderRepository(Config.connectionString)
fun place(order: Order) {
order.markPlaced()
repository.save(order)
}
}
Стрелка зависимости смотрит не туда: главное в системе – правила оформления заказа – зависит от второстепенного, от способа хранения. Чем это оборачивается:
- тест требует базу – подменить хранилище нечем, оно создаётся внутри;
- сменить хранилище значит править логику – переезд на другую СУБД или добавление кеша
идёт правкой
OrderService, хотя правила оформления заказа не изменились; - логику не собрать отдельно – модуль с бизнес-правилами тянет за собой драйвер БД;
- писать приходится снизу вверх – пока нет хранилища, сервис не скомпилируется.
Как перевернуть зависимость
Интерфейс объявляет старший модуль – под свои нужды, своими словами. Хранилище его реализует:
// Контракт принадлежит логике заказа, а не базе
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);
}
}
// Деталь реализует контракт, а не диктует его
public class SqlOrderRepository : IOrderRepository
{
public void Save(Order order) { }
}
// Контракт принадлежит логике заказа, а не базе
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_;
};
// Деталь реализует контракт, а не диктует его
class SqlOrderRepository : public IOrderRepository {
public:
void Save(const Order& order) override {}
};
// Контракт принадлежит логике заказа, а не базе
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)
}
// Деталь реализует контракт, а не диктует его
type SQLOrderRepository struct{}
func (SQLOrderRepository) Save(order *Order) error { return nil }
# Контракт принадлежит логике заказа, а не базе
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)
# Деталь реализует контракт, а не диктует его
class SqlOrderRepository:
def save(self, order: Order) -> None:
pass
// Контракт принадлежит логике заказа, а не базе
interface OrderRepository {
save(order: Order): void;
}
class OrderService {
constructor(private readonly repository: OrderRepository) {}
place(order: Order): void {
order.markPlaced();
this.repository.save(order);
}
}
// Деталь реализует контракт, а не диктует его
class SqlOrderRepository implements OrderRepository {
save(order: Order): void {}
}
// Контракт принадлежит логике заказа, а не базе
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);
}
}
// Деталь реализует контракт, а не диктует его
public class SqlOrderRepository implements OrderRepository {
public void save(Order order) { }
}
// Контракт принадлежит логике заказа, а не базе
interface OrderRepository {
fun save(order: Order)
}
class OrderService(private val repository: OrderRepository) {
fun place(order: Order) {
order.markPlaced()
repository.save(order)
}
}
// Деталь реализует контракт, а не диктует его
class SqlOrderRepository : OrderRepository {
override fun save(order: Order) = Unit
}
Ключевое здесь – кому принадлежит интерфейс. Если IOrderRepository объявлен рядом с
логикой заказа и описан её словами, зависимость действительно перевёрнута. Если он лежит в
пакете с базой и повторяет её API – это просто ещё один файл, а стрелка смотрит туда же,
куда и смотрела.
Что даёт соблюдение
- Модуль тестируется без базы, сети и очередей. Подставили реализацию в памяти – и тест идёт за миллисекунды, а не за секунды.
- Реализацию можно заменить, не трогая логику. Переезд с одной СУБД на другую – это новый класс рядом, а не правки в бизнес-коде.
- Границы модулей становятся явными. Интерфейс – это записанный контракт: видно, что именно старший модуль требует от младшего.
- Порядок разработки перестаёт быть жёстким. Логику заказа можно писать до того, как готово хранилище: контракт уже есть.
Где здесь «инверсия»
Обычно старший модуль зависит от младшего: сервис знает про базу. После применения принципа интерфейс принадлежит старшему модулю, а младший его реализует – стрелка зависимости разворачивается в обратную сторону. Отсюда и название.
Не путайте принцип с Dependency Injection: DI – это способ передать реализацию внутрь, а DIP – решение о том, на что вообще смотрит зависимость.