Зачастую, когда речь заходит про принцип инверсии зависимостей, можно услышать, что инверсия зависимостей (далее DIP) — это что-то там про зависимость от абстракций, и приводятся примеры, где в качестве «плохого» случая, используются конкретные классы, а в исправленном случае, используются абстрактные классы или интерфейсы. Но такая трактовка принципа в корне неверна.
Почему такая трактовка неверна и в чем же суть принципа — об этом и пойдет речь далее.

Итак, вспомним определение DIP:
Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба модуля должны зависеть от абстракций.
Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Начнем разбор принципа с самого названия. В названии содержится слово «инверсия», поэтому логично предположить, что какая-то зависимость, в результате применения принципа, должна поменять свое направление на противоположное и в этом действии должен быть какой-то смысл.
Если бы это было не так (зависимость не меняла бы направление), то об этом уже давно бы все говорили и знали о несоответствии названия принципа и его содержания. Раз никто по поводу самого названия принципа не спорит, значит инверсия все же где-то есть. То есть, смена направления зависимости должна присутствовать обязательно!
Опираясь на сказанное выше и будем строить дальнейший разбор принципа.
Что такое зависимость?
Сейчас ненадолго отойдем от DIP и разберемся, что же такое зависимость.
Например, когда говорят, что класс A
зависит от класса B
, то это означает, что в классе A
физически находится код из класса B
и мы не можем класс A
вынести, условно, в другой проект, так как внутри него содержится/вызывается код, который объявлен в другом классе/файле и если мы хотим вынести класс A
в другой проект, то должны вместе с ним «тянуть» за собой и класс B
.

А что означает, когда говорят, что класс A
не зависит от класса B
? Это означает, что в классе A
нет ни одной строчки кода из класса B
и вообще класс A
ничего не знает про класс B
, поэтому, если мы захотим перенести класс A
в другой проект, то ничего нам не помешает этого сделать.
У нас может быть всего два случая взаимодействия сущностей:
-
A зависит от B
-
A не зависит от B. B зависит от A
И при переходе от одного случая к другому зависимость меняет направление на противоположное, или другими словами, инвертируется.
DIP
Теперь вернемся к DIP и обратим внимание на первый пункт принципа, а конкретно на слова «Модули верхних уровней не должны зависеть от модулей нижних уровней».
Это, на мой взгляд, ключевое предложение этого принципа, которое непосредственно отражает само название принципа, а предложения про абстракции будут соблюдаться сами собой при корректной реализации первого пункта принципа.
Модуль — функционально связанная часть системы. Для простоты, чтобы не задерживаться на терминах «функционально связанная часть системы», можно воспринимать модуль, как директорию с файлами кода, которые отвечают за какой-то совместный функционал, общий именно для данной директории.
Модуль верхнего уровня (далее МВУ
) — это модуль, который в конечном итоге использует реализацию другого модуля. Можно провести аналогию из природы: модуль верхнего уровня — это модуль, который находится выше в «пищевой цепочке», т.е. потребляет (имеет внутри себя) код другого модуля.
Модуль нижнего уровня (далее МНУ
) — это тот модуль, реализация которого используется другим модулем.
Итак, исходный случай, в котором нарушается принцип DIP, т.е. когда модуль верхнего уровня зависит от модуля нижнего уровня, выглядит так:

Для того, чтобы взаимодействие между двумя модулями соответствовало DIP, нужно сделать так, чтобы МВУ
не зависел от МНУ
.
В сноске про «зависимость» уже упоминалось, что означает «не зависит». Т.е. в МВУ
не должно быть ни одной строчки кода из МНУ
.
Отсюда может возникнуть справедливый вопрос: тогда от чего будет зависеть модуль верхнего уровня, ведь он так или иначе должен пользоваться реализацией модуля нижнего уровня? Ответ прост: модуль верхнего уровня должен зависеть сам от себя, а именно, от интерфейса, которого мы ему добавим, а сам интерфейс уже будет реализовывать модуль нижнего уровня.

На самом деле, не имеет большой разницы от какой именно собственной пограничной (находящейся на границе между двумя модулями) сущности будет зависеть модуль верхнего уровня. Интерфейс это или класс или что-то еще — неважно, главное чтобы соблюдалось основное условие — МВУ
должен зависеть сам от себя. Далее будем считать, что собственная пограничная сущность модуля верхнего уровня — это интерфейс, но, повторюсь, это не обязательно должен быть интерфейс.
Ключевой момент DIP - это то, что интерфейс должен принадлежать МВУ
. Поэтому, когда мы хотим понять, соблюдается ли DIP в коде, первый вопрос, на который нужно получить положительный ответ: принадлежит ли интерфейс модулю верхнего уровня? Если интерфейс принадлежит модулю нижнего модуля, то никакой инверсии зависимости нет, мы как зависели от МНУ
, так и зависим.
Итого, шаги определения корректности реализации DIP:
-
определиться с границами модулей
-
убедиться, что интерфейс принадлежит
МВУ
Т.е. DIP — это про изоляцию модуля, про уменьшение связанности. Роберт Мартин в своей книге «Чистая архитектура» много пишет об уменьшении связанности и DIP не выбивается из этой канвы повествования.
В чем ошибка многих статей о DIP?
Основная ошибка большинства статей, видеороликов и других обучающих материалов о DIP в том, что никто не говорит о границах модуля, а это является одним из ключевых моментов принципа. Ведь пограничную сущность мы должны добавлять именно в МВУ
, чтобы МВУ
зависел сам от себя, иначе DIP будет не выполнен.
Зачастую, вообще, игнорируется первый пункт принципа, и говорится только про зависимость от абстракций. Сами по себе абстракции ничего не инвертируют.
Весь проект может быть построен на абстракциях и при этом в нем может не быть ни одной инверсии зависимостей. Зависимость от абстракций и инверсия зависимостей — это не одно и то же.
Даже если приводить пример наследования классов/интерфейсов на UML диаграмме, то мы увидим, что зависимость всегда смотрит в одном направлении и не зависит от «степени абстрактности» сущности.

Проверьте себя, выбрав правильный ответ:

1. DIP реализован корректно
2. DIP реализован некорректно
3. Недостаточно данных
Скрытый текст
Ответ: 3. Недостаточно данных.
Здесь нужно определиться с границами модулей. Если интерфейс принадлежит МВУ, то принцип выполнен корректно, иначе DIP не выполнен.

1. DIP реализован корректно
2. DIP реализован некорректно
3. Недостаточно данных
Скрытый текст
Ответ: 2. DIP реализован некорректно.
Интерфейс должен принадлежать модулю верхнего уровня.

1. DIP реализован корректно
2. DIP реализован некорректно
3. Недостаточно данных
Скрытый текст
Ответ: 1. DIP реализован корректно.
Подписывайтесь на мои соц. сети АйТиКартоха. Я только начинаю :-)
Автор: itkartoha