SOLID на практике — принцип инверсии зависимостей

Принцип инверсии зависимостей (Dependency Inversion PrincipleDIP, буква D в аббревиатуре SOLID), описанный Робертом Мартином, состоит из двух постулатов:

  • Высокоуровневые модули не должны зависеть от низкоуровневых; и те и другие должны зависеть от абстракций
  • Абстракции не должны зависеть от деталей, детали должны зависеть от абстракций

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

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

Рефакторинг с применением принципа инверсии зависимостей

Давайте вернёмся к примеру кода из поста про принцип единой ответственности:

Как видно, класс TopicService содержит три члена, к которым может быть применён принцип инверсии зависимостей. Более того, к самому классу TopicService он тоже может быть применён с целью скрыть детали реализации бизнес-логики от контроллера, являющегося частью сервисного слоя.

Таким образом мы получим интерфейс:

и реализующий его класс:

Аналогичным образом нужно выделить интерфейсы из остальных классов:

Сервис валидации:

Сервис проверки прав доступа:

Поскольку классы могут обращаться друг к другу опосредованно через интерфейсы и абстрактные классы, механизм создания объектов тоже изменится. Для создания объектов потребуется применением таких шаблонов проектирования как «Фабрика» и «Фабричный метод», либо механизма внедрения зависимостей, что в нашем примере реализуется средствами Spring Framework.

Использование совместно с другими принципами SOLID

Принцип инверсии зависимостей используется совместно с «Принципом подстановки [Барбары] Лисков» (LSP), позволяя иметь несколько реализаций одних и тех же интерфейсов и абстрактных классов и использовать их в зависимости от ситуации.

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

Аналогично, наличие интерфейсов или абстрактных классов, являющихся «божественными объектами» приводит к нарушению принципа единственной ответственности, а одним из решений проблемы «божественных объектов» является принцип разделения интерфейсов.

Сторонние зависимости и XML

Класс Topic и UpdateTopicAction, используемые в данном примере, имеют внешние зависимости в виде аннотаций Java EE Persistence API и Java EE Bean Validation API. Эти аннотации являются интерфейсами, таким образом принцип инверсии зависимостей частично соблюдён.

Но с другой стороны у проекта есть внешние зависимости, от которых хочется избавиться. У данной задачи есть два решения. Первое решение предполагает создание дополнительных классов, которые будут использовать указанные API, но использоваться только в конкретных реализациях TopicRepository и UpdateTopicActionValidationService. Для большего удобства предполагается использование шаблонов проектирования «Прокси», «Адаптер» и «Обёртка».

Второе решение — конфигурирование настроек взаимодействия со сторонними API при помощи XML-файлов, что позволяет изменять настройки без внесения изменений в классы, исключая таким образом необходимость в их повторной компиляции. И JPA и Bean Validation API поддерживают такую возможность. То же самое можно сказать про большинство наиболее распространённых фреймворков: Spring (Framework, Security, Integration и т.д.), Hibernate, Apache Camel и многих других.