Эта аббревиатура является самой известной (после ООП), она говорит нам о 5 принципах “хорошего дизайна ПО”. При этом является самой бесполезной, потому что однозначно никто не может обозначить критерий для того или иного принципа. Часто на форумах приходится видеть споры о том у кого программа SOLIDнее.

Про SOLID пишут часто и много, но большинство пишущих не читали или мало читали первоисточник (признайтесь, вы читали?). Автор аббревиатуры SOLID - Роберт Мартин, он придумал саму аббревиатуру и описал 5 принципов. На самом деле он описал больше, но звучных буквосочетаний не придумал, многие вещи остались забытыми. Заметьте что Мартин именно описал принципы, он не является их автором. Зачастую объяснения на пальцах на примерах сложно перенести в свой код.



Who is mister SOLID?

Аббревиатура (длинное и неприятное слово) SOLID состоит из:

  • Single Responsibility Principle (SRP) – принцип единственной отвественности
  • Open\Close Principle (OCP) – принцип открытости\закрытости
  • Liskov Substitution Principle (LSP) – принцип подстановки Лисков (это фамилия)
  • Interface Segregation Principle (ISP) – принцип изоляции интерфейсов
  • Dependency Inversion Principle (DIP) – принцип инверсии зависимостей

Далее буду пользоваться только акронимами, указанными в скобках.

Критика

Как связаны между собой вышеуказанные принципы никто не говорит, какой из них важнее, а какой нет – тоже никто не в курсе.

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

SRP

На каждый объект должна быть возложена одна единственная обязанность.

Первое же определение взрывает мозг. Что такое обязанность? Мартин определяет обязанность как причину изменения. Стало понятнее? Мне не очень.

OCP

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

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

LSP

Объекты в программе могут быть заменены их наследниками без изменения свойств программы.

Тут немного лучше, потому что принцип LSP предельно формален, про него подробнее напишу ниже.

ISP

Много специализированных интерфейсов лучше, чем один универсальный.

Это Мартин  решил поиграть в Капитана Очевидность.

DIP

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

Это просто бред, написанный для красного словца. Если все зависит от абстракций, то как понять где модули верхнего уровня, а где нижнего?

У Мартина, кстати, не такое определение.

Терминология

Чтобы навести формализм во всей этой кухне необходимо ввести определения. большинство горячих споров происходят как раз из-за разных определений.

Итак определения

Интерфейс – некоторый набор функций (их параметров и возвращаемых значений), с помощью которого одна часть программы обращается к другой.

Контракт – надмножество интерфейса, описывающее также поведение функций, ограничения на входные\выходные параметры, инварианты, последовательность вызовов итд. Контракт обычно присутствует в программе неявно, но есть средства, позволяющие часть его описать явно. Например навороченные системы типов как в haskell, внешние средства вроде Code Contracts в .NET. Даже если контракт не определен явно, то в программе он неявно присутствует.

Абстрактный интерфейс – некоторый тип данных,состоящий из набора методов без реализации. Всегда соответствует некоторому интерфейсу.

Класс – некоторый тип данных, который состоит из данных и методов. Классы могут наследоваться один от другого, могут реализовывать несколько абстрактных интерфейсов. Класс всегда имеет некоторый интерфейс и контракт, зачастую больше одного. 

Модуль – некоторое множество функций и, возможно, данных, объединенные для решения задачи. Модулем могут быть как классы в ОО-языках, так и другие средства группирования кода. Модуль также имеет интерфейс и контракт, скорее всего один.

Принципы

Для начала стоит сказать что многие принципы не являются прерогативой ООП, а применимы для широкого класса парадигм.

Начнем с SRP

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

Начинать стоит с простого: если можно отделить – надо отделить. Необходимо чтобы внутри одного модуля весь функционал был связан между собой (high cohesion, такое словосочетание вы наверное слышали). Принцип работает только в одну сторону: если подмножество A некоторого модуля не имеет ссылок на подмножество B, то это не значит что B не имеет ссылок на A, причем скорее всего именно B будет ссылаться на A.

