Принципы SOLID на примере ruby♦️

Alexander Svetly
6 min readAug 11, 2017

--

В данной статье будут описаны пять принципов SOLID, а также их практическое применение в языке ruby.

SOLID это свод пяти основных принципов ООП, введённый Майклом Фэзерсом в начале нулевых. Эти принципы — часть общей стратегии гибкой и адаптивной разработки, их соблюдение облегчает расширение и поддержку проекта.

Таким образом, акроним SOLID содержит в себе пять принципов ООП:

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

Рассмотрим каждый из них подробнее.

Single Responsibility Principle

Wenger doesn’t care about SRP

A class should have only one reason to change.

Robert C. Martin

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

Сложный класс должен быть разбит на несколько простых составляющих, отвечающих за определенный аспект поведения. Это упрощает расширение системы, редактирование классов.

Anti-SRP — Принцип размытой ответственности. Чрезмерная любовь к SRP ведет к обилию мелких классов/методов и размазыванию логики между ними.

Теперь в качестве примера рассмотрим класс ReportMailer.

На первый взгляд он выглядит довольно-таки просто и лаконично. Но, взглянув повнимательнее, видно, что класс решает сразу две задачи: генерация отчета и его отправка. Допустим, мы хотим отправить письмо не почтой, а смской, или использовать другой генератор отчета. Для этого нам потребуется менять логику класса, и если в рамках примера это легко, то в больших проектах это может вылиться в большую проблему. Следуя принципу единственной ответственности, разделим класс ReportMailer на два класса: первый — ответственный за генерацию отчета, второй — за отправку.

Теперь у нас есть два класса, которые отвечают за решение одной конкретной задачи. В случае, если мы хотим изменить генерацию отчета, достаточно внести изменения в класс генератора, не касаясь класса отправки.

Open Closed Principle

Gtfo for changes, welcome for extension

Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.

Bertrand Meyer

Каждый класс должен быть закрыт для изменения и открыт для расширения.

Очевидные решения этой дилеммы — наследование и создание интерфейсов. Например, для изменения поведения некоторого класса нужно создать класс, который наследует поведение исходного, и переопределить нужные методы. Соблюдение этого принципа позволяет строить гибкие системы, которые способны подстраиваться под конкретные задачи без изменения исходного кода.

Anti-OCP Принцип фабрики-фабрик: Чрезмерная любовь к OCP ведет к переусложненным решениям с чрезмерным числом уровней абстракции.

Возвращаясь к нашему ReportSender, рассмотрим его относительно принципа открытости/закрытости. Допустим, заказчик предъявил новое требование к отправке отчета: он хочет, чтобы это делалось не только посредством email, но и по sms. Попробуем добавить этот функционал и посмотреть, что из этого получится.

Для того, чтобы добавить новый способ отправки, клиенту потребуется изменить код метода send_report и дописать в него очередное условие. Следуя принципу OCP, отрефакторим пример так, чтобы клиенту не пришлось изменять существующий код для расширения функционала.

Теперь, для того, чтобы добавить новый способ доставки, не нужно редактировать существующий код, достаточно написать наследника базового класса Sender и реализовать в нем метод отправки.

Liskov Substitution Principle

OMG!!!

Subtype Requirement: Let Φ(𝒙) be a property provable about objects 𝑥 of type T. Then Φ(𝒚) should be true for objects 𝒚 of type S where S is a subtype of T.

Barbara Liskov and Jeannette Wing

Принцип подстановки Барбары Лисков тесно связан с принципом выше, даже, можно сказать, является его вариацией. Если S является подтипом T, тогда объекты типа T в программе могут быть замещены объектами типа S без каких-либо изменений желательных свойств этой программы. Или, говоря иными словами, использование класса, разработанного на основе базового путем расширения (наследующего базовый), там, где используется базовый, не должно нарушать работу системы. То есть поведение наследуемых классов должно быть ожидаемым для кода, использующего переменные базового класса.

Этот принцип является важнейшим критерием для оценки качества принимаемых решений при построении иерархий наследования.

