12.1.5 — пятая буква SOLID12.1.5 — the fifth letter of SOLID

Dependency Inversion Principle (DIP)

Модули верхнего уровня не должны зависеть от модулей нижнего уровня — оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей — детали должны зависеть от абстракций. «Инверсия» — в том, что стрелка зависимости в исходном коде направлена противоположно потоку управления.High-level modules should not depend on low-level modules — both should depend on abstractions. Abstractions should not depend on details — details should depend on abstractions. The "inversion" is that the source-code dependency arrow points opposite to the flow of control.

частый вопрос на собеседованииcommon interview question SOLID · 5/5

Что этоWhat it is

Без DIP бизнес-логика верхнего уровня (например, OrderService) напрямую вызывает конкретный класс низкого уровня (MySqlDatabase) — при этом стрелка зависимости в коде совпадает с потоком выполнения: сервис знает имя конкретного класса БД и не может работать ни с чем другим без правки кода сервиса. DIP разрывает эту связь, вводя абстракцию (interface Repository) между ними: OrderService зависит от Repository, и MySqlDatabase тоже зависит от Repository (реализуя его) — обе стороны теперь зависят от абстракции, а не друг от друга.Without DIP, high-level business logic (say, an OrderService) calls a concrete low-level class (MySqlDatabase) directly — here the source-code dependency arrow matches the flow of execution: the service knows the concrete database class's name and can't work with anything else without editing the service's code. DIP breaks this coupling by introducing an abstraction (interface Repository) between them: OrderService depends on Repository, and MySqlDatabase also depends on Repository (by implementing it) — both sides now depend on the abstraction rather than on each other.

До и послеBefore and after

прямая зависимость — сервис знает про MySQLdirect dependency — the service knows about MySQL
class MySqlDatabase {
public:
    void saveOrder(const Order&);
};

class OrderService {
    MySqlDatabase db;  // жёсткая зависимость от конкретного класса// a hard dependency on a concrete class
public:
    void placeOrder(Order o) { db.saveOrder(o); }
};
// переход на PostgreSQL требует правки OrderService// switching to PostgreSQL requires editing OrderService
инверсия — обе стороны зависят от абстракцииinversion — both sides depend on the abstraction
struct OrderRepository {  // абстракция, определена рядом с OrderService// the abstraction, defined alongside OrderService
    virtual void save(const Order&) = 0;
};

class OrderService {
    OrderRepository& repo;  // зависит только от абстракции// depends only on the abstraction
public:
    explicit OrderService(OrderRepository& r) : repo(r) {}
    void placeOrder(Order o) { repo.save(o); }
};

class MySqlRepository : public OrderRepository {  // деталь зависит от абстракции// the detail depends on the abstraction
    void save(const Order& o) override;
};

DIP vs Dependency InjectionDIP vs Dependency Injection

01

DIP — принцип, DI — механизм его реализацииDIP is the principle, DI is the mechanism that implements it

Dependency Inversion — про направление зависимости в исходном коде (что от чего зависит на уровне объявлений типов). Dependency Injection — про способ передачи конкретной реализации абстракции в объект (через конструктор, сеттер или IoC-контейнер). DIP можно соблюсти без единого DI-фреймворка — достаточно передать реализацию через конструктор вручную (constructor injection), как в примере выше.Dependency Inversion is about the direction of the dependency in the source code (what depends on what, at the level of type declarations). Dependency Injection is about the mechanism for handing a concrete implementation of the abstraction to an object (via a constructor, a setter, or an IoC container). DIP can be satisfied with zero DI frameworks — just passing the implementation through the constructor by hand (constructor injection), as in the example above.

02

Rust: trait как граница, dyn/generic на выборRust: a trait as the boundary, dyn/generic by choice

DIP в Rust идиоматично выражается через trait-границу с двумя вариантами использования: fn new(repo: impl OrderRepository) (статическая диспетчеризация, мономорфизация под каждый конкретный тип) или fn new(repo: Box<dyn OrderRepository>) (динамическая диспетчеризация, единый бинарный код) — выбор между ними определяется теми же компромиссами, что и в generic programming.DIP in Rust is idiomatically expressed via a trait boundary with two usage modes: fn new(repo: impl OrderRepository) (static dispatch, monomorphized per concrete type) or fn new(repo: Box<dyn OrderRepository>) (dynamic dispatch, a single shared binary). The choice between them comes down to the same trade-offs as in generic programming.

ГраблиGotchas

Абстракция принадлежит модулю верхнего уровня, а не нижнегоThe abstraction belongs to the high-level module, not the low-level one

Частая путаница: интерфейс OrderRepository объявляется в пакете БД, а не рядом с OrderService. Формально DIP при этом «соблюдён» (обе стороны зависят от интерфейса), но реальный контроль над формой абстракции остаётся у деталей — если БД-пакет решит поменять сигнатуру, бизнес-логика вынуждена подстраиваться. Абстракция должна проектироваться под нужды потребителя (сервиса), а не под удобство поставщика реализации.A common confusion: the OrderRepository interface is declared inside the database package instead of next to OrderService. DIP is formally "satisfied" (both sides depend on the interface), but real control over the abstraction's shape stays with the details — if the DB package decides to change the signature, the business logic is forced to adapt. The abstraction should be designed around the consumer's (the service's) needs, not the implementation provider's convenience.

DI-контейнер — не обязательное условие DIPA DI container is not a prerequisite for DIP

Тяжёлые IoC-фреймворки (Spring, .NET DI) часто ошибочно считают синонимом DIP — на деле они лишь автоматизируют связывание реализаций с абстракциями по конфигурации. Тот же принцип полностью работает и с ручной передачей зависимостей через конструктор без единой сторонней библиотеки.Heavy IoC frameworks (Spring, .NET DI) are often mistaken for synonyms of DIP — in reality they merely automate wiring implementations to abstractions via configuration. The same principle works perfectly well with dependencies passed by hand through a constructor, with zero third-party libraries.

Виртуальный вызов через границу абстракции — цена, а не подарокA virtual call across the abstraction boundary is a cost, not a free lunch

Как и с OCP, инверсия зависимостей через runtime-полиморфизм (интерфейс + vtable) вводит косвенный вызов на каждой границе модулей — в системах с жёсткими требованиями к латентности (торговые движки, разбор рыночных данных) эта косвенность в горячем пути измеряется и часто заменяется на compile-time инверсию через шаблоны/generic.Just as with OCP, inverting dependencies through runtime polymorphism (an interface plus a vtable) introduces an indirect call at every module boundary — in latency-sensitive systems (trading engines, market-data parsing) this indirection on the hot path gets measured, and is often replaced with compile-time inversion via templates/generics.