Следуя данному принципу весь код будет распадаться на множество маленьких модулей, многие из которых выродятся до одной функции. Это нормально, даже хорошо. Функции потом можно группировать в модули по логической связности, добиваясь все того же high cohesion.

Модули будут зависеть друг от друга, они будут выстраиваться в ориентированный граф. Расположив зависимости сверху вниз можно условно разделить модули на верхне- и нижне- уровневые. На самом “пространство”, в котором мы пытаемся упорядочить модули, многомерно. Придумать одно отношение порядка для всего этого пространства невозможно. Но для двух модулей, между которыми есть путь, можно сказать какой из них верхнеуровневый, а какой нижнеуровневый.

Зависимость между модулями может быть:

  1. Ссылочной, когда модуль A непосредственно обращается к модулю B, его функциям и данным.
  2. Наследованием, когда модуль A является частным случаем B.
  3. Зависимостью по состоянию, когда два модуля оперируют одним внешним состоянием (глобальные переменные, файлы, БД) и влияют на работу друг друга. Это плохая зависимость, от нее надо избавляться.
  4. Зависимостью по времени. Когда для работы требуется одного модуля требуется вызов функций другого модуля в нужные моменты. Это самый плохой вид зависимости, он него надо избавляться однозначно всеми возможными способами.
 
Далее ISP

Как я уже писал выше, если следовать SRP, то программа распадется на множество мелких модулей. ISP говорит нам что это хорошо. Далее когда занимаетесь объединением отдельных функций в некоторые модули по смысловой связности, то учитывайте также ISP, не создавая модулей с очень жирным интерфейсом.

 

Перейдем к DIP

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

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

 

Теперь рассмотрим LSP

Принцип подстановки Барбары Лисков сформулирован предельно формально и говорит вообще о любых типа, а не только классах ООП.

Если A является подтипом B, то в любом месте программы (функции), где требуется объект типа B, можно подставить объект типа A и поведение программы (функции) при этом не изменится.

В ООП если класс A унаследован от класса B, равно как класс A реализует абстрактный интерфейс B, то A является подтипом B, а B является супертипом A.  Кроме того некоторые языки программирования поддерживают вариантность типов, для них тоже надо применять LSP, но там помогает компилятор.

Принцип LSP надо использовать максимально широко, надо ориентироваться на весь контракт, в том числе пред- и пост-условия, а также то что не описано в самой программе.

Для контрактов правила простые:

  1. Предусловия в подтипе должны быть не сильнее, чем в супертипе.
  2. Постусловия в подтипе должны быть не слабее, чем в супертипе.
  3. Перечень выбрасываемых исключений в подтипе должен быть не шире, чем в супертипе. (хотя часто на это не обращают внимания)
  4. Остальные детали контракта, которые нельзя проверить статически, должны проверяться тестами, и подтипы должны проходить все тесты, которые проходят супертипы.

 

Напоследок OCP

Хорошо понимая LSP легко сообразить о чем говорит OCP.

Все не-sealed классы должны быть спроектированы таким образом, чтобы наследники не могли нарушить LSP.

Ну вот и все.

Что не вошло в пятерку

Принцип бритвы Оккма

Не плодите сущности без нужды

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

Do not repeat yourself (DRY)

Одинаковый или похожий код должен быть вынесен в отдельный модуль и использован другими. Для этого сильно помогают инструменты вроде IoC-контейнеров с возможностью AOP.

Command-Query Separation (CQS)

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

Пожалуйста не путайте этот принцип с модным нынче CQRS, который является гипертрофированным CQS для непонятно каких целей.

Keep it simple, stupid! (KISS)

Делайте все настолько простыми, насколько можно, но не проще.

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

Заключение

Самыми важными принципами в начале проектирования частей программы являются KISS и SRP. После того как появился некоторый граф модулей надо сразу применять Бритву Оккама, LSP, ISP. Когда начинаете писать код, то применяйте DRY, DIP, CQS и OCP.

Ну вот теперь совсем все.

Теги : архитектура, SOLID, ООП