Принцип инверсии зависимостей (dependency inversion principle)

В объектно-ориентированном проектировании принцип инверсии зависимостей представляет собой особую форму развязки программных модулей. Следуя этому принципу, обычные отношения зависимости, установленные от модулей высокого уровня, устанавливающих политику, к модулям низкого уровня, меняются местами, что делает модули высокого уровня независимыми от подробностей реализации модуля низкого уровня. Принцип гласит:

  • Модули высокого уровня не должны зависеть от модулей низкого уровня. Оба должны зависеть от абстракций (например, интерфейсов).
  • Абстракции не должны зависеть от деталей. Детали (конкретные реализации) должны зависеть от абстракций.

Предписывая, что объекты как высокого, так и низкого уровня должны зависеть от одной и той же абстракции, этот принцип проектирования инвертирует то, как некоторые люди могут думать об объектно-ориентированном программировании.

Идея, лежащая в основе пунктов этого принципа, заключается в том, что при проектировании взаимодействия между модулем высокого уровня и модулем низкого уровня взаимодействие следует рассматривать как абстрактное взаимодействие между ними. Это влияет не только на конструкцию модуля высокого уровня, но и на модуль низкого уровня: модуль низкого уровня должен быть разработан с учетом взаимодействия, и может потребоваться изменить его интерфейс использования.

Во многих случаях представление о взаимодействии само по себе как об абстрактной концепции позволяет уменьшить связность компонентов без введения дополнительных паттернов написания кода, что позволяет использовать только более легкую и менее зависимую от реализации схему взаимодействия.

Когда обнаруженная абстрактная схема (схемы) взаимодействия между двумя модулями является/являются общими и имеет смысл обобщение, этот принцип проектирования также приводит к паттерну инверсии зависимостей.

Традиционная структура слоев

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

Цель паттерна инверсии зависимостей состоит в том, чтобы избежать этого сильно связанного распределения при посредничестве абстрактного уровня и повысить возможность повторного использования более высокого уровня/уровней политик.

Модель инверсии зависимостей

С добавлением абстрактного слоя уровни как высокого, так и нижнего уровня уменьшают традиционные зависимости сверху вниз. Тем не менее, концепция "инверсии" не означает, что слои более низкого уровня зависят от слоев более высокого уровня. Оба слоя должны зависеть от абстракций, которые рисуют поведение, необходимое для слоев более высокого уровня.

При непосредственном применении инверсии зависимостей абстракции принадлежат верхним уровням/уровням политик. Эта архитектура группирует компоненты верхнего уровня/уровня политик и абстракции, которые определяют низшие сервисы вместе в одном пакете. Уровни более низкого уровня создаются путем наследования/реализации этих абстрактных классов или интерфейсов.

Инверсия зависимостей и владения поощряет возможность повторного использования более высоких уровней/уровней политик. Верхние уровни могут использовать другие реализации нижних сервисов. Когда компоненты нижнего уровня закрыты или когда приложение требует повторного использования существующих сервисов, обычно Адаптер является посредником между сервисами и абстракциями.

Обобщение модели инверсии зависимостей

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

  1. Проще видеть принцип хорошего мышления в качестве паттерна создания кода. После того, как абстрактный класс или интерфейс был реализован в коде, программист может сказать: "Я выполнил работу по абстракции".
  2. Поскольку многие инструменты модульного (юнит) тестирования полагаются на наследование для создания моков, использование общих интерфейсов между классами (не только между модулями, когда имеет смысл использовать универсальность), стало правилом.

Если используемый инструмент создания моков опирается только на наследование, может возникнуть необходимость в широком применении паттерна инверсии зависимостей. Это имеет серьезные недостатки:

  1. Простая реализация интерфейса над классом недостаточна для уменьшения связанности (coupling); только размышления о потенциальной абстракции взаимодействий могут привести к менее связанному дизайну.
  2. Внедрение универсальных интерфейсов повсюду в проекте усложняет понимание и поддержку. На каждом шаге читатель будет спрашивать себя, каковы другие реализации этого интерфейса, и ответ обычно таков: только моки.
  3. Обобщение интерфейса требует большего количества программного кода, в частности, на фабриках, которые обычно используют фреймворк внедрения зависимостей.
  4. Обобщение интерфейса также ограничивает использование языка программирования.
