Пятым и последним принципом в списке принципов SOLID является принцип инверсии зависимости (Dependency Inversion Principle; DIP), который Роберт Мартин в книге «Чистая архитектура» формулирует следующим образом: «Код, реализующий высокоуровневую политику, не должен зависеть от кода, реализующего низкоуровневые детали. Напротив, детали должны зависеть от политики«.
Читать далее Принцип инверсии зависимости — SOLID в деталяхМетка: SOLID
SOLID в деталях: Принцип открытости/закрытости (Видео)
В серии роликов «SOLID в деталях» я постараюсь подробно рассказать о принципах SOLID, а так же продемонстрировать их практическое применение.
Второй ролик посвящён принципу открытости/закрытости.
SOLID в деталях: Принцип подстановки Лисков
Третьим принципом в списке SOLID является Принцип подстановки Лисков (Liskov Substitution Principle; LSP), который из всех принципов имеет самую сложную формулировку, звучащую следующим образом:
Читать далее SOLID в деталях: Принцип подстановки ЛисковПусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.
Барбара Лисков
SOLID в деталях: Принцип единственной ответственности (Видео)
В серии роликов «SOLID в деталях» я постараюсь подробно рассказать о принципах SOLID, а так же продемонстрировать их практическое применение.
В первом ролике речь пойдёт о принципе единственной ответственности (Single Responsibility Principle; SRP), его трактовках и их практическом применении.
Видео можно посмотреть на следующих платформах:
SOLID в деталях: принцип открытости/закрытости
Принцип открытости/закрытости (Open-Closed Principle; OCP), на мой взгляд, является главным принципом в списке принципов SOLID, в то время как все остальные в той или иной мере обеспечивают его соблюдение. Этот принцип сформулировал Бертран Мейер в своей книге «Object-Oriented Software Construction» в 1988 году следующим образом: «Программные сущности (классы, модули, функции и т.д.) должны быть открыты для расширения, но закрыты для изменения» (Software entities (classes, modules, functions, etc) should be open for extension, but closed for modification). Иными словами, должна иметься возможность изменять поведение программных сущностей без их изменения, как написал Роберт Мартин в книге «Чистая архитектура».
Читать далее SOLID в деталях: принцип открытости/закрытостиSOLID в деталях: Принцип единственной ответственности
Я уже писал ранее в своём блоге о принципах SOLID, но это было практически пять лет назад, с тех пор утекло немало воды, а я всё это время на ежедневной основе набирался опыта их применения в проектах разных масштабов от мала до велика. И, когда в очередной раз прочитал свои старые заметки, я понял, что опыт внёс определённые корректировки в понимание принципов SOLID, а это хороший повод снова по ним пройтись. Эта статья, а так же последующие предполагают, что мои ранее написанные заметки по принципам SOLID можно не брать в расчёт.
Читать далее SOLID в деталях: Принцип единственной ответственностиМногоуровневая архитектура в проекте на Java (Часть 2)
Сервис, реализующий бизнес-логику работы с заметками, который я описал в своей предыдущей статье, достаточно простой. В реальной жизни требуются различные проверки при выполнении CRUD-операций: проверка прав доступа, валидация полученных данных и т.д.
Очевидно, что в нашем сервисе нужна валидация получаемых данных при создании и редактировании заметок, а также проверка прав доступа к заметкам. Выделение валидации и проверки прав доступа в отдельные компоненты фактически является разделением слоя бизнес-логики на дополнительные подслои.
Читать далее Многоуровневая архитектура в проекте на Java (Часть 2)
SOLID на практике — Принцип подстановки Барбары Лисков
Принцип подстановки [Барбары] Лисков (Liskov Substitution Principle — LSP, буква L в аббревиатуре SOLID), сформулирован Барбарой Лисков в 1987 году и звучит следующим образом:
Пусть q(x) является свойством, верным относительно объектов x некоторого типа T. Тогда q(y) также должно быть верным для объектов y типа S, где S является подтипом типа T.
Упрощенное описание этого принципа предложил Роберт Мартин:
Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
Иными словами, поведение реализующих и наследующих классов не должно противоречить поведению базовых типов.
Читать далее SOLID на практике — Принцип подстановки Барбары Лисков
SOLID на практике — принцип инверсии зависимостей
Принцип инверсии зависимостей (Dependency Inversion Principle — DIP, буква D в аббревиатуре SOLID), описанный Робертом Мартином, состоит из двух постулатов:
- Высокоуровневые модули не должны зависеть от низкоуровневых; и те и другие должны зависеть от абстракций
- Абстракции не должны зависеть от деталей, детали должны зависеть от абстракций
Инверсия зависимостей заключается в том, что модули разных уровней зависят не друг от друга, а от абстракций. В общих чертах принцип инверсии зависимостей сводится к следующему набору простых правил:
- Взаимодействие между классами должно быть реализовано через интерфейсы или абстрактные классы
- Типами всех членов классов должны быть интерфейсы или абстрактные классы
- Классы, являющиеся конечными реализациями не должны расширяться (или должны быть финальными)
- Аналогично методы не должны перекрываться при наследовании (или быть финальными)
Читать далее SOLID на практике — принцип инверсии зависимостей
SOLID на практике — принцип единственной ответственности
Принцип единственной ответственности (Single Responsibility Principle — SRP, буква S в аббревиатуре 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 |
public class TopicService { private final EntityManager entityManager; @Transactional public Topic update(int id, String title, String content, int userAccountId) { if (title == null || title.length() > 255) { throw new IllegalArgumentException("Title is too long"); } if (content.length() > 65535) { throw new IllegalArgumentException("Content is too long"); } return Optional.ofNullable(this.entityManager.find(Topic.class, id)) .filter(topic -> topic.getUserAccountId() == userAccountId) .map(topic -> { topic.setContent(content); topic.setTitle(title); topic.setDateLastModified(new Date()); return this.entityManager.merge(topic); }) .orElseThrow(EntityNotFoundException::new); } } |
В примере приведён метод сохранения изменений в записи типа Topic. Однако этот метод выполняет слишком много действий: валидацию полученных данных, поиск существующего объекта типа Topic, проверку прав доступа пользователя на этот объект, применение и сохранение изменений. И все эти действия могут меняться независимо друг от друга.
Следуя логике принципа единственной ответственности, нам следует выделить работу с источником данных, валидацию и проверку прав доступа, как минимум, в отдельные методы, а наиболее правильно — в отдельные классы.
Выделение логики в отдельные методы
В первом варианте мы разделим логику между методами класса TopicService. Кроме этого данные, получаемые методом update, логично будет инкапсулировать в одном классе:
1 2 3 4 5 6 7 8 9 |
public class UpdateTopicAction { @NotBlank @Size(max = 255) private String title; @Size(max = 65535) private String content; } |
В результате выделения логики валидации, работы с источником данных и проверки прав доступа в отдельные методы должен получиться примерно такой код:
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 31 32 33 34 35 36 37 38 39 40 41 42 |
public class TopicService { private final EntityManager entityManager; private final Validator validator; private void validate(UpdateTopicAction action) { Set<ConstraintViolation<UpdateTopicAction>> violations = this.validator.validate(action); if (!violations.isEmpty()) { throw new IllegalArgumentException("Invalid data passed"); } } private Topic findOneById(int id) { return Optional.ofNullable(this.entityManager.find(Topic.class, id)) .orElseThrow(EntityNotFoundException::new); } @Transactional protected Topic save(Topic topic) { return this.entityManager.merge(topic); } private void checkAccess(Topic topic, int userAccountId) { if (topic.getUserAccountId() != userAccountId) { throw new EntityActionForbiddenException(); } } public Topic update(UpdateTopicAction action, int id, int userAccountId) { this.validate(action); Topic topic = this.findOneById(id); this.checkAccess(topic, userAccountId); topic.setContent(action.getContent()); topic.setTitle(action.getContent()); topic.setDateLastModified(new Date()); return this.save(topic); } } |
Теперь этот код на уровне методов соответствует принципу единственной ответственности, чего не скажешь о всём классе TopicService. Не смотря на то, что методы findOneById, save, checkAccess и validate скрыты от внешнего наблюдателя, класс всё равно выполняет много действий.
Однако даже при этом код уже более удобен для понимания, а так же пригоден для повторного использования.
Выделение логики в отдельные классы
При выделении логики валидации, проверки прав и работы с источниками данных в отдельные классы, код приложения станет ещё удобнее и понятнее.
Таким образом мы получим класс, работающий с источником данных:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class TopicRepository { private final EntityManager entityManager; public Optional<Topic> findOneById(int id) { return Optional.ofNullable(this.entityManager.find(Topic.class, id)); } @Transactional public Topic save(Topic topic) { return this.entityManager.merge(topic); } } |
Класс, выполняющий валидацию UpdateTopicAction:
1 2 3 4 5 6 7 8 |
public class UpdateTopicActionValidationService { private final Validator validator; public Set<ConstraintViolation<UpdateTopicAction>> validate(UpdateTopicAction action) { return this.validator.validate(action); } } |
Класс, проверяющий доступ пользователя к объекту Topic:
1 2 3 4 5 6 |
public class TopicAccessCheckService { public boolean hasAccess(Topic topic, int userAccountId) { return topic.getUserAccountId() == 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 |
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); } } |
Таким образом мы на практике применили принцип единой ответственности.