В этой статье я попытаюсь рассказать про принцип инверсии зависимостей (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.