Anti-LSP — Принцип непонятного наследования. Данный анти-принцип проявляется либо в чрезмерном количестве наследования, либо в его полном отсутствии, в зависимости от опыта и взглядов местного главного архитектора.

Данный принцип также очень хорошо описывают Саттер и Александреску в своём руководстве по использованию C++:

подкласс не должен требовать от вызывающего кода больше, чем базовый класс, и не должен предоставлять вызывающему коду меньше, чем базовый класс

Рассмотрим сначала пример, удовлетворяющий принципу LSP.

Классы Cat и Dog — производные от Animal. Функция animal_info (условно, потому что Duck Typing) работает с объектами класса Animal и ожидает от них определенное поведение. Классы Cat и Dog реализуют метод talk, не нарушая общего интерфейса, потому могут быть использованы в функции animal_info.

В языках программирования со строгой типизацией нарушить этот принцип труднее ввиду того, что типы входных параметров и возвращаемых значений строго определены. В ruby мы самы за это отвечаем.

Для примера, не удовлетворяющего принципу, изменим тип возвращаемого значения метода talk класса Dog на массив строк.

animal_info ожидает, что метод talk возвратит ему строку, но не тут-то было.

Interface Segregation Principle

Think twice before you’ll answer

Clients should not be forced to depend upon interfaces that they do not use.

Robert C. Martin

Клиенты не должны зависеть от методов, которые они не используют. Иными словами, клиенты не должны реализовывать методы, которые им не нужны.

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

Anti-ISP — Принцип тысячи интерфейсов. Интерфейсы классов разбиваются на слишком большое число составляющих, что делает их неудобными для использования всеми клиентам

К счастью или сожалению, в ruby нет интерфейсов. Можно, конечно, реализовать что-то подобное через module и include. Но я возьму пример с классами.

Интерфейс класса Car частично используется как Driver, так и Mechanic. Мы можем улучшить наш ситуацию следующим образом:

Теперь у нас два интерфейса, которые полностью используются двумя классами. Не самый крутой пример, но что поделать ¯\_(ツ)_/¯.

Dependency Inversion Principle

Try to find another pic for this one!

Определение принципа состоит из двух формулировок:

  • Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Другими словами, зависимости должны строится относительно абстракций (интерфейсов), а не деталей.

Интересно, что сам Robert C. Martin не считает принцип самостоятельным, а результатом строгого выполнения двух предыдущих: принцип открытости/закрытости и подстановки Барбары Лисков. Код, который следует принципам OCP и LSP, должен быть читаемым и содержать четко разделенные абстракции. Он также должен быть расширяемым, а дочерние классы легко заменяемыми другими экземплярами базового класса.

Anti-DIP — Принцип инверсии сознания или DI-головного мозга. Интерфейсы выделяются для каждого класса и пачками передаются через конструкторы. Понять, где находится логика становится практически невозможно.

Вернемся к нашему ReportSender, немного его изменив для наглядности несоблюдения DIP.

ReportSender напрямую зависит от двух классов: SmsSender и EmailSender, то есть зависимости в нем построены относительно деталей, а не абстракций. Исправим это.

Теперь мы можем использовать любой Sender, удовлетворяющий интерфейсу. В вышеприведенном коде ReportSender — объект высокого уровня — не зависит непосредственно от реализации объектов низкого уровня — способов доставки отчета. Кроме того, все модули зависят от абстракции. Наша высокоуровневая функциональность отделена от всех деталей низкого уровня, поэтому мы можем легко изменить логику деталей без последствий для всей системы.

Бестолковое использование этих принципов не гарантирует вам построение идеального object-oriented design. Нужно понимать: какой принцип зачем нужен, где и как его применять. Последствия чрезмерного потребления изложены для каждого принципа. Но, безусловно, в умелых руках SOLID поможет вам создать архитектуру вашей мечты. 🙌

--

--

Alexander Svetly

Young passionate web developer from Russia, write mostly for practicing English ⚫️ https://asvetly.io