Что такое dependency inversion
Ричард Фейнман был прекрасным рассказчиком, способным ясно и доходчиво объяснять сложные вещи (см., например, это видео). Джоэл Спольски считает, что по-настоящему умный программист обязательно должен уметь говорить на языке непрофессионала (а не только на языке программирования C). И почти всем известен афоризм Альберта Эйнштейна: «Если вы не можете что-то объяснить шестилетнему ребенку, значит, вы сами этого не понимаете». Конечно, я не сравниваю вас с шестилетним ребенком, но я постараюсь рассказать о DI, IoC и другом DI наиболее ясно и внятно.
Не все зависимости стоят того, чтобы их инвертировать
Модули теперь меньше связаны между собой, чего мы собственно и добивались. Мы не стали делать это для всего, поскольку изменений в других модулях ближайшие пару лет не предвидится. Не стоит волноваться об изменениях в том, что редко меняется. А вот если у вас есть куски системы, которые меняются часто, или вы просто сейчас не знаете что там будет по итогу, имеет смысл защититься от возможных изменений.
К примеру, если нам понадобится логгер, мы всегда сможем использовать интерфейс PSR\Logger поскольку он стандартизирован, а такие вещи крайне редко меняются. Затем мы сможем выбрать любой логгер реализующий этот интерфейс на наш вкус:
Как вы можете видеть, благодаря этому интерфейсу, наше приложение все еще не зависит от конкретного логгера. Логгер же зависит от этой абстракции. Но оба "модуля" не зависят друг от друга.
Инверсия зависимостей
Итак, мы уже определились что модуль E все ломает. И ваш коллега захотел защититься от будущих изменений в "чужом" коде. Как никак, он из этого модуля использует только одну функцию.
Очень важно то, что у нас два интерфейса, а не один. Если бы мы поместили интерфейс в модуль E, мы бы не устранили зависимости между модулями. Тем более, разным модулям требуются разные возможности. Наша задача изолировать ровно ту часть, которую мы собираемся использовать. Это значительно упростит поддержку.
Так же, если вы посмотрите на картинку выше, вы можете заметить, что поскольку реализация адаптеров лежит в модуле E, теперь этот модуль вынужден реализовывать интерфейсы из других модулей. Тем самым мы инвертировали направление стрелочки, указывающей зависимость. Мы инвертировали зависимости.
Инверсия контроля
Что вы обычно делаете в свой выходной? Может, вы читаете книги. Может, вы играете в видеоигры. Может быть, вы пишете код, а может, пьете пиво во время просмотра какого-нибудь сериала (вместо того, чтобы сажать яблони на Марсе). Но что бы вы ни делали, в вашем распоряжении целый день, и вы единолично контролируете свое расписание.
Инверсии зависимостей (Dependency Inversion)
DIP в первую очередь заботится о том, чтобы класс зависел только от абстракций более высокого уровня. Например, интерфейсы существуют на более высоком уровне абстракции, чем конкретный класс.
DIP не касается внедрения зависимостей, хотя шаблон внедрения зависимостей является одним из многих методов, которые могут помочь обеспечить уровень косвенного обращения, необходимый, чтобы избежать зависимости от деталей низкого уровня и связи с другими конкретными классами.
Рассмотрим сценарий на языке со статической типизацией, где классу требуется возможность читать запись из базы данных приложения:
В приведенном выше примере, несмотря на использование Dependency Injection, класс Foo по-прежнему жестко зависит от SqlRecordReader, но единственное, что его действительно волнует, это то, что существует метод с именем readAll (), который возвращает некоторые записи.
Рассмотрим ситуацию, когда запросы к базе данных SQL позже реорганизуются в отдельные микросервисы, требующие изменения кодовой базы; вместо этого классу Foo потребуется читать записи из удаленной службы. Или, в качестве альтернативы, ситуация, когда модульные тесты Foo должны считывать данные из хранилища в памяти или плоского файла.
Если, как следует из названия, SqlRecordReader содержит базу данных и логику SQL, для любого перехода к микросервисам потребуется изменение класса Foo.
Рекомендации по инверсии зависимостей предполагают, что SqlRecordReader следует заменить абстракцией более высокого уровня, которая предоставляет только метод readAll () то есть использовать интерфейс:
Согласно DIP, IRecordReader является абстракцией более высокого уровня, чем SqlRecordReader, и принуждение Foo зависеть от IRecordReader вместо SqlRecordReader соответствует рекомендациям DIP.
Модули
модуль — логически взаимосвязанная совокупность функциональных элементов.
Что бы не было недопонимания, введем немного терминологии. Под модулем мы будем понимать любую функционально связанную часть системы. Например фреймворк мы можем поместить как отдельный независимый модуль, а логику работы с пользователями — в другой.
Модуль, это ничто иное, как элемент декомпозиции системы. Модуль может включать в себя другие модули, формируя что-то вроде дерева. Соответственно можно выделить модули разных уровней:
Здесь стрелочки между модулями показывают кто что использует. Соответственно эти же стрелочки будут показывать нам направления зависимостей между нашими модулями.
И вот пришло время добавить "еще одну кнопочку". И мы понимаем что функционал этой кнопки реализован в модуле E. Мы не раздумывая полезли добавлять то что нам надо, и нам пришлось поменять интерфейс взаимодействия с нашим модулем.
А еще коллеге вашему прилетел баг от тестировщика, мол модуль C сломался. Оказалось что он по неосторожности завязался на ваш модуль E, а вам об этом не сказал. Да еще и модуль этот состоит из кучи файлов, и всем от модуля E что-то нужно. И вот теперь и он лазает по своей части графа зависимостей (потому что ему проще в нем ориентироваться чем вам, не ваша же часть системы) и проклинает вас.
Инверсия зависимостей
- Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба типа модулей должны зависеть от абстракций.
- Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
На диаграмме UML графически показаны оба варианта.
Проблема возникает, когда вы спрашиваете, где же здесь реальная инверсия? Основная идея, которая позволяет нам ответить на этот вопрос, заключается в том, что интерфейсы принадлежат не их реализациям, а их клиентам. Название интерфейса, IBar, сбивает с толку и заставляет нас видеть пару IBar + Bar как единое целое. Но истинным владельцем IBar является класс Foo, и если вы примете это во внимание, то направление связи между Foo и Bar действительно меняется.
Внедрение зависимости
Глядя на полученный код, внимательный читатель заметит, что, несмотря на введение промежуточной абстракции, класс Foo по-прежнему отвечает за создание экземпляра класса Bar. Очевидно, это не то разделение, которого мы ожидали.
DIP в реальном мире
Перед тем как мы начнем писать код, я бы хотел рассказать вам историю. В Syneto мы были не всегда так внимательны к нашему коду. Несколько лет назад мы знали гораздо меньше, хотя и старались делать лучшее, на что были способны, не все наши проекты были такими блестящими. Мы прошли через ад и вернулись назад, но при этом мы выучили много ошибок.
Принципы SOLID и принципы чистой архитектуры Дядюшки Боба (Роберта C. Мартина) трансформировали наш стиль кодирования такими разными способами, которые тяжело описать. Я постараюсь проиллюстрировать, несколько основных архитектурных решений, введенных с помощью DIP, которые оказали большое влияние на наши проекты.
Большинство веб-проектов состоят из трех основых технологий: HTML, PHP и SQL. Определенные версии приложений, о которых мы говорим, или какую версию SQL вы используете, совсем не важно. Дело в том, что информация из HTML-формы должна в конечном итоге оказаться в базе данных. Связующим звеном между ними может стать PHP.
Важно понять то, как красиво эти три технологии представляют собой три разных архитектурных слоя: пользовательский интерфейс, бизнес-логика и хранилище. Мы поговорим о последствиях этих слоёв через минуту. Теперь давайте остановимся на некоторых странных, но часто встречающихся решениях заставить эти технологии работать вместе.
Много раз я видел проекты, которые используют SQL запросы внутри PHP тегов в HTML файле, или PHP код, который выводит HTML страницу и напрямую обрабатывает глобальные переменные $_GET или $_POST . Но почему это плохо?
Изображения выше представляют сырую версию того, что мы описали в предыдущем параграфе. Стрелки обозначают разные зависимости, и как мы можем судить, по сути все зависит от всего. Если нам необходимо поменять таблицу в базе данных, то в итоге нужно будет изменить HTML файл. Или же если мы поменяем поле в HTML, то нам потребуется изменить имя колонки в SQL запросе. Или если мы взглянем на вторую схему, нам скорее всего потребуются изменения в PHP, если изменится HTML, или в самом худшем случае, когда мы генерируем весь HTML контент внутри PHP файла, нам обязательно придется внести изменения в PHP, чтобы изменить содержимое HTML. Таким образом, нет никаких сомнений, что зависимости жестко переплетены между классами и модулями. Но это еще не все. Вы еще мажете хранить процедуры; PHP код в SQL таблицах.
На схеме выше запросы к SQL базе возвращают PHP код, cгенерированный данными из таблиц. Эти PHP функции или классы выполняют другие SQL запросы, которые в свою очередь возвращают новый PHP код, и так по кругу, пока в конце концов не будет получена вся необходимая информация.
Я знаю, это может показаться возмутительным для многих из вас, но если вы еще не работали с проектом, который работает таким образом, то это безусловно еще будет в вашей будущей карьере. Большинство существующих проектов, независимо от используемого языка программирования, написаны с использованием старых принципов программистами, которые не заботились о том, чтобы сделать что-то лучше. Если вы читаете эти статьи, вы вероятнее всего на уровень выше чем они. Вы уважаете свою профессию, и стараетесь все делать как можно лучше.
Другой же вариант может быть в том, чтобы повторять ошибки ваших предшественников и жить с последствиями. В Syneto один из наших проектов достиг почти такого состояния, когда его уже невозможно было поддерживать из-за его старой сильно связанной архитектуры и мы решили оставить его навсегда и никогда больше не идти по такому пути. С тех пор мы стремились к чистой архитектуре, к соблюдению SOLID принципов, и больше всего к инверсии зависимостей.
Самое удивительно в этой архитектуре то, куда указывают зависимости:
- Пользовательский интерфейс (в большинстве случаев это MVC фреймворк) или любой другой механизм в вашем приложении будет зависеть от бизнес-логики. Бизнес-логика является довольно абстрактной. Пользовательский интерфейс довольно конкретный. Таким образом пользовательский интерфейс является деталью или уточнением проекта, и он может часто меняться. Ничто не должно зависеть от пользовательского интерфейса, ничто не должно зависеть от вашего MVC фреймворка.
- Мы можем сделать еще одно интересное наблюдение об уровне хранения, базе данных, MySQL или PostgreSQL, в зависимости от вашей логики. Бизнес логика не должна зависеть от базы данных. Это позволяет по желанию переключаться между хранилищами. Если завтра вам нужно будет сменить MySQL на PostgreSQL или даже простые текстовые файлы, вы сможете это сделать. Вам конечно же потребуется реализовать определенный уровень хранения для нового хранилища, но вам не нужно будет изменять ни одной строчки в бизнес логике. Вы можете прочитать более детальное объяснение темы хранения данных в статье Движение в Строну Слоя Хранения Данных.
- Наконец справа от бизнес-логики, вне ее, у нас есть все классы, которые создают классы бизнес-логики. Это фабрики и классы, созданные точкой входа в наше приложение. Много людей склонны думать, что эти классы принадлежат к бизнес логике, так как они создают классы бизнес логики, это основная причина их существования. Но это просто классы, которые помогают нам создавать другие классы. Бизнес объекты и логика, которая в них содержится, не зависят от этих фабрик. Мы можем использовать разные шаблоны, как например Простая фабрика, Абстрактная фабрика или строитель или даже простое создание объектов. Это не имеет значения. Как только бизнес объекты были созданы, они могут начать выполнять свою работу.
Покажите мне код
Применить принцип инверсии зависимостей на архитектурном уровне достаточно просто если вы используете шаблоны проектирования. Применение его внутри бизнес-логики также является довольно простым и даже веселым занятием. Представим, что у нас есть приложение, которое читает электронные книги.
Начнем с реализации чтения книг в формате PDF. Пока все в порядке. У нас есть класс PDFReader , который использует PDFBook . Метод read() читателя дерегирует выполнение методу read() книги. Мы просто проверяем это, выполнив проверку регулярным выражением после ключевой части строки, возвращаемой методом reader() класса PDFBook .
Пожалуйста, имейте в виду, что это всего лишь пример. Мы не будем реализовывать логику чтения PDF файлов или других форматов. Вот почему наши тесты просто проверяют некоторые основные строки. Если бы мы собирались написать реальное приложение, то разница была бы только в том, как мы тестируем разные форматы файлов. Схема зависимостей будет очень похожа на наш пример.
PDF читатель, который использует PDF книгу, может быть решением для ограниченного приложения. Но если бы нашей задачей было написать не только PDF читателя, то данное решение абсолютно нам не подходит. Но мы хотим написать универсальную читалку, которая будет поддерживать несколько форматов, среди которых будет наша уже реализованная версия PDF. Так что давайте переименуем наш класс читателя.
Переименование не создаст никаких функциональных эффектов. Тесты по-прежнему проходят.
Testing started at 1:04 PM .
PHPUnit 3.7.28 by Sebastian Bergmann.
Time: 13 ms, Memory: 2.50Mb
OK (1 test, 1 assertion)
Process finished with exit code 0
Но это будет иметь значимый эффект в архитектуре.
Наш читатель стал более абстрактным. Гораздо более обобщенным. Мы имеем общего EBookReader , который может работать со специфичным форматом книги PDFBook . Абстракция зависит от детали. Тот факт, что наша книга имеет тип PDF является деталью, и ни что не должно зависеть от него.
Наиболее общее и часто используемое решением является инверсия зависимости, чтобы представить более абстрактный модуль в нашем дизайне. "Самым абстрактным элементом в ООП является интерфейс. Таким образом любой другой класс может зависеть интерфейса и по-прежнему соблюдать DIP".
Мы создали интерфейс для нашего читателя. Интерфейс называется EBook и представляет интересы класса EBookReader . Это прямое следствие соблюдения принципа разделения интерфейсов, который продвигает идею о том, что интерфейсы должны отражать потребности клиентов. Интерфейсы принадлежат клиентам, поэтому им и дают такие имена, которые отражают типы и объекты, в которых клиенты нуждаются, и также содержат методы, которые клиенты хотят использовать. Вполне естественно, что EBookReader будет использовать использовать метод read() у EBooks .
Вместо одной зависимости, мы здесь теперь имеем две.
- Первая зависимость указывает от EBookReader к EBook интерфейcу, и обозначает тип использования. EBookReader использует EBooks .
- Вторая зависимость другая. Она указывает от PDFBook к тому же EBook интерфейсу, он это уже тип реализации. Класс PDFBook является лишь уточнением EBook , и поэтому реализует этот интерфейс, чтобы удовлетворить запросам клиента.
Неудивительно, что это решение также позволяет нам подключать различные типы электронных книг в нашу читалку. Единственным условием для всех этих книг будет - соблюдать EBook интерфейс и реализовать его.
Что в свою очередь подводит нас к принципу открытости/закрытости, и круг замыкается.
Принцип инверсии зависимостей помогает нам в итоге подойти к соблюдению всех остальных принципов. Соблюдение принципа инверсии зависимостей:
- Подтолкнет к соблюдение принципа открытости/закрытости.
- Позволит разделить обязанности.
- Позволит правильно использовать подтипы.
- Даст возможность разделить интерфейсы.
Интерфейсы и позднее связывание
Позднее связывание означает, что объект связывается с вызовом функции только во время исполнения программы, а не на этапе компиляции.
Как известно, интерфейсы определяют некий контракт. И каждый объект, реализующий этот контракт, обязан его соблюдать. Например пишем мы регистрацию пользователей. И вспоминаем требование — пароль пользователя должен быть надежно захэширован на случай утечки данных из базы. Предположим что в данный момент мы не знаем как правильно это делать. И предположим что мы еще не выбрали фреймворк или библиотек для того чтобы делать проект. Безумие, я знаю… Но давайте представим что у нас сейчас нет ничего, кроме логики приложения.
Мы вспоминаем о требовании, но не бросать же нам все? Давайте все же сначала закончим с регистрацией пользователя, а уж потом будем разбираться как чего делать. Надо все же последовательно подходить к работе. А потому вместо того чтобы гуглить "как правильно хэшировать пароль" или разбираться как это делать в нашем фреймворке, давайте сделаем интерфейс PasswordEncoder . Сделав это, мы создадим "контракт". Мол всякий кто решится реализовать этот интерфейс, обязан предоставить надежное и безопасное хэширование пароля. Сам же интерфейс будет до безумия простым:
Это именно то, что нам нужно для работы в данный момент времени. Мы не хотим знать как это будет происходить, мы еще не знаем про соль и медленное хэширование. Мы можем сделать сделать заглушку, которая будет на момент разработки возвращать то, что мы запихнули. А уж потом сделаем нормальную реализацию. Точно так же мы можем поступить с отправкой email-а о том что мы успешно зарегистрировали пользователя. Мы можем даже параллельно посадить еще людей, которые будут эти интерфейсы реализовывать для нас, что бы дело быстрее шло. Красота.
А прелесть в том, что мы можем динамически заменить реализацию. То есть непосредственно перед вызовом регистрации пользователя выбрать, какой энкодер паролей нам надо использовать. Именно это подразумевается под поздним связыванием. Возможность "выбрать" реализацию прямо перед использованием оной.
В языках с динамической системой типов, такой как в PHP, есть еще более простой способ добиться позднего связывания — не использовать тайп хинтинг. От слова совсем. Правда сделав это, мы полностью потеряем статическую (представленную явно в коде) информацию о том, кто что использует. И когда мы что-то поменяем, нам уже не выйдет так просто определить, не сломался ли код. Это как выключить свет и искать парные носки в горе из 99 одного левого и 1-ого правого.
Заключение
Прим. переводчика: далее оригинальная статья заканчивается. Но мне захотелось ее расширить повторением определений основных терминов Dependency Injection и Dependency Inversion, взятыми с этой страницы StackOverflow .
Изоляция
Интерфейсы и позднее связывание позволяют нам "абстрагировать" реализацию логики от посторонних деталей. Мы должны стараться делать модули как можно более изолированными и самодостаточными. Когда все модули независимы, мы получаем возможность и независимо их развивать. А это может быть важно с точки зрения бизнеса.
Часто, когда речь заходит об абстракциях, люди любят доводить все до крайности, забывая зачем изначально все это нужно.
Когда проект планируется поддерживать намного дольше, нежели период поддержки вашего фреймворка, имеет смысл все используемые вещи завернуть в адаптеры. Это своего рода крайность, но в таких условиях она оправдана. Менять фреймворк мы врядли будем, а вот обновить мажорную версию в будущем без боли мы пожалуй бы хотели.
Или к примеру еще одно распространенное заблуждение — абстракция от хранилища. Возможность полной замены базы данных ни в коем случае не является целью реализации этой абстракции, это скорее критерий качества. Вместо этого мы просто должны дать такой уровень изоляции, чтобы наша логика не зависела от возможностей базы данных. Причем это не значит что мы не должны пользоваться этими возможностями.
К примеру мы реализовали поиск в нашей любимой MySQL, но в итоге потребовался более качественная реализация. И мы решили взять ElasticSearch для этого, просто потому, что с ним поиск делать быстрее. Отказываться от MySQL мы так же не можем, но благодаря выстроенной абстракции, мы можем добавить еще одну базу данных, чтобы эффективнее выполнить конкретную задачу.
Или мы делаем очередную соц сеть, и нам надо как-то трекать репосты. Да, мы можем сделать это на MySQL но выйдет неудобно. Тут напрашиваются графовые базы данных. И таких сценариев массы. Мы должны руководствоваться здравым смыслом в первую очередь а не догмами.
Разбираемся с SOLID: Инверсия зависимостей
Давайте глянем на определение принципа инверсии зависимостей из википедии:
A. Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
B. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Большинство разработчиков, с которыми мне доводилось общаться, понимают только вторую часть определения. Мол "ну а что тут такого, надо завязывать классы не на конкретную реализацию а на интерфейс". И вроде бы верно, но только кому должен принадлежать интерфейс? Да и почему вообще этот принцип так важен? Давайте разбираться.
Принцип инверсии зависимостей — доходчивое объяснение
Наталия Ништа, PHP Developer, в своей статье на DOU.UA привела очень неординарное пояснение принципа инверсии зависимостей. Представляем его вашему вниманию.
В этой статье я попытаюсь рассказать про принцип инверсии зависимостей (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 !).
И вот мы вновь убедились, что ООП — это до тошноты логическая и достаточно простая для понимания вещь.
Определение
A. Модули высокого уровня не должны зависеть от модулей более низкого уровня. Все они должны зависеть от абстракций.
B. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
SOLID: часть 4 - Принцип инверсии зависимостей
Принцип единственной ответственности (SPR), открытости/закрытости(OCP), подстановки Лисков, множественности интерфейсов и инверсии зависимостей. Пять гибких принципов, которыми вы должны руководствоваться каждый раз, когда пишете код.
Было бы несправедливо сказать вам, что какой-либо из SOLID принципов является более важным, чем другой. Однако возможно ни один из других принципов не имеет такого назамедлительного и глубокого влияния на ваш код, как принцип инверсии зависимостей, или сокращенно DIP. Если вы сочтете другие принципы тяжелыми для понимания и применения, то следует начать с этого и затем применять остальные к коду, который уже соблюдает принцип инверсии зависимостей.
Шаблон Dependency Injection
То есть класс может указывать свои переменные экземпляра, но не выполняет никакой работы по заполнению этих переменных экземпляра (за исключением использования параметров конструктора в качестве «сквозного»)
Класс, разработанный с учетом внедрения зависимостей, может выглядеть так:
С другой стороны, класс Foo, используемый без внедрения зависимостей, может просто создать сами экземпляры Meow и Woof или, возможно, использовать тот же ServiceLocator или фабрику сервисов:
Таким образом, внедрение зависимостей просто означает, что класс отложил ответственность за получение или предоставление своих собственных зависимостей; вместо этого эта ответственность лежит на том, кто хочет создать экземпляр. (Обычно это контейнер IoC)
Заключительные мысли
Вот и все. Мы это сделали. Все статьи о SOLID принципах закончены. Для меня лично, открытие этих принципов и реализация проектов с соблюдением этих принципов, было огромным прорывом. Я полностью поменял свой способ мышления об архитектуре и дизайне, и могу сказать что после этого все проекты, над которыми я работал стали гораздо проще в обслуживании и понимании.
Я считаю, что SOLID принципы являются одной из основных концепций в объектно-ориентированном дизайне. Эти концепции должны указывать нам путь в улучшении нашего кода, и наша жизнь, как программиста, станет гораздо легче. Программистам гораздо легче понять правильно организованный код. Компьютеры умны, они поймут любой код, не смотря на его сложность. А вот люди с другой стороны имеют ограниченное количество вещей, на которых они могут сфокусироваться. Говоря более конкретно количество таких вещей является магическом числом семь, плюс или минус два.
Нам следует стремиться организовывать наш код, основываясь на этих цифрах, и вот несколько техник, которые могут в этом помочь. Максимальная длина функций должна быть не более чем четыре строчки (пять вместе с заголовком), таким образом они полностью могут поместиться у нас в уме. Отступы должны углубляться не более чем на пять уровней. Классы не более чем с пятью методами. В шаблонах проектирования количество используемых классов обычно варьируется от пяти до девяти. Наша архитектура высокого уровня выше содержит от четырех до пяти концепций. Существует пять SOLID принципов, каждый из которых требует от пяти до девяти концепций/модулей/классов для примеров. Идеальный размер команды разработчиков между пятью и девятью. Идеальное количество команд в компании также между пятью и девятью.
Как видите, магическое число семь, плюс минус два встречается повсюду, так почему же ваш код должен отличаться.
Читайте также: