Ещё будучи совсем-совсем начинающим разработчиком, я услышал про принципы SOLID и, конечно же, совершенно их не понял. Их не помогли понять ни лекции Дядюшки Боба, ни книга его же авторства, не несколько десятков (а то и сотен) статей в интернете, которые я тогда прочитал (по какой-то причине мне эти принципы казались чем-то очень важным, прямо-таки фундаментальным). Сейчас я могу сказать, что понимаю их как-то, по-своему, и пользуюсь ими каждый день. В данной короткой статье хочу поделиться своими размышлениями о сути принципов, которые были бы понятны тому мне из прошлого, который пытался загуглить доступное объяснение этих принципов.

Главный принцип разработки

Всю суть SOLID можно выразить простым допущением: "Как правило разработчикам приходится реализовывать новую функциональность, а не менять старую. Так давайте писать код так, чтобы новое было легко писать, а старое — тяжело сломать". Этот основной принцип совпадает с буквой О — open–closed principle, принципом открытости-закрытости. Все остальные принципы направлены именно на то, чтобы обеспечить именно такую структуру кода, при которой некоторый рефакторинг ничего не ломает, и в то же время новые фичи без проблем реализуются.

Буква S

Принцип единственной ответственности (он же single-responsibility principle) говорит о том, что какие-угодно блоки приложения (методы, классы, модули) лучше создавать такого размера, чтобы их не приходилось потом часто менять ("иметь одну и только одну причину для изменений"). То есть код написан один раз, и мы в принципе-то не хотим его менять. Но на всякий случай лучше сделать так, чтобы вероятность необходимости изменений была как можно более низкой.

Буква L

Принцип подстановки Барбары Лисков (Liskov Substitution Principle) говорит, что если мы дописываем новых наследников к классу, то нужно это делать таким образом, чтобы не пришлось менять весь старый код, который этих самых наследников будет использовать (старый код-то и не знает ничего про наследников). Опять-таки: легко дописываем, но тяжело ломаем.

Буква I

Принцип разделения интерфейса (interface segregation principle) — производный принцип от первой буквы, S. Дело в том, что сформулировать "единую ответственность" в терминах интерфейса бывает сложно, а этот принцип нам однозначно говорит: если какая-то реализация не использует некоторые методы интерфейса, то эти методы в интерфейсе лишние. Здесь проще всего показать на примере: в Java вот есть интерфейс Collection с методами в духе add(), remove() и так далее. И в неизменяемых реализациях коллекций эти методы, ясное дело, не нужны. Поэтому, согласно принципу I, интерфейс стоит разделить на Collection и его наследника, MutableCollection (как сделано в Kotlin, например). Цель у этого всего опять-таки одна: чем меньше методов в интерфейсе, тем реже его придется менять (и тем меньше шанс что-нибудь сломать). Ну и дописывать новые интерфейсы проще, чем дописывать методы в существующие интерфейсы.

Буква D

Принцип инверсии зависимостей (dependency inversion principle) — принцип-компаньон буквы L. Чтобы можно было легко дописывать наследников или менять реализации, нужно, чтобы код, который все это использует, не приходилось менять. То есть хочется, чтобы он зависел от чего-то постоянного, в определении принципа называемого "абстракцией". Принцип такой же, как и у остальных: мы ввели абстракцию, написали какой-то код опираясь на эту самую абстракцию. Затем можно писать разные реализации абстракции, менять реализацию уже написанных — на тот код, который опирается на неё, это не повлияет (при выполнении правила L, конечно).

Понятное дело, что мое понимание принципов не является единственно верным (а то и вообще неправильным). Однако, такое объяснение принципов мне видится очень понятным и прикладным.