Ограничения обобщения

Наличие интерфейсов для реализации паттерна инверсии зависимостей (Dependency Inversion Pattern, DIP) имеет другие конструктивные последствия в объектно-ориентированной программе:

  • Все переменные-аттрибуты в классе должны быть интерфейсными или абстрактными.
  • Все конкретные пакеты классов должны соединяться только через интерфейс или пакеты абстрактных классов.
  • Ни один класс не должен быть производным от конкретного класса.
  • Ни один метод не должен переопределять реализованный метод.
  • Все экземпляры переменных требуют реализации порождающего паттерна (creational pattern), такого как фабричный метод или паттерн фабрика, или использования фреймворка внедрения зависимостей.
Ограничения создания моков интерфейса

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

  • Статически видимые элементы должны систематически полагаться на внедрение зависимостей, что делает их гораздо труднее в реализации.
  • Все тестируемые методы должны стать реализацией интерфейса или переопределением абстрактного определения.
Будущие направления

Принципы - это способы мышления, паттерны - это распространенные способы решения проблем. Паттерны создания кода могут рассматриваться как отсутствующие функции языка программирования.

  • Языки программирования будут продолжать развиваться, чтобы обеспечить более строгие и точные контракты на использование, по крайней мере, в двух направлениях: соблюдение условий использования (пре-, пост- и инвариантные условия) и интерфейсы на основе состояния. Это, вероятно, поощрит и потенциально упростит более сильное применение паттерна инверсии зависимостей во многих ситуациях.
  • Все больше и больше инструментов создания моков теперь используют внедрение зависимостей для решения проблемы замены статических и не виртуальных членов. Язык программирования, вероятно, будет развиваться, чтобы генерировать совместимый с созданием моков байт-код. Одно направление будет заключаться в ограничении использования не виртуальных членов, другое - генерировать, по крайней мере в тестовых ситуациях, байт-код, допускающий ложное наследование.

Реализации

Две распространенные реализации DIP используют одинаковую логическую архитектуру с различными последствиями.

Прямая реализация упаковывает классы политик с помощью сервисных абстракций классов в одной библиотеке. В этой реализации высокоуровневые компоненты и низкоуровневые компоненты распределяются в отдельные пакеты/библиотеки, где интерфейсы, определяющие поведение/службы, требуемые высокоуровневым компонентом, принадлежат и существуют в библиотеке высокоуровневого компонента. Реализация интерфейса компонента высокого уровня компонентом низкого уровня требует, чтобы пакет компонента низкого уровня зависел от компонента высокого уровня для компиляции, таким образом инвертируя обычные отношения зависимости.

На рисунках 1 и 2 показан код с той же функциональностью, однако на рисунке 2 для инвертирования зависимости использовался интерфейс. Направление зависимости может быть выбрано, чтобы максимизировать повторное использование кода политики и устранить циклические зависимости.

В этой версии DIP зависимость компонента нижнего уровня от интерфейсов/абстракций в слоях более высокого уровня затрудняет повторное использование компонентов нижнего уровня. Эта реализация вместо этого ″инвертирует″ традиционную зависимость сверху вниз до противоположности снизу вверх.

Более гибкое решение извлекает абстрактные компоненты в независимый набор пакетов/библиотек:

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

Примеры

Генеалогический модуль

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

Но для некоторых модулей более высокого уровня может потребоваться более простой способ просмотра системы: любой человек может иметь детей, родителей, братьев и сестер (включая сводных братьев и сестер или нет), бабушек и дедушек, двоюродных братьев и сестер и так далее.

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

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

В этом примере абстрагирование взаимодействия между модулями приводит к упрощенному интерфейсу модуля более низкого уровня и может привести к более простой его реализации.

Клиент удаленного файлового сервера

