Источник: Nuances of Programming
Не раз и даже не два случалось мне возиться с кодом ради исправления одной крошечной ошибки или расширения функциональности, пока, наконец, в моей жизни не появились принципы SOLID.
С их помощью я стала писать небольшие фрагменты кода с одной ответственностью в каждом и без лишних инструкций if-else. И теперь я счастливый обладатель слабо связанного кода, который к тому же мне удается сохранять простым , поддерживаемым , читаемым , тестируемым и легко расширяемым .
SOLID — это принципы объектно-ориентированного программирования, сформулированные Робертом Сесилом Мартином, известным как Дядя Боб, в его работе “Design Principles and Design Patterns” (“Принципы и шаблоны проектирования”). Однако сам термин появился позднее благодаря Майклу Фэзерсу.
Итак, знакомьтесь:
- Принцип единственной ответственности.
- Принцип открытости/закрытости.
- Принцип подстановки Лисков.
- Принцип разделения интерфейса.
- Принцип инверсии зависимостей.
Давайте рассмотрим, в чем суть каждого из них.
Принцип единственной ответственности (SRP)
У класса должна быть одна и только одна причина для изменения.
Каждая функция или класс призваны выполнять только одну задачу.
Позднее Дядя Боб напишет о том, что этот принцип касается и людей. И действительно, суть его становится намного понятнее, если вместо классов и функций провести аналогию с жизнью. Например, у каждого члена семьи есть свои обязанности, которые он выполняет лучше всего: кто-то отвечает за стирку, а другой готовит еду.
Ниже представлен класс User , предназначенный для хранения данных пользователя и выполнения его действий, например Registration. Функция Registration выполняет несколько задач: обеспечивает соединение с базой данных (БД), сохраняет данные, отправляет подтверждение по e-mail и при необходимости записывает лог события.
При таком способе написания кода каждое изменение в движке БД, сервере e-mail или логике логирования будет влиять на класс. Эти изменения не входят в число обязанностей User . Его задача — убедиться в том, что данные пользователя сохранены в БД. Он не должен следить за тем, как они сохраняются, какая строка отвечает за подключение или какой тип движка БД используется. Перед нами явный пример нарушения SRP.
Для решения этой проблемы делегируем ответственность за действия БД классу DBUtil и добавим службы обмена сообщениями и логирования. После разделения ответственности класс User будет более читаемым, и изменения ему грозят только в случае модификации данных или логики регистрации.
Принцип открытости/закрытости (OCP)
Модуль должен быть открыт для расширения, но закрыт для модификации.
Это значит, что мы должны избегать изменения существующего кода, добавляя новую функциональность посредством нового класса.
В классе MessagingService с помощью инструкции if-else и обработки всей логики обмена сообщениями мы проверяем, какой требуется тип сообщения о доставке. В этом случае при добавлении новых типов доставки или изменении логики текущего типа нам придется просканировать и протестировать весь код.
Во избежание нарушения OCP добавим интерфейс IMessagingService , т.е. класс для каждого типа сообщения о доставке, а также класс MessagingFactory для реализации фабричного шаблона проектирования.
Для отправки сообщения применяется класс Messaging factory , который возвращает соответствующий конкретный объект.
MessegingFactory messagingFactory = new MessagingFactory();
IMessagingService messagingService = messagingFactory.InitializeMessege(deliveryType);
messagingService.Send(content);
Теперь добавление нового типа доставки никак не меняет наши службы. Единственный и известным нам участок кода, подверженный изменениям, — это messagingFactory . Соблюдая принцип OCP и избегая модификаций, мы сможем сохранить код стабильным, поддерживаемым, легко расширяемым, не ломая при этом текущий код.
Принцип подстановки Лисков (LSP)
Этот принцип был введен Барбарой Лисков.
Если П является подтипом Т, то объекты типа Т, присутствующие в программе, можно заменить объектами типа П без модификации установленных свойств этой программы.
или так:
Объекты должны быть заменяемы экземплярами своих подтипов без изменения работы программы.
Необходимо, чтобы каждый наследующий класс мог заменить родительский без нарушения кода.
Суть LSP часто поясняется с помощью утиного теста : “Если нечто выглядит и крякает как утка, но при этом работает на батарейках, то, вероятно, проблема в неверной абстракции”. При попытке заменить настоящую утку (duck) ее моделью она явно не полетит. Так что модель класса duck не сможет реализовать класс duck .
Для воплощения LSP на практике класс-потомок должен соблюдать несколько правил, чтобы оставить в неизменном виде поведение и свойства своего родителя. Например, “правило сигнатуры” обязывает придерживаться сигнатуры метода родительского класса, сохраняя число аргументов и их типы. То же требование распространяется на возвращаемые типы и набор исключений.
Следующий пример демонстрирует нарушение LSP в результате переопределения исключений метода и возвращения их набора, отличного от родительского класса.
У нашей системы есть 2 типа пользователей: зарегистрированные и гости, оба из которых реализуют интерфейс IUser . Одно из действий зарегистрированных пользователей — UpdatePassword (обновление пароля). Класс Guest использует тот же интерфейс, а следовательно вынужден реализовывать функциональность UpdatePassword . В результате выбрасывается исключение, поскольку у гостей нет пароля для обновления.
Напишем 2 интерфейса для исправления этой ошибки:
- IUser с общей функциональностью для зарегистрированных пользователей и гостей;
- IRegisteredUser реализует IUser и расширяет функциональность зарегистрированного пользователя.
Принцип разделения интерфейса (ISP)
Лучше несколько отдельных клиентских интерфейсов, чем один общий интерфейс.
Иначе говоря, “клиенты не должны зависеть от методов, которые они не используют”. Этот принцип очень близок по смыслу SRP. Однако отличие состоит в том, что здесь мы исходим из позиции клиента. Если он применяет одну функцию, то не следует привязывать его к интерфейсу, включающему больше методов, чем ему требуется. Вот что пишет по этому поводу Дядя Боб:
Чтобы не перегружать класс с несколькими клиентами всеми необходимыми для них методами, следует создать отдельные интерфейсы для каждого клиента и привязать их все к этому классу.
Допустим, необходимо отправить уведомления всем пользователям. С этой целью создаем класс Notification с одной функцией Send , которая побуждает объект IUser получить e-mail пользователя с помощью свойства GetEmailNotification.
Класс User реализует набор функций, несоответствующих классу Notification , что говорит о нарушении принципа ISP.
Для решения этой проблемы с помощью GetEmailNotification определяем новый интерфейс INotifiable , и в службе уведомлений вместо IUser получаем INotifiable .
Теперь класс GuestUser реализует интерфейсы IUser и INotifiable .
В этом случае класс Notification зависит не от большого интерфейса с кучей лишних функций, а от маленького и с одной. Более того, если в перспективе потребуется применить службу уведомлений для других типов, мы легко сможем это сделать без реализации всей функциональности IUser .
Принцип инверсии зависимостей (DIP)
Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те, и другие должны зависеть от абстракций.
Класс User реализует функцию Registration , которая в качестве аргумента получает класс SQLDBUtil для управления операциями с данными в БД. Очевидно, что любые изменения в ее движке повлияют на Registration . Например, задумай мы перейти от SQL к MongoDB, нам бы потребовалось соответствующим образом модифицировать и эту функцию. А с учетом потенциального наличия многочисленных классов, использующих SQLDBUtil , масштабы работы будут невероятно велики.
Рассмотрим пример с нарушением принципа DIP:
Чтобы не нарушать принцип, объявим новый интерфейс IDBUtil , который реализуется классом SQLDBUtil , изменяющим функцию Registration для получения IDBUtil .
Обратите внимание, как класс User вместо конкретного SQLDBUtil получает IDBUtil .
В таком случае при переходе с движка SQL на Mongo нужно лишь изменить фрагмент кода, создающего не класс User , а экземпляр БД.
IDDBUtil db = new MongoDbUtil();
User.Registration(db,DeliveryType.sms);
Теперь класс User зависит от абстракции IDBUtil , а не от нижнеуровнего класса SQLDBUtil .
Заключение
Напомню, что SOLID являются принципами, а не правилами. Это инструмент, позволяющий сделать код читаемым, поддерживаемым и расширяемым. Используйте его разумно.
Читайте также:
Перевод статьи Liat Kompas : Coding the SOLID Principles