Принцип инверсии зависимостей — доходчивое объяснение

0
3240
views

Наталия Ништа, PHP Developer, в своей статье на DOU.UA привела очень неординарное пояснение принципа инверсии зависимостей. Представляем его вашему вниманию.

Image by Peter Wolf from Pixabay

В этой статье я попытаюсь рассказать про принцип инверсии зависимостей (Dependency inversion principle, далее DIP). В статье будут упомянуты уровни абстракций, поэтому настоятельно рекомендую ознакомиться с этим понятием заблаговременно.

Завязка

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

Вспоминаем, что интерфейс — это средство осуществления взаимного воздействия; общая граница двух отдельно существующих составных частей, посредством которой они обмениваются информацией (честно списала из Википедии). Короче говоря, вот у нас есть механические наручные часы. И все взрослые люди знают, как читать время, используя интерфейс «циферблат со стрелочками». Я понятия не имею, как оно устроено внутри, какие там шестерёнки-колёсики, пружинки и прочее барахло. Мне не надо знать о богатстве внутреннего мира этого чуда инженерии. Я лишь знаю, что все механические часы поддерживают интерфейс «циферблат со стрелочками», и пользуюсь этим. Происходит абстрагирование от деталей реализации.

То есть, интерфейс — это абстракция. Давайте взглянем на это как на концепцию. Когда мы что-то проектируем, по сути нам важно лишь знать составные части системы и что они умеют делать. Как именно они умеют это делать — в момент конструирования никого не колышет. Выражаясь более заумно, нас интересуют уровни абстракции (и перечень элементов, находящихся в каждом уровне), а также их интерфейсы.

Все классы надо рассматривать как абстракции, обладающие своими интерфейсами. Это и значит проектировать на уровне интерфейсов, а не реализаций. Какую именно конструкцию языка в дальнейшем мы используем, Abstract Class или Interface, — по сути также не важно.

Если желаете, можно взглянуть на этот принцип и под другим углом: нам не обязательно знать, с каким конкретным классом (реализацией) мы имеем дело (часы фирмы такой-то, модель такая-то). Достаточно знать, какой у него суперкласс, чтобы пользоваться его методами (Abstract Class или Interface, в нашем примере это циферблат со стрелочками).

Кульминация

А теперь настало время чудес: я приведу наглядный образчик проектирования с кусками кода. Так как моим основным языком программирования является PHP (простите, так вышло), то и примеры я адаптирую под особенности этого языка.

Итак, любой музыкальный инструмент производит звуки (не важно какой именно — шумит себе и всё). Конструируем:

Например, это может быть барабан:

Или гитара, или губная гармошка, да что угодно. Но вот когда мои друзья-хипстеры решают, что мне непременно в жизни не хватает чего-то эдакого и сообщают, что подарят мне неведомый музыкальный инструмент, — во мне пробуждается непоколебимая уверенность, что я таки смогу извлечь из него хоть какой-то звук. Хотя я и не знаю заранее, что же за инструмент это будет.

Вот мы и сконструировали ряд классов, акцентируясь на том, что они умеют (т.е. на интерфейсах).

А теперь давайте немного усложним наш пример и продолжим проектировать на уровне интерфейсов. Мои друзья решили сэкономить и взять, что там они выбрали, в магазине подержанных инструментов. В нём перед продажей все инструменты ремонтируются и натираются до блеска (repair), а также заворачиваются в упаковку индивидуальной формы (pack). Очевидно, что инструменты разных производителей будут по-разному чиниться и по-разному упаковываться. На одном дыхании пишем следующие классы для нашей задачи.

Нам понадобится инструмент, из которого можно извлекать звук, его можно чинить и упаковывать:

Например вот такая губная гармошка фирмы Marys (только что придумала):

А также нам нужен магазин подержанных инструментов, который подготавливает инструменты к продаже:

Набор классов, мягко говоря, весёлый, но для примера нам подойдёт.

