Принципы SOLID в картинках

Автор оригинала: Ugonna Thelma
  • Перевод


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

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

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

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

Ну, приступим.

Принципы SOLID



S – Single Responsibility (Принцип единственной ответственности)


Каждый класс должен отвечать только за одну операцию.



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

Назначение

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

O — Open-Closed (Принцип открытости-закрытости)


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



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

Назначение

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

L — Liskov Substitution (Принцип подстановки Барбары Лисков)


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


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

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

Необходимо, чтобы класс-потомок был способен обрабатывать те же запросы, что и родитель, и выдавать тот же результат. Или же результат может отличаться, но при этом относиться к тому же типу. На картинке это показано так: класс-родитель подаёт кофе (в любых видах), значит, для класса-потомка приемлемо подавать капучино (разновидность кофе), но неприемлемо подавать воду.

Если класс-потомок не удовлетворяет этим требованиям, значит, он слишком сильно отличается от родителя и нарушает принцип.

Назначение

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

I — Interface Segregation (Принцип разделения интерфейсов)


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



Когда классу приходится производить действия, не несущие никакой реальной пользы, это выливается в пустую трату ресурса, а в случае, если класс выполнять эти действия не способен, ведёт к возникновению багов.

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

Назначение

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

D — Dependency Inversion (Принцип инверсии зависимостей)


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


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

Модули (или классы) верхнего уровня = классы, которые выполняют операцию при помощи инструмента
Модули (или классы) нижнего уровня = инструменты, которые нужны для выполнения операций
Абстракции – представляют интерфейс, соединяющий два класса
Детали = специфические характеристики работы инструмента

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

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

Назначение

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

Обобщая сказанное


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

