Русский
Русский
English
Статистика
Реклама

Recovery mode Понятнее о S.O.L.I.D

Большинство разработчиков с разговорами о принципах архитектурного дизайна, да и принципах чистой архитектуры вообще, обычно сталкивается разве что на очередном собеседовании. А зря. Мне приходилось работать с командами, которые ничего не слышали о S.O.L.I.D, и команды эти пролетали по срокам разработки на многие месяцы. Другие же команды, которые следовали принципам дизайна и тратили очень много времени на буквоедство, соблюдение принципов чистой архитектуры, код-ревью и написание тестов, в результате значительно экономили время Заказчика, писали лёгкий, чистый, удобочитаемый код, и, самое главное, получали от этого кайф.

Сегодня мы поговорим о том, как следовать принципам S.O.L.I.D и получать от этого удовольствие.



Что такое S.O.L.I.D? Погуглите и получите 5 принципов, которые в 90% случаев описываются очень скупо. Скупость эта потом выливается в непонимание и долгие споры. Я же предлагаю вернуться к одному из признанных источников и хотя бы на время закрыть этот вопрос.

Источником принципов S.O.L.I.D принято считать книгу Роберта Мартина Чистая архитектура. Если у Вас есть время прочесть книгу, лучше отложите эту статью и почитайте книгу. Если времени у Вас нет, а завтра собес велком.

Итак, 5 принципов:

Single Responsibility Principle принцип единственной ответственности.
Open-Closed Principle принцип открытости/закрытости.
Liskov Substitution Principle принцип подстановки Барбары Лисков.
Interface Segregation Principle принцип разделения интерфейсов.
Dependency Inversion Principle принцип инверсии зависимости.


Разберём каждый из принципов. В примерах я буду использовать Java и местами Kotlin.

Single Responsibility Principle принцип единственной ответственности.


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

Сформулировать его можно так:

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

Вот тут становится непонятно, поэтому мы обратимся к признанному первоисточнику книге Роберта Мартина, у которого есть отличный пример на этот счёт. Воспользуемся им.

Предположим, что существует какая-то система, в которой ведётся учёт сотрудников. Сотрудники интересны как отделу кадров, так и бухгалтерии. Для их нужд, в числе прочих, в сервисе EmployeeService есть 2 метода:

fun calculateSalary(employeeId: Long): Double //рассчитывает зарплату сотрудникаfun calculateOvertimeHours(employeeId: Long): Double //рассчитывает сверхурочные часы

Методом расчёта зарплаты пользуется бухгалтерия. Методом расчёта сверхурочных часов пользуется отдел кадров.

Логично, что для расчёта зарплаты бухгалтерии может потребоваться учесть сверхурочные часы сотрудника. В таком случае, метод calculateSalary() может вызвать calculateOvertimeHours() и применить его результаты в своих формулах.

Окей, прошло полгода, и в отделе кадров решили поменять алгоритм расчёта сверхурочных. Предположим, раньше один сверхурочный час рассчитывается не по коэффициенту * 2, а стал по коэффициенту * 2,5. Разработчик получил задание, изменил формулу, проверил, что всё работает, и успокоился. Город засыпает, просыпается бухгалтерия. А у бухгалтерии ничего не поменялось, они считают зарплаты по тем же формулам, вот только в этой формуле теперь будут другие цифры, потому что calculateSalary() ходит в calculateOvertimeHours(), а там теперь по просьбе отдела кадров сверхурочные не * 2, а * 2,5. Упс

Теперь, если мы вернёмся к описанию принципа, всё станет намного понятнее.

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

Теперь, я надеюсь, стало понятнее.

Каким может быть решение проблемы выше?

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

Open-Closed Principle принцип открытости/закрытости.


Принцип гласит:

Программные сущности должны быть открыты для расширения и закрыты для изменения.

Возьмём пример из суровой реальности.

У нас есть приложение, и, предположим, довольно неплохое. В нём есть сервис, который пользуется внешним сервисом. Предположим, это сервис котировок ЦБ РФ. В простой реализации архитектура этого сервиса будет выглядеть следующим образом:



Вроде всё неплохо, но проект пора отдавать на интеграционное тестирование, и нам нужно отключить запросы в ЦБ. Как бы мы ни попытались решить эту проблему, нам будет необходимо изменить текущую реализацию FinancialQuotesRestService так, чтобы она не отправляла больше запросы. Но в чистой архитектуре такой подход недопустим, поскольку нарушается Принцип открытости-закрытости старый код не должен меняться.

Как решить проблему и сделать так, чтобы при добавлении новой реализации не трогать старый код?

Конечно же, через интерфейсы. Стоит нам разделить объявление функциональности и её реализацию, как эта проблема будет решена. Теперь мы можем реализовать интерфейс FinancialQuotesRestService сколько угодно раз; в нашем случае, это будет FinancialQuotesRestServiceImpl и FinancialQuotesRestServiceMock. На тестовом стенде приложение будет запускаться по Mock-профилю, на продакшене оно будет запускаться в обычном режиме.



