Почему в программировании родители должны зависеть от абстракций, а не от детей
Что проверяют
Знаете ли вы принцип инверсии зависимостей и умеете ли объяснить его на своём стеке, а не процитировать формулировку. На фронтенде за вопросом обычно стоит очень конкретная вещь: родительский компонент, который перебирает всех своих детей через switch по типу, зависит от деталей и переписывается при появлении каждого нового ребёнка.
Как отвечать
Формулировка простая: модуль верхнего уровня не должен зависеть от модуля нижнего уровня, оба зависят от абстракции. Смысл в направлении, по которому расходятся изменения. Зависимость это канал, по которому чужая правка приезжает к вам, и если родитель знает про конкретных детей, любой новый ребёнок правит родителя.
На фронтенде это видно на трёх уровнях:
- компоненты: вместо разбора типа внутри списка передавать содержимое через
childrenили render-prop. Список знает только контракт «мне дают, что рисовать», и новый тип блока его не трогает; - данные: компонент принимает функцию загрузки или хук, а не импортирует конкретный клиент. В тесте такая зависимость подменяется без перехвата сети;
- типы: в TypeScript абстракция это структурный тип рядом с потребителем, а не рядом с реализацией.
type Loader<T> = (id: string) => Promise<T>;
function useEntity<T>(id: string, load: Loader<T>) {
// компонент не знает про конкретный транспорт
}Стоит сразу обозначить границу: абстракция оправдана, когда реализаций больше одной или нужна подмена в тестах. Интерфейс под единственную реализацию это лишний файл и лишний прыжок при чтении кода.
Где спотыкаются
Пересказывают наследование: базовый класс, потомки, полиморфизм. Полиморфизм это механизм, а принцип про направление зависимостей, и базовый класс, который проверяет тип потомка, нарушает принцип, оставаясь полиморфным.
Абстрагируют всё подряд, заводя интерфейс на каждый сервис. Получается слой, который ничего не скрывает, а при первой же смене реализации выясняется, что контракт списан с прежней и всё равно течёт наружу.
Объявляют контракт в модуле реализации и импортируют его в потребителя. Зависимость при этом никуда не инвертировалась: файл с реализацией по-прежнему выше по графу импортов, и выкинуть его нельзя.