Программа курса← Объектно ориентированное программирование
1История ООП
2Объект и Класс
3Базовые концепции ООП
4Принцип проектирования GRASP
5Принцип проектирования SOLID
6Паттерны GOF
7Методические материалы

DIP – принцип инверсии зависимостей

Высокоуровневые модули не должны зависеть от низкоуровневых. Оба типа должны зависеть от абстракций.

Абстракции не должны зависеть от деталей – детали должны зависеть от абстракций.

Смысл: вместо зависимости от конкретных реализаций нужно ориентироваться на интерфейсы.

Как выглядит нарушение

Сервис заказов сам создаёт себе хранилище – и вместе с ним получает конкретную СУБД:

// Логика заказа знает про 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 – решение о том, на что вообще смотрит зависимость.