Представьте, что вам нужно внедрить клиент на удаленный файловый сервер (FTP, облачное хранилище ...). Вы можете подумать об этом как о наборе абстрактных интерфейсов:

  1. Соединение/Отключение (может потребоваться уровень постоянства соединения)
  2. Папка/метки создание/переименование/удаление/список интерфейсы
  3. Файлы создание/замена/переименование/удаление/чтение интерфейсы
  4. Поиск файлов
  5. Параллельная замена или удаление разрешения
  6. Управление историей файлов

Если и локальные, и удаленные файлы предлагают одинаковые абстрактные интерфейсы, любой высокоуровневый модуль, использующий локальные файлы и полностью реализующий паттерн инверсии зависимостей, сможет получить доступ к локальным и удаленным файлам без разбора.

Локальный диск обычно будет использовать папку, удаленное хранилище может использовать папку и/или метки. Вы должны решить, как объединить их, если это возможно.

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

Поиск файлов может быть подключаемым: поиск файлов может опираться на ОС или, в частности, на поиск по меткам или полному тексту, может быть реализован в разных системах (встроенных в ОС или доступных отдельно).

Одновременное обнаружение замены или удаления разрешения может повлиять на другие абстрактные интерфейсы.

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

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

Как только вы это сделаете, ваше приложение сможет прозрачно сохранять свои документы локально или удаленно. Проще говоря, модуль высокого уровня, использующий новые интерфейсы доступа к файлам, может использоваться неразличимо в сценариях локального или удаленного доступа к файлам, что делает его многоразовым.

Примечание: многие ОС могут уже реализовывать подобные функции, и ваша работа может быть ограничена, чтобы адаптировать ваш новый клиент к этим уже абстрагированным моделям.

В этом примере, рассматривая модуль как набор абстрактных интерфейсов и адаптируя другие модули к этому набору интерфейсов, вы можете предоставить общий интерфейс для многих систем хранения файлов.

Model View Controller (Модель Вид Контроллер)

Пакеты UI и ApplicationLayer содержат в основном конкретные классы. Контроллеры содержат абстракные/интерфейсные типы. У пользовательского интерфейса есть экземпляр ICustomerHandler. Все пакеты физически разделены. В ApplicationLayer есть конкретная реализация, которую будет использовать класс Page. Экземпляры этого интерфейса создаются фабрикой динамически (возможно, в том же пакете контроллеров). Конкретные типы, Page и CustomerHandler, не зависят друг от друга; оба зависят от ICustomerHandler.

Прямой эффект заключается в том, что пользовательский интерфейс не должен ссылаться на ApplicationLayer или какой-либо конкретный пакет, который реализует ICustomerHandler. Конкретный класс будет загружен с использованием отражения. В любой момент конкретная реализация может быть заменена другой конкретной реализацией без изменения класса пользовательского интерфейса. Еще одна интересная возможность заключается в том, что класс Page реализует интерфейс IPageViewer, который можно передать в качестве аргумента методам ICustomerHandler. Тогда конкретная реализация может взаимодействовать с пользовательским интерфейсом без конкретной зависимости. Опять же, оба связаны интерфейсами.

Связанные паттерны

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

Различные паттерны, такие как плагин, сервисный локатор или внедрение зависимостей, используются для облегчения обеспечения во время выполнения выбранной реализации компонента низкого уровня компоненту высокого уровня.

История

Принцип инверсии зависимостей был постулирован Робертом К. Мартином и описан в нескольких публикациях, включая статью "Метрики качества объектно-ориентированного проектирования: анализ зависимостей", статью, опубликованную в отчете C++ в мае 1996 г. и озаглавленную "Принцип инверсии зависимости", и книги Agile Software Development, Principles, Patterns, and Practices, и Agile Principles, Patterns, and Practices in C#.


Читайте также:

Комментарии

Популярные сообщения из этого блога

Язык поисковых запросов в Graylog

Хэш-таблица: разрешение коллизий

Нормальные формы, пример нормализации в базе данных