Комментарии (35)


  1. pukinlou
    10.01.2022 16:51
    +10

    а где статья?


    1. babayota_kun Автор
      10.01.2022 17:00
      +3

      С публикацией что-то пошло не так? Проверил через incognito — вроде бы доступна, корректно отображается.


      1. MentalBlood
        10.01.2022 17:13
        +5

        Наверное имелась ввиду низкая содержательность/объем


        P.S.


        Их не помогли понять ни

        А что в итоге помогло? Имхо, все эти принципы легко обнаруживаются с опытом как нечто само собой разумеющееся, далее уже оттачивание мастерства их применения


        1. babayota_kun Автор
          10.01.2022 17:39
          +4

          Да, опыт, конечно :)

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


    1. acmesun
      10.01.2022 20:09
      +4

      Душнила


  1. ExplodeMan
    10.01.2022 17:11
    +1

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


  1. Dekmabot
    10.01.2022 17:16
    +18

    Статья лёгенькая, буквально в двух словах, поэтому позволю себе показать ещё более простой вариант на картинке под спойлером (автор неизвестен):

    буквально в двух словах


    1. OlegZH
      10.01.2022 22:08
      +3

      Как же, теперь, жить? После такого спойлера.


    1. SadOcean
      11.01.2022 23:31

      "Делай просто, насколько возможно, но не проще этого."


  1. apache2
    10.01.2022 17:38
    +6

    Может это просто плохая идея если уже в 1000 раз повторяете а они все не могут вашу молитву выучить?


  1. gkislin
    10.01.2022 17:48
    +2

    Вот как раз по теме, подробнее: https://blog.byndyu.ru/2009/10/solid.html


  1. vt4a2h
    10.01.2022 18:32
    +1

    Буква L

    Принцип подстановки Барбары Лисков (Liskov Substitution Principle) говорит, что если мы дописываем новых наследников к классу, то нужно это делать таким образом, чтобы не пришлось менять весь старый код, который этих самых наследников будет использовать (старый код-то и не знает ничего про наследников). Опять-таки: легко дописываем, но тяжело ломаем.

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


    1. nin-jin
      10.01.2022 19:07
      +1


      1. funca
        11.01.2022 00:30
        +1

        Ну ковартантность и LSP они обсуждали ещё в том же самом тредике, где Дядя Боб имел неосторожность предложить свои принципы (их там кстати больше) - https://groups.google.com/g/comp.object/c/WICPDcXAMG8?hl=en&pli=1#adee7e5bd99ab111.

        Барбара в интервью рассказывала, что формулировала LSP как неформальное правило, больше с целью порассуждать, без претензий на научную строгость. У Дяди же адаптация принципа под OOD (с оглядкой на C++) выглядит вообще как каламбур. Это уже потом Дядю канонизировали, а сказанное превратили в догму.


        1. nin-jin
          11.01.2022 01:42
          +3

          Ну как без претензий..

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

          Вообще, определение подтипизации, зависящее от того какие функции мы вызываем в программе, - это какое-то вырывание гланд через анус.


          1. ApeCoder
            11.01.2022 08:59
            +1

            Спасибо за ссылку
            1987 - это ваша ссылка

            3.3. Type Hierarchy A type hierarchy is composed of subtypes and supertypes. The intuitive idea of a eubtype is one whose objects provide all the behavior of objects of another type (th e supertype) plus something extra. What is wanted here is something like the following substitution property [S]: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for oz, then S is a subtype of T. (See also [2, 171 for other work in this area.)

            Т.е. для всех программ, не для одной функции.

            А вот википедия - 1994
             Barbara Liskov and Jeannette Wing described the principle succinctly in a 1994 paper as follows:[1]

            Subtype Requirement: Let {\displaystyle \phi (x)}

             be a property provable about objects {\displaystyle x} of type T. Then {\displaystyle \phi (y)} should be true for objects {\displaystyle y} of type S where S is a subtype of T.

            Вообще, определение подтипизации, зависящее от того какие функции мы вызываем в программе, - это какое-то вырывание гланд через анус.

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

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


            1. funca
              11.01.2022 12:51
              +1

              У нее есть более поздняя работа, где она придумала другое определение подтипов на полстраницы A behavioral notion of subtyping.


            1. nin-jin
              11.01.2022 13:31
              +1

              Для "всех возможных программ" по такому определению мутабельные типы вообще не образуют иерархии ибо являются инвариантными.


              1. funca
                11.01.2022 14:08

                Что такое инвариантные типы?


                1. nin-jin
                  11.01.2022 15:11
                  +1

                  1. ApeCoder
                    11.01.2022 20:52
                    +1

                    Подставляются не типы а объекты типа. Конвариантны/контравариантны не типы а преобразования типов. Т.е. функция над типами. Ко-вариантны

                    Within the type system of a programming language, a typing rule or a type constructor is:

                    • covariant if it preserves the ordering of types (≤), which orders types from more specific to more generic: If A ≤ B, then I<A> ≤ I<B>;

                    • contravariant if it reverses this ordering: If A ≤ B, then I<B> ≤ I<A>;

                    • bivariant if both of these apply (i.e., if A ≤ B, then I<A> ≡ I<B>);[1]

                    • variant if covariant, contravariant or bivariant;

                    • invariant or nonvariant if not variant.

                    т.е. у вас есть спрособ описать функцию на пространстве типов f(x) -> y
                    если из x1 is x2 следует f(x1) is f(x2) то преобразование ковариантно. (Аргумент со-варьируется с результатом)

                    Например в C# вот это не скомпилируется, так как List<> сам по себе не тип а фактически конструктор типов.

                     public static void Main()
                      {
                        object x = null;
                        if (x is List<object> || x is List<>)
                        {
                    
                    
                        }
                      }

                    Впрочем, мы это уже обсуждали


              1. ApeCoder
                11.01.2022 20:38

                Тут мне непонятно. Если есть коллекция ICollection с методами Add(Object x) и Object GetFirst() почему мутабельный List с такими же методами не является его подтипом?


          1. funca
            11.01.2022 10:24
            +2

            Это исследовательская работа, скорее уровня реферата. Она там собирает примеры абстракции данных в паре языков и в одном из разделов рассуждает про разницу между наследованием реализации и иерархией типов (а сейчас будет лучше сказать - спецификаций - потому, что ее трактовка типа там отличается от привычной из type theory). А сам "принцип" это цитата из другого автора, который она привела просто в качестве примера:

            The intuitive idea of a subtype is one whose objects provide all the behavior of objects of another type (the supertype) plus something extra. What is wanted here is something like the following substitution property [6]: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2, then S is a subtype of T. (See also [2, 17] for other work in this area.)

            Я так понимаю LSP для ООП это стало с подачи дяди, сама Барбара ни чего такого не предполагала. Даже наоборот:

            We are using the words “subtype” and “supertype” here to emphasize that now we are talking about a semantic distinction. By contrast, “subclass” and “superclass” are simply linguistic concepts in programming languages that allow programs to be built in a particular way. They can be used to implement subtypes, but also, as mentioned above, in other ways.


  1. LARII
    10.01.2022 21:23

    SRP трактуют по разному. "Одна причина" - это про Big Bang или теологию?


  1. OlegZH
    10.01.2022 22:19
    +1

    Почему-то в статье (да и в самой методологии) ничего не говорится о том, что нужны различные уровни описания. Дело в том, что, обычно, берётся иерерхия классов, инкапсулирующих важнейшие библиотеки, и создаётся новая иерархия классов. Точнее, создаются наследники существующих классов и соответствующие им иерархии. Классов (например, в Java) и так очень много, а тут ещё добавляется новая иерархия для описания предметной области. Тут зарыта довольно серьёзная собака. Системный подход не допускает смешение уровней описания, а это значит, что иерархия системных объектов и иерархия прикладных объектов — это две незавиисмые друг от друга иерархии. Вы берёте иерархию системных объектов и создаёте, по сути, новый язык описания, в терминах которого описываете уже объекты предметной области.


  1. OlegZH
    10.01.2022 22:30
    -2

    в Java вот есть интерфейс Collection с методами в духе add(), remove() и так далее. И в неизменяемых реализациях коллекций эти методы, ясное дело, не нужны. Поэтому, согласно принципу I, интерфейс стоит разделить на Collection и его наследника, MutableCollection (как сделано в Kotlin, например).

    А не будет более естественным иметь единый класс Collection, но иметь параметр в конструкторе, который описывает поведение коллекции? (В некоторых ситуациях, можно было бы, даже, ожидать создания шаблона Collection.) По своей сути, коллекция — это абстрактный тип данных, который в самом общем виде описывает некоторый контейнер однородных (в определённом отношении) объектов. Но этот абстрактный тип инкапсулирует два следующих объекта: способ доступа и внутреннее представление.

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


    1. csl
      10.01.2022 23:11

      неизменяемые реализации коллекций

      Подозреваю, топик стартер имел в виду, что в java.util.Collections есть методы .unmodifiableList, .unmodifiableSet, возвращающие экземпляры неизменяемых коллекций.


  1. OlegZH
    10.01.2022 22:33

    Совершенно выпустил из виду, что интересное начинается уж здесь:

    в Java вот есть интерфейс Collection

    То есть: колекция сама является интерфейсом (задумчиво уходит в совершенном замешательстве).


  1. funca
    10.01.2022 23:39

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

    А вот в английской wiki про SOLID

    design principles intended to make software designs more understandable, flexible, and maintainable

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


    1. babayota_kun Автор
      10.01.2022 23:53

      Так это ведь никак не противоречит статье: поддержка-то в основном заключается в допиливании новых фич или в исправлении багов.
      Когда пилишь новую фичу будет хорошо, если это можно сделать просто и быстро, без глобальных изменений в коде. Когда фиксишь баг — хорошо, если изменения в одном месте не повлекут необходимости переписать пол проекта (и отломать кучу всего в придачу).

      Код живёт в разработке ну может пару дней

      В этом же и суть: запилили фичу и забыли. Написанный код больше стараемся не трогать.


  1. yurii_yakhnytsa
    10.01.2022 23:49
    +2

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

    А чисто от себя могу добавить, что понимание пришло довольно быстро. И на проекте на котором сейчас работаю благо все принципы закладывались со старта. И если какой либо принцип по той или иной причине нарушался было чувство, что ставишь на дороге грабли, и что вы думаете? Через пару месяцев эти грабли били по голове, и ты в очередной раз убеждаештся в их правомерности и актуальности.


  1. agoncharov
    11.01.2022 07:46
    +1

    Как правило разработчикам приходится реализовывать новую функциональность

    Это неверное допущение. Принципы SOLID в первую очередь о том, как сделать так чтобы изменение существующей функциональности — равно как и добавление новой — не было сильно трудозатратым и не часто приводило к багам.

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


  1. Litovets
    11.01.2022 12:14
    +1

    Принцип "O" пропустил )


  1. devalio
    12.01.2022 16:53

    Приз за самую короткую и бессодержательную статью о принципах SOLID сфоткаете?

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

    с этой фразой согласен


    1. vgogolin
      12.01.2022 20:57
      +1

      Учитывая интуитивное соотношение новых/существующих систем, как правило, приходится дополнять уже неотъемлемую старую функциональность. Слово "новое" в этом ключе не совсем верное. Больше похоже на приобревшее дальнейшее развитие старое. И поскольку мы, как правило, докладываем небольшие кирпичики к уже существующей большой куче, основной акцент должен быть на том, как эту кучу не разрушить.