Комментарии 22

    +8
    Очередная попытка объяснить принципы SOLID на пальцах. Мне кажется что самый простой способ это сделать — это показать примеры кода «ка не надо делать» и «как надо делать».
      +1
      И на каком же языке показывать? Имхо, абстракции самое то. Вы, как разработчик не командами же мыслите.
        +4
        На псеводокоде можно. Мне бы подошел любой С-подобный, думаю большинство разработчиков также найдут его понятым. Я в целом о подходе «от негативного сценария». Если просто сказать что есть принципы и их нужно использовать — это такое себе. Если показать что бывает когда их не используешь — будет нагляднее, имхо.
      +4

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

        +3

        И скорее даже первый робот торчит из второго вместо режущей руки.

          +9

          Не обязательно.


          SOLID — это не про ООП в условной Java — это шире.


          Например есть консольная утилита sort — сортирует строки по не убыванию, и много утилит и людей использует её такой какая она есть.


          Но тут становится понятно что иногда мы получаем не тот результат что нам нужен:


          $ sort << EOF
          > 1 one
          > 2 two
          > 10 ten
          > 20 twenty
          > EOF
          10 ten
          1 one
          20 twenty
          2 two

          Нам хочется что бы числа интерпретировались именно как числа при сортировке (а не набор символов) что бы получать "математически правильную" сортировку.


          У нас есть 2 пути — нарушить SOLID и изменить поведение утилиты — но тогда десятки/сотни уже написанных утилит (использующих sort) перестанут корректно работать.
          Или не нарушать SOLID и расширить функциональность (добавив в данном случае опцию -g):


          $ sort -g << EOF
          1 one
          2 two
          10 ten
          20 twenty
          EOF
          1 one
          2 two
          10 ten
          20 twenty

          И даже если мы говорим про классы — принцип Open-Close говорит нам, что если у нас есть условный класс Foo в версии 1.0, то в версии 1.1 у него могут появиться новые методы (или опциональные параметры к старым), но старые методы должны работать так как и раньше. И не обязательно для этого создавать новый класс потомок.


          По большому счету Open-Close — это про обратную совместимость.

            +1
            $ sort -g << EOF
            По большому счету Open-Close — это про обратную совместимость.
            Разве для того, чтобы добавить новую сортировку разработчикам не понадобилось модифицировать sort? В пределе, это приводит к созданию комбайнов (что нарушает SRP), появлению мертвого кода (когда в программе требуется только сортировка по числам, то не зачем тащить балластом сортировку по строкам) и т.п.

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

            `sort -q` это пример разумного отступления от принципов SOLID в пользу упрощения.

            Хорошим примером OCP является `git [command]`, где каждая команда реализуется отдельным файлом с именем `git-*`. Чтобы добавить новую команду не нужно менять сам `git` и ждать нового релиза. Достаточно создать новый файл с командой и поместить в PATH. Сам же `git` это просто единая точка входа для запуска таких команд и вывода подсказок.
          +4

          Для начинающих стоит отметить, что солид — не панацея и не отменяет того, что писать надо прежде всего для людей, а не для машины. А то часто залезаешь в проект — формально всё по солиду, а фактически хочется запихать его авторам туда, откуда они его достали.

            +5
            У вас «правильный» пример Open-Closed нарушает Single Responsibility
              +1
              Ага. Тут как-то так:
              А умею резать полукольцами =>… => Я умею резать полукольцами и соломкой
              0
              По большому счету Open-Close — это про обратную совместимость

              Все же, нет. Данный принцип не зря сформулирован именно так, как сформулирован.
              В нем не говориться «Классы следует расширять», но
              «Классы должны  быть открыты для расширения, но закрыты для модификации.»
              То есть, код, который этому принципу следует, попросту не позволит взять и переписать поведение. Но позволит расширить функциональность через наследование или агрегацию.
              На обратную совместимость это только на первый взгляд похоже.
                0
                И вообще эти принципы говорят не о процессе написания кода, как многие заблуждаются (ибо тогда этот подход начинает запрещать рефакторинг), а о том, как этот код должен выглядеть в итоге независимо от процесса.

                Это не значит, что если код ещё никто не видел, то класс нужно модифицировать, а не расширять. И это не значит, что если мы класс пишем с нуля, то можем в него напихать 50 методов, а вот если он уже написан — то методы добавлять нельзя.

                И конечно, глупость считать, что ОпенКлоуз — про обратную совместимость. Она нужна далеко не всегда, а тут внезапно влезла в базовый принцип проектирования (которое часто вообще происходит до того, как у нас появляется хоть какое-то Legacy).
                  0
                  По-моему обратную совместимость обеспечивает LSP, а OCP просто не дает шаловливым ручкам залезать куда не просят.
                    0
                    Никакой из принципов ничего не говорит об обратной совместимости.
                    Более того, отсутствие обратной совместимости никак не противоречит принципам Solid.

                    Пример: у меня есть контракт, который следует принципам SOLID, в нём метод GetItem.

                    interface IContract {
                      Item GetItem();
                    }


                    Теперь я понимаю, что есть более подходящее название для этого метода и произвожу рефакторинг:
                    interface IContract {
                      Item GetOrCreateItem();
                    }


                    Код всё так же следует принципам SOLID (что OCP, что LSP), а обратная совместимость — сломана.
                      0
                      Следование принципам позволяет избежать типичных проблем. Если подставить вторую версию IContract туда, где ожидается первая, то программа сломается. Согласно LSP подобные типы не совместимы, и это говорит о том, что такой рефакторинг является небезопасным.
                        0
                        Это вы додумываете, ничего подобного в LSP не говорится.
                +2
                SRP — по утверждению автора наиболее трудно понимаемый принцип.

                Вы указали:

                Каждый класс должен отвечать только за одну операцию.

                В оригинале не совсем так. Такой принцип есть, но как утверждает сам автор «Uncle Bob», это не SRP.
                Под SRP понимается следующее:

                Модуль должен отвечать за одного и только за одного пользователя или заинтересованное лицо

                Окончательная версия выглядит так:

                Модуль должен отвечать за одного и только одного актора

                В книге есть пример про клаcc Employee и 2 метода reportHours() и calculatePay(). Вроде все нормально, это все касается работника. Но на деле это может нарушать SRP по той причине, что разные отделы отвечают за эти методы.

                reportHours() — Бухгалтерия
                calculatePay() — Отдел по работе с персоналом.

                И если делать изменения в соответствие с запросом одних, могут пострадать другие, если возьмем например тот факт, что оплата считается по часам.
                  +1
                  > Каждый класс должен отвечать только за одну операцию.
                  Это понятно

                  > Модуль должен отвечать за одного и только одного актора
                  Это непонятно. Т.к. операции/сценарии/юзкейсы почти всегда включают множество сущностей. акторов итд.
                  +2
                  А я читал, как реализовать SOLID на примере армии pikabu.ru/story/solid_army_7082439
                    –3
                    Возможно у меня старческое бурчание, но объяснять эти принципы картинками похоже на движение ко дну.

                    Разве человек, который не способен понять SOLID из словесного описания и нескольких примеров сможет в дальнейшем манипулировать в своей голове объектами и понятиями достаточно хорошо, чтоб писать приличный код?
                      0
                      1. Судя по тому, что SOLID регулярно постоянно обсуждают — всё это не так просто и однозначно, полученную информацию можно по разному понять, интепретировать и использовать. Причём обсуждают и примерами кода, и словесными описаниями, а тема никак не утихает.
                      2. На эту того, что такое "приличный код" в холиварах тоже сломано немало копий.
                      3. Навыки "понять из словесного описания" и "хорошо манипулировать в своей голове объектами и понятиями" (а это два отдельных, но связанных скилла) являются именно навыками — т.е. чем-то, что можно развить. Кому-то и такой формат в картинках может пойти на пользу, чтобы развить соотвествующий навык для дальнейшего использования.

                      Так что, имхо — нет, не движение на дно, а с точностью до наоборот.

                      +1
                      Хорошая попытка, но увы не наглядно.
                      :-(

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

                      Самое читаемое