Принцип инверсии зависимостей (Dependency Inversion Principle — DIP, буква D в аббревиатуре SOLID), описанный Робертом Мартином, состоит из двух постулатов:
- Высокоуровневые модули не должны зависеть от низкоуровневых; и те и другие должны зависеть от абстракций
- Абстракции не должны зависеть от деталей, детали должны зависеть от абстракций
Инверсия зависимостей заключается в том, что модули разных уровней зависят не друг от друга, а от абстракций. В общих чертах принцип инверсии зависимостей сводится к следующему набору простых правил:
- Взаимодействие между классами должно быть реализовано через интерфейсы или абстрактные классы
- Типами всех членов классов должны быть интерфейсы или абстрактные классы
- Классы, являющиеся конечными реализациями не должны расширяться (или должны быть финальными)
- Аналогично методы не должны перекрываться при наследовании (или быть финальными)
Рефакторинг с применением принципа инверсии зависимостей
Давайте вернёмся к примеру кода из поста про принцип единой ответственности:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class TopicService { private final TopicRepository topicRepository; private final UpdateTopicActionValidationService updateTopicActionValidationService; private final TopicAccessCheckService topicAccessCheckService; public Topic update(UpdateTopicAction action, int id, int userAccountId) { Topic topic = this.topicRepository.findOneById(id) .orElseThrow(EntityNotFoundException::new); if (!this.topicAccessCheckService.hasAccess(topic, userAccountId)) { throw new EntityActionForbiddenException(); } Set<ConstraintViolation<UpdateTopicAction>> violations = this.updateTopicActionValidationService.validate(action); if (!violations.isEmpty()) { throw new ValidationFailedException(violations); } topic.setTitle(action.getTitle()); topic.setContent(action.getContent()); topic.setDateLastModified(new Date()); return this.topicRepository.save(topic); } } |
Как видно, класс TopicService содержит три члена, к которым может быть применён принцип инверсии зависимостей. Более того, к самому классу TopicService он тоже может быть применён с целью скрыть детали реализации бизнес-логики от контроллера, являющегося частью сервисного слоя.
Таким образом мы получим интерфейс:
1 2 3 4 |
public interface TopicService { Topic update(UpdateTopicAction action, int id, int userAccountId); } |
и реализующий его класс:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
public class DefaultTopicService implements TopicService { private final TopicRepository topicRepository; private final UpdateTopicActionValidationService updateTopicActionValidationService; private final TopicAccessCheckService topicAccessCheckService; @Override public Topic update(UpdateTopicAction action, int id, int userAccountId) { Topic topic = this.topicRepository.findOneById(id) .orElseThrow(EntityNotFoundException::new); if (!this.topicAccessCheckService.hasAccess(topic, userAccountId)) { throw new EntityActionForbiddenException(); } Set<ConstraintViolation<UpdateTopicAction>> violations = this.updateTopicActionValidationService.validate(action); if (!violations.isEmpty()) { throw new ValidationFailedException(violations); } topic.setTitle(action.getTitle()); topic.setContent(action.getContent()); topic.setDateLastModified(new Date()); return this.topicRepository.save(topic); } } |
Аналогичным образом нужно выделить интерфейсы из остальных классов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public interface TopicRepository { Optional<Topic> findOneById(int id); Topic save(Topic topic); } public class JpaEntityManagerTopicRepository implements TopicRepository { private final EntityManager entityManager; @Override public Optional<Topic> findOneById(int id) { return Optional.ofNullable(this.entityManager.find(Topic.class, id)); } @Override @Transactional public Topic save(Topic topic) { return this.entityManager.merge(topic); } } |
Сервис валидации:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public interface UpdateTopicActionValidationService { Set<ConstraintViolation<UpdateTopicAction>> validate(UpdateTopicAction action); } public class ValidationApiUpdateTopicActionValidationService implements UpdateTopicActionValidationService { private final Validator validator; @Override public Set<ConstraintViolation<UpdateTopicAction>> validate(UpdateTopicAction action) { return this.validator.validate(action); } } |
Сервис проверки прав доступа:
1 2 3 4 5 6 7 8 9 10 11 12 |
public interface TopicAccessCheckService { boolean hasAccess(Topic topic, int userAccountId); } public class SimpleTopicAccessCheckService { @Override public boolean hasAccess(Topic topic, int userAccountId) { return topic.getUserAccountId() == userAccountId; } } |
Поскольку классы могут обращаться друг к другу опосредованно через интерфейсы и абстрактные классы, механизм создания объектов тоже изменится. Для создания объектов потребуется применением таких шаблонов проектирования как «Фабрика» и «Фабричный метод», либо механизма внедрения зависимостей, что в нашем примере реализуется средствами Spring Framework.
Использование совместно с другими принципами SOLID
Принцип инверсии зависимостей используется совместно с «Принципом подстановки [Барбары] Лисков» (LSP), позволяя иметь несколько реализаций одних и тех же интерфейсов и абстрактных классов и использовать их в зависимости от ситуации.
Так же принцип инверсии зависимостей пересекается с «Принципом открытости/закрытости»: если происходит наследование от класса, являющегося конкретной реализацией какого-либо интерфейса или абстрактного класса, то это является нарушением принципа открытости/закрытости. То же самое можно сказать и о перекрытии реализаций методов.
Аналогично, наличие интерфейсов или абстрактных классов, являющихся «божественными объектами» приводит к нарушению принципа единственной ответственности, а одним из решений проблемы «божественных объектов» является принцип разделения интерфейсов.
Сторонние зависимости и XML
Класс Topic и UpdateTopicAction, используемые в данном примере, имеют внешние зависимости в виде аннотаций Java EE Persistence API и Java EE Bean Validation API. Эти аннотации являются интерфейсами, таким образом принцип инверсии зависимостей частично соблюдён.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
@Entity public class Topic { @Id @GeneratedValue private int id; private String title; @Column(columnDefinition = "TEXT") private String content; @NotNull @Temporal(TemporalType.TIMESTAMP) private Date dateCreated; @Temporal(TemporalType.TIMESTAMP) private Date dateLastModified; @NotNull private int userAccountId; } |
Но с другой стороны у проекта есть внешние зависимости, от которых хочется избавиться. У данной задачи есть два решения. Первое решение предполагает создание дополнительных классов, которые будут использовать указанные API, но использоваться только в конкретных реализациях TopicRepository и UpdateTopicActionValidationService. Для большего удобства предполагается использование шаблонов проектирования «Прокси», «Адаптер» и «Обёртка».
Второе решение — конфигурирование настроек взаимодействия со сторонними API при помощи XML-файлов, что позволяет изменять настройки без внесения изменений в классы, исключая таким образом необходимость в их повторной компиляции. И JPA и Bean Validation API поддерживают такую возможность. То же самое можно сказать про большинство наиболее распространённых фреймворков: Spring (Framework, Security, Integration и т.д.), Hibernate, Apache Camel и многих других.