Мы не нарушали принципа «проектировать на уровне интерфейсов, а не реализаций». Мы создавали классы, концентрируясь на их способностях. Однако, давайте пристально взглянем на последний класс Pawnshop.

Допустим, по какой-то причине в будущем мы решим изменить интерфейс Instrument, в результате чего набор его методов станет другим. Или наш магазин решит вдобавок к подержанным балалайкам приторговывать ещё и абсолютно новыми инструментами, не нуждающимися ни в упаковке, ни в ремонте. Или ещё что-то произойдёт и конкретный музыкальный инструмент перестанет поддерживать знакомый нам интерфейс. Но в работе Pawnshop мы опираемся на надежду, что только что созданный конкретный объект гарантированно будет субклассом Instrument — это совершенно безрассудно. А сколько таких Pawnshop у нас по всему проекту — страшно даже представить.

Почему плохо зависеть от конкретных реализаций? Да потому, что они слишком часто меняются. А концепции (абстракции и интерфейсы) гораздо более живучи.

Развязка

Настало время взглянуть на определение принципа инверсии зависимостей, формулировок которого в ассортименте и количестве:
— Код должен зависеть от абстракций, а не от конкретных классов;
— Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций;
— Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.

Данный принцип призывает не только проектировать на уровне интерфейсов, но и пресечь беспорядочное использование конструкции new, потому что она создаёт конкретную реализацию. Соответственно, класс, в котором используется new, автоматически становится зависимым от этой конкретной реализации, а не от абстракции. Не смотря даже на то, что мы проектировали на уровне интерфейсов.

Если всё так просто, то почему же этот принцип называется «инверсия зависимостей». Что инвертируется?

Вернёмся к нашим музыкальным инструментам. Хотя мы и строили классы, проектируя их на уровне интерфейсов, всё равно наш класс Pawnshop зависит от конкретных реализаций:

Если мы попытаемся применить DIP, нам нужно будет изолировать все new внутри некоторой ограниченной области — для этого надо использовать какой-то из порождающих паттернов или Dependency Injection. В результате мы можем получить совершенно другую картину.

Например, можем сделать так:

Или так:

Или ещё как-нибудь. Суть в том, что теперь мы получаем объекты гарантированного типа. Новая схема зависимостей будет выглядеть так:

Стрелки, идущие к конечным реализациям (MarysHarmonica, BillysDrum и др.), поменяли своё направление. Мы инвертировали зависимости. До применения принципа DIP у нас присутствовала зависимость Pawnshop от конкретных классов музыкальных инструментов. Теперь же ничто не зависит от конечных реализаций, всё зависит только от абстракций. За исключением наших «изоляторов», куда мы поместили new. Но изменить механизм создания экземпляров внутри этих ограниченных конструкций гораздо легче, чем рыскать по необъятным просторам кода, выискивая, где же мы наплодили наши вновь изменившиеся объекты.

Данный принцип (ограничение new) не применим к библиотечным классам. Потому что мы не будем их менять никогда. А раз эти классы «хронически неизменны», то и связанные с изменениями риски отпадают.

Итак, коротко говоря, принцип DIP призывает:
— проектировать на уровне интерфейсов;
— локализовать создание изменяемых классов (скажи нет беспорядочным new!).

И вот мы вновь убедились, что ООП — это до тошноты логическая и достаточно простая для понимания вещь.

P.S. Использовав конструкцию php <new $firmName . $instrumentName> я экономлю количество строк кода, акцентируя ваше внимание на происходящем внутри метода. Для дотошных: можете представить себе, что вместо этой строки там написано if-if-if.

P.P.S. Да, да «никто не будет писать такого в боевом проекте». Однако, используя упрощённые примеры, я полагаю, мне удалось продемонстрировать работу принципа DIP.

ОСТАВЬТЕ ОТВЕТ

Please enter your comment!
Please enter your name here