Далее, если мы захотим добавить ещё одну реализацию, нам не придётся менять существующий код. Например, мы хотим добавить получение котировок не ЦБ, а в некоторых случаях Сбербанка. Нет ничего проще:



Мы добавили новую функциональность, но прежние две затронуты не были. Даже если команда разработчиков полностью обновится и приложение будут разрабатывать совсем другие программисты, им не придётся трогать старый код. Таким образом, достигается главная цель Принципа Открытости-Закрытости сделать систему легко расширяемой и обезопасить её от влияния изменений.

Liskov Substitution Principle принцип подстановки Барбары Лисков.


Принцип подстановки Барбары Лисков можно сформулировать так:

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

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

public interface FinancialQuotesRestService {    List<Quote> getQuotes();}

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

В нашем случае, контроллер будет выглядеть так:

@RestController("/quotes")@RequiredArgsConstructorpublic class FinancialQuotesController {        private final FinancialQuotesRestService service;        @GetMapping    public ResponseEntity<List<Quote>> getQuotes() {        return ResponseEntity.ok(service.getQuotes());    }}

Как мы видим, контроллер не знает, какая именно реализация будет реализована далее. Получит ли он котировки ЦБ, Сбербанка или просто моковые данные. Мы можем запустить приложение в любом режиме, и от этого работа контроллера не изменится.

В этом и есть смысл Принципа подстановки Барбары Лисков. Любой компонент системы должен работать с компонентами, от которых он зависит, через неизменяемый контракт.

Interface Segregation Principle принцип разделения интерфейсов.


Принцип можно сформулировать так:

Необходимо избегать зависимости от того, что не используется.

Например, мы решили реализовать CRUD-интерфейс, который будет поддерживать операции CRUD. И реализовать его во всех сервисах. Выглядеть интерфейс может так:

public interface CrudService<T> {        T create(T t);        T update(T t);        T get(Long id);        void delete(Long id);}

Окей, мы реализовали его в сервисе UserService:

@Service@RequiredArgsConstructorpublic class UserService implements CrudService<User> {    private UserRepository repository;        @Override    public User create(User user) {        return repository.save(user);    }    @Override    public User update(User user) {        return repository.update(user);    }    @Override    public User get(Long id) {        return repository.find(id);    }    @Override    public void delete(Long id) {        repository.delete(id);    }}

Далее, нам потребовалось реализовать класс PersonService, который мы тоже наследуем от CrudService. Но проблема в том, что сущности Person не удаляемые, и нам не нужна реализация метода delete(). Как быть? Можно сделать так, например:

    @Override    public void delete(Long id) {        //не реализуется    }

И это будет ужасно. Мало того, что мы имеем в классе мёртвый код, который выглядит, как живой, так его кто-то ещё может и вызвать, не зная, что удаление не произойдёт.

Что делать в такой ситуации?

Выделить реализацию CRUD без удаления в отдельный интерфейс. Например, так:

public interface CruService<T> {    T create(T t);    T update(T t);    T get(Long id);}

Да простит меня читатель за такое именование интерфейса, но для наглядности самое то.

@Service@RequiredArgsConstructorpublic class PersonService implements CruService<Person> {    private final PersonRepository repository;    @Override    public Person create(Person person) {        return repository.save(person);    }    @Override    public Person update(Person person) {        return repository.update(person);    }    @Override    public Person get(Long id) {        return repository.find(id);    }}

Теперь сущность Person нельзя удалить! PersonService реализует интерфейс, в котором не объявлено ничего лишнего. В этом и есть соблюдение Принципа разделения интерфейсов.

Dependency Inversion Principle принцип инверсии зависимости.


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

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

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

У этого принципа, впрочем, есть исключения. Можно строить зависимости от класса, если он предельно стабилен. Например, класс String. Вероятность изменения чего-либо в String всё-таки очень мала (несмотря на то, что я сам описывал добавления в функциональность в String в Java 11 в одной из предыдущих статей, хехе). В случае со стабильным классом String, мы можем себе позволить вызывать его напрямую. Как и в случае с другими стабильными классами.

В общем, Принцип инверсии зависимости можно постулировать так:

Абстракции стабильны. Реализации нестабильны. Строить зависимости необходимо на основе стабильных компонентов. Стройте зависимости от абстракций. Не стройте их от реализаций.

Заключение.


Мы, разработчики, не только пишем код (и это лучшие моменты в нашей работе). Мы вынуждены его поддерживать. Чтобы эти моменты нашей работы не стали худшими, используйте принципы S.O.L.I.D. Использование принципов S.O.L.I.D, по моему опыту, окупается в течение одного спринта. Потом сами себе скажете спасибо.
Источник: habr.com
К списку статей
Опубликовано: 29.06.2020 08:16:31
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Программирование

Java

Проектирование и рефакторинг

It-стандарты

Чистая архитектура

S.o.l.i.d

Категории

Последние комментарии

© 2006-2020, personeltest.ru