SOLID критикует тот, кто думает, что действительно понимает ООП
© Куряшкин Виктор

Я знаком с принципами SOLID уже 6 лет, но только в последний год осознал, что они означают. В этой статье я дам простое объяснение этим принципам. Расскажу о минимальных требованиях к языку программирования для их реализации. Дам ссылки на материалы, которые помогли мне разобраться.


Первоисточники


Придумал принципы SOLID Роберт Мартин (Uncle Bob). Естественно, что в своих работах он освещает эту тему.


Книга “Принципы, паттерны и методики гибкой разработки на языке C#” 2011 года. Большинство статей, которые я видел, основываются именно на этой книге. К сожалению, она дает расплывчатое описание принципов, что сильно ударило по их популярности.


Видео сайта cleancoders.com. Дядюшка Боб в шутливой форме на пальцах рассказывает, что же именно означают принципы и как их применять.


Книга “Clean Architecture” 2017 года. Описывает архитектуру, построенную из кирпичиков, удовлетворяющих SOLID принципам. Дает определение структурному, объектно-ориентированному, функциональному программированию. Содержит лучшее описание SOLID принципов, которое я когда-либо видел.


Требования


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


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


  • Динамический полиморфизм — это про абстрактные классы, интерфейсы, утиную типизацию, т.е. только в рантайме будет понятно, с каким типом будет работать наш код.
  • Статический полиморфизм — это в основном про шаблоны (genererics). Когда уже на этапе компиляции из одного шаблонного кода генерируется код специфичный для каждого используемого типа.

Кроме привычных языков вроде Java, C#, Ruby, JavaScript, динамический полиморфизм реализован, например в


  • Golang, с помощью интерфейсов
  • Clojure, с помощью протоколов и мультиметодов
  • в прочих, совсем не “ООП” языках

Принципы


SOLID принципы советуют, как проектировать модули, т.е. кирпичикам, из которых строится приложение. Цель принципов — проектировать модули, которые:


  • способствуют изменениям
  • легко понимаемы
  • повторно используемы

SRP: The Single Responsibility Principle


A module should be responsible to one, and only one, actor.
Старая формулировка: A module should have one, and only one, reason to change.


Часто ее трактовали следующим образом: Модуль должен иметь только одну обязанность. И это главное заблуждение при знакомстве с принципами. Все несколько хитрее.


На каждом проекте люди играют разные роли (actor): Аналитик, Проектировщик интерфейсов, Администратор баз данных. Естественно, один человек может играть сразу несколько ролей. В этом принципе речь идет о том, что изменения в модуле может запрашивать одна и только одна роль. Например, есть модуль, реализующий некую бизнес-логику, запросить изменения в этом модуле может только Аналитик, но никак не DBA или UX.


OCP: The Open Closed Principle


A software artifact should be open for extension but closed for modification.
Старая формулировка: You should be able to extend a classes behavior, without modifying it.


Это определенно может ввести в ступор. Как можно расширить поведение класса без его модификации? В текущей формулировке Роберт Мартин оперирует понятием артефакт, т.е. jar, dll, gem, npm package. Чтобы расширить поведение, нужно воспользоваться динамическим полиморфизмом.


Например, наше приложение должно отправлять уведомления. Используя dependency inversion, наш модуль объявляет только интерфейс отправки уведомлений, но не реализацию. Таким образом, логика нашего приложения содержится в одном dll файле, а класс отправки уведомлений, реализующий интерфейс — в другом. Таким образом, мы можем без изменения (перекомпиляции) модуля с логикой использовать различные способы отправки уведомлений.


Этот принцип тесно связан с LSP и DIP, которые мы рассмотрим далее.


LSP: The Liskov Substitution Principle


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


Классический пример нарушения. Есть базовый класс Stack, реализующий следующий интерфейс: length, push, pop. И есть потомок DoubleStack, который дублирует добавляемые элементы. Естественно, класс DoubleStack нельзя использовать вместо Stack.


У этого принципа есть забавное следствие: Объекты, моделирующие сущности, не обязаны реализовывать отношения этих сущностей. Например, у нас есть целые и вещественные числа, причем целые числа — подмножество вещественных. Однако, double состоит из двух int: мантисы и экспоненты. Если бы int наследовал от double, то получилась бы забавная картина: родитель содержит 2-х своих детей.


В качестве второго примера можно привести Generics. Допустим, есть базовый класс Shape и его потомки Circle и Rectangle. И есть некая функция Foo(List<Shape> list). Мы считаем, что List<Circle> можно привести к List<Shape>. Однако, это не так. Допустим, это приведение возможно, но тогда в list можно добавить любую фигуру, например rectangle. А изначально list должен содержать только объекты класса Circle.


ISP: The Interface Segregation Principle


Make fine grained interfaces that are client specific.


Под интерфейсом здесь понимается именно Java, C# интерфейс. Разделение интерфейса облегчает использование и тестирование модулей.


DIP: The Dependency Inversion Principle


Depend on abstractions, not on concretions.


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

Что такое модули верхних уровней? Как определить этот уровень? Как оказалось, все очень просто. Чем ближе модуль к вводу/выводу, тем ниже уровень модуля. Т.е. модули, работающие с BD, интерфейсом пользователя, низкого уровня. А модули, реализующие бизнес-логику — высокого уровня.


Что такое зависимость модулей? Это ссылка на модуль в исходном коде, т.е. import, require и т.п. С помощью динамического полиморфизма в runtime можно обратить эту зависимость.


Есть модуль Logic, реализующий логику, который должен отсылать уведомления. В этом же пакете объявляется интерфейс ISender, который используется Logic. Уровнем ниже, в другом пакете объявляется ConcreteSender, реализующий ISender. Получается, что в момент компиляции Logic не зависит от ConcreteSender. В runtime, например, через конструктор в Logic устанавливается экземпляр ConcreteSender.


Отдельно стоит отметить частый вопрос “Зачем плодить абстракции, если мы не собираемся заменять базу данных?”.


Логика тут следующая. На старте проекта, мы знаем, что будем использовать реляционную базу данных, и это точно будет Postgresql, а для поиска — ElasticSearch. Мы даже не планируем их менять в будущем. Но мы хотим отложить принятие решений о том, какая будет схема таблиц, какие будут индексы, и т.п. до момента, пока это не станет проблемой. И на этот момент мы будем обладать достаточной информацией, чтобы принять правильное решение. Также мы можем раньше отладить логику нашего приложения, реализовать интерфейс, собрать обратную связь от заказчика, и минимизировать последующие изменения, ведь многое реализовано только в виде заглушек.


Принципы SOLID подходят для проектов, разрабатываемых по гибким методологиям, ведь Роберт Мартин — один из авторов Agile Manifesto.


Принципы SOLID стремятся свести изменение модулей к их добавлению и удалению.


Принципы SOLID способствуют откладыванию принятия технических решений и разделению труда программистов.

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


  1. mkuzmin Автор
    05.02.2018 10:58

    Если вы знакомы с Clean Architecture и не боитесь Clojure, то стоит просмотреть мой проект



    1. yvm
      06.02.2018 08:01

      Вот вы умничаете а у вас XSS в демке…


  1. solver
    05.02.2018 11:49
    -1

    Как так получается? «Придумал принципы SOLID Роберт Мартин (Uncle Bob)», но как минимум один из принципов называется «LSP: The Liskov Substitution Principle»?
    Вам не кажется, что вы не разобравшись в первоисточниках вводите людей взаблуждение?


    1. SamDark
      05.02.2018 12:34
      +1

      Придумал, а точнее взял Coupling/Cohesion, вывел из них красивые буквы SOLID и сделал их модными именно Роберт Мартин. Тут ошибки нет.


      1. solver
        05.02.2018 12:55

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


      1. ookami_kb
        06.02.2018 13:26

        Вот как раз красивые буквы SOLID вывел Michael Feathers


        1. SamDark
          06.02.2018 13:51

          Похоже на то, но вообще это одна контора же, ThoughtWorks. Они зарабатывают на консалтинге и им нужно было чтобы старый добрый cohesion/coupling стал новым модным SOLID.


  1. a4k
    05.02.2018 11:52

    SRP: The Single Responsibility Principle
    В этом принципе речь идет о том, что изменения в модуле может запрашивать одна и только одна роль. Например, есть модуль, реализующий некую бизнес-логику, запросить изменения в этом модуле может только Аналитик, но никак не DBA или UX.
    То есть если метод состоит из 3k+ строк он соответствует этому принципу главное что бы изменения запрашивались от одной роли?
    OCP: The Open Closed Principle

    Используя dependency inversion, наш модуль объявляет только интерфейс отправки уведомлений, но не реализацию.
    Зачем же тогда отдельно выделены dependency inversion principle?


    1. mayorovp
      05.02.2018 12:04

      То есть если метод состоит из 3k+ строк он соответствует этому принципу главное что бы изменения запрашивались от одной роли?

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


      1. a4k
        05.02.2018 12:54

        Автор исключил дополнительную ответственность из принципа


        1. mayorovp
          05.02.2018 13:07

          Там ее и не было…


          1. a4k
            05.02.2018 13:12

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


            1. mayorovp
              05.02.2018 13:15

              Если автор не использует какого-то термина — то мне его тоже нельзя использовать? Хорошо, переформулирую.


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


              1. a4k
                05.02.2018 13:26

                Ну уменьшите количество строк до 500, вероятность того что изменения запрошены от одной роли будут намного выше.
                Если вы пишете обычно, значит описанная мной ситуация возможна.
                Так все же такой метод соответствует SRP?


                1. mayorovp
                  05.02.2018 13:27

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


                  1. a4k
                    05.02.2018 13:32

                    А что вам даст код? Что вы там хотите увидеть?
                    Ответ на критерий по которому автор определяет соответствие кода принципу SRP я дал.


                    1. mayorovp
                      05.02.2018 13:33

                      Я вам уже написал: метод на 3к строк может как удовлетворять этому критерию, так и нет. Что еще вы хотите услышать?


              1. boblenin
                06.02.2018 00:10

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


        1. VolCh
          05.02.2018 15:21

          Чем больше строк в методе, тем больше вероятность, что в нём есть дополнительные ответственности, но это лишь вероятность.


          1. boblenin
            06.02.2018 00:13

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


    1. SamDark
      05.02.2018 12:36
      +2

      Зачем же тогда отдельно выделены dependency inversion principle?

      Потому что не получилась бы красивая аббревиатура :) В SOLID часть принципов прилично накладываются на остальные.


  1. vasIvas
    05.02.2018 11:56
    -1

    Например, есть модуль, реализующий некую бизнес-логику, запросить изменения в этом модуле может только Аналитик, но никак не DBA или UX.

    Тогда, если двум объектам понадобится изменить один объект, то они попросят сделать это Аналитика.
    А разве это не нарушит принцип, так как аналитика будут дергать сразу двое?

    И вроде уже давно разобрались что именно эта формулировка как раз и не верная…


    1. mayorovp
      05.02.2018 11:59

      Объект не может о чем-то попросить Аналитика, потому что Аналитик — роль в команде, а не объект и не модуль :-)


      1. vasIvas
        05.02.2018 14:58

        говорится что «запросить изменения в этом модуле может только Аналитик». Но если никто не может попросить актора что-то сделать, то как тогда он понимает что нужно что-то вызвать?


        1. mayorovp
          05.02.2018 15:00

          Вас кто-то просил писать этот комментарий? Если нет — то как вы поняли что вам это нужно сделать?


          1. vasIvas
            05.02.2018 16:32

            А играющий роль может просить другого играющего роль что-то сделать?
            То есть два других актора могут попросить одного Аналитика что-то сделать?


            1. mkuzmin Автор
              05.02.2018 17:30

              конечно, программа подстраивается под отношения людей, а не наоборот


            1. VolCh
              05.02.2018 18:48
              +1

              Могут. Но к разработчику он должен обратиться не в форме "меня тут попросили...", а в форме "сделай мне...". А разработчик ему может ответить "не буду, это не твой уровень абстракции" :)


              1. boblenin
                06.02.2018 00:16

                Все верно, но «сделай мне» говорит не аналитик, а «product owner» хотя бы в SCRUM. Который занимается управлением бэклогом. А значит, согласно, данному в статье определению — фигачим в метод все что хотим, все-равно придет все через того же самого «product owner»-a.


                1. VolCh
                  06.02.2018 13:13

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


                  1. boblenin
                    06.02.2018 16:17

                    Да это понятно. Только вот как вы представляете трансляцию пожеланий скажем от 4-го клерка 128-го отделения банка? Должен ли product owner в user story писать кто источник информации? Всегда ли product owner будет это знать?

                    Это создает такой избыточный геморой на всем этапе анализа, что не в сказке сказать. А все ради того чтобы программист чуть-чуть по-другому структурировал код. Сдается мне — это фантастика.


                    1. VolCh
                      07.02.2018 12:06

                      Вряд ли 4-й клерк 128-го отделения банка вообще знает о существовании product owner. Непосредственному начальству сообщит или в саппорт. И так или иначе пожелание дойдёт до стейкхолдера (например, директора операционного департамента), который либо передаст его product owner, либо получит от него это пожелание для утверждения/отклонения.


                      1. boblenin
                        07.02.2018 18:05

                        Ну так у директора пожеланий может быть в стольких областях. 10 экранами логики в одном модуле не обойдетесь.


                        1. VolCh
                          07.02.2018 18:14

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


                          1. boblenin
                            07.02.2018 18:32

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

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


                            1. VolCh
                              07.02.2018 18:46

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


                              1. boblenin
                                07.02.2018 21:13

                                Так ведь согласно описанию необходимым условием является один стэйкхолдер. Директор Опс-ов. По закону Мерфи разработчик сделает все одним объектом. В лучшем виде с dependency injection, в виде command pattern.

                                Объявит интерфейс, со 100500 зависимостями и единственным методом Run(). Сделает одну уникальную его реализацию в которую запихнет вызов репозитория, который в свою очередь тоже будет реализовывать Command pattern. И после 10 вызовов зависимостей, которые только и делают что вызывают зависимости — внизу будет хранимая процедура, которая посчитает биллинг, проставит флаги и отправит email. И вот эта процедура и будет соответствовать SOLID.

                                Сдается мне, что в статье на задана достаточность условия.


                                1. VolCh
                                  08.02.2018 18:18

                                  Что у модуля/класса/метода должен быть один стейкхолдер не означает, что у стейкхолдера должен быть один модуль/класс/метод.


                                  1. boblenin
                                    08.02.2018 18:36

                                    Но обратное также не зафиксировано в определении.


                1. mayorovp
                  06.02.2018 13:25

                  Хоть Product owner и управляет бэклогом, но сами задачи в бэклоге исходят не от него.


  1. AbstractGaze
    05.02.2018 11:57
    -2

    На старте проекта, мы знаем, что будем использовать реляционную базу данных, и это точно будет Postgresql, а для поиска — ElasticSearch.

    На старте это хорошо, и как часто то, что, на старте доходит до конца проекта? Вы на старте привязываетесь, и не можете ничего поменять потому что:
    1. это SOLID
    2. это потребует больших затрат времени
    3. выбросить уже написанное, а это время/деньги.
    Что по этому принципу можно писать? программы? продукты? или системы? Если только программы то тогда еще более менее понятно. Но продукты и системы пишутся не один месяц, и много меняется. Как вы по этим принципам подстраиваетесь под реалии и новые требования клиентов?


    1. c4boomb
      05.02.2018 12:19
      +1

      Вы бы сначала почитали
      SOLID говорит строить зависимости на абстракциях.
      Ваши репозитории зависят на AbstractPersistor или PersistorInterface.
      В начале реализовали PostgreSQLPersistor который имплементирует абстракцию. Захотелось использовать MariaDB имплементировали MariaPersistor и с помощью dependency injection подменили.
      SOLID вам только помогает в этом вопросе


    1. Atorian
      05.02.2018 12:27
      +2

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

      А по поводу 3-го пункта — так надо иногда делать, ибо выбранная в начале пути технология может не обеспечить удовлетворения новых требований, предъявляемых проекту. Если этого не сделать — вы просто навредите проекту.

      Технологии не приколачиваются гвоздями к решению.


      1. AbstractGaze
        05.02.2018 12:32

        Тогда я совсем не понял, зачем нам надо точно знать что мы будем использовать, ведь по большому счету мы можешь использовать в начале просто заглушку для бд? И почему мы не можем запланировать замену в будущем, если все достаточно легко меняется?
        Можно этот момент пояснить?


        1. mayorovp
          05.02.2018 12:34

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


        1. mkuzmin Автор
          05.02.2018 12:37

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


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


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


          1. SamDark
            05.02.2018 12:43

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


            1. mkuzmin Автор
              05.02.2018 12:56

              Можно провести такую аналогию. Заказчик говорит: Нужно отправить письмо, если (тут набор бизнес требований). Т.е. он не говорит, я хочу отправить письмо через Mailchimp. Он закрывается абстракцией и откладывает решение о реализации.


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


              Все зависит от архитектора и его компетенций. Само-собой можно такого напроектировать.


              Просмотри первоисточники, там подробно разобраны твои вопросы.


              1. SamDark
                05.02.2018 13:06

                В случае письма у нас:


                1. Требования ясны с самого начала.
                2. Абстракция довольно проста (стоимость внедрения стремится к нулю).
                3. Мотивация смены последующего способа отправки, как и его вероятность высока и понятна.

                Очевидно, что тут не сделать небольшую абстракцию — преступление.


                В случае с той же СУБД всё уже не так просто.


                1. mkuzmin Автор
                  05.02.2018 13:11

                  Про случай СУБД, советую посмотреть https://github.com/darkleaf/building-application


                  1. SamDark
                    05.02.2018 13:26

                    Да ясно, что clean architecture и всё такое. Это не единственный подход к проектированию.


          1. AbstractGaze
            05.02.2018 12:44

            Меня скорее смутило другое. Допустим я создаю абстракцию, но при этом до ее создания я должен точно знать что буду использовать (как написано в статье)
            Чтобы точно знать, я должен решить и возможно изучить вопрос — т.е. потратить время/деньги. В случае же применения заглушки сразу — я не буду тратить время. Знать, что мне там точно нужно, получиться само собой после реализации логики и т.п.
            Или я все равно что, то недопонимаю?


            1. mkuzmin Автор
              05.02.2018 12:50

              Чтобы вставить заглушку — нужно объявить интерфейс(абстракцию).


              Давай пример попроще.
              Допустим, нужно отправлять уведомления.
              Мы объявляем интерфейс вроде IMailSender с одним методом void Send(address, title, content).
              Далее описываем бизнес-логку. В тесах используем mock/stub/fake object с интерфейсом IMailSender.


              Нам важно примерно знать что будет, а не точно.


              Найди книжку Clean Architecture, там все доступно объясняется.


            1. mkuzmin Автор
              05.02.2018 12:57

              посмотри еще вот это: https://github.com/darkleaf/building-application


              там пример на clojure, но все доступно объясняется


            1. Atorian
              05.02.2018 15:02

              Так в этом и прелесть абстракций. Вам не надо знать что именно вы будете использовать. Заглушка === Interface === Абстракция. Мне кажется вы все правильно поняли.

              Другое дело, что не все абстракции одинаково полезны.

              Про ту же СУБД. Мы можем ввести абстракцию TableGateway или Repository. Обе они прячут имплементацию БД. Но первая, скорее всего, треснет, как только надо будет поменять тип хранилища с реляционого на ключ-значение. Но никаких проблем в смене MySQL на другую SQL базу не возникнет. А Repository будет жить даже в любом случае, но нужен ли он в приложении — другой вопрос.

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


          1. boblenin
            06.02.2018 00:21

            Это типичный путь архитектора, который приводит к перерасходу бюджета. Цель продукта — заработать денег. Значит надо как можно скорее произвести конкретную реализацию конкретного бизнесс процесса преподнесенного как тезис, получить подтверждение тезиса через прибыль (или другие измеряемые метрики), реализовать новую версию продукта (возможно выбросив, но лучше дополнив оригинальную — OpenClose (не правим, а дополняем… или выбрасываем)).


        1. VolCh
          05.02.2018 15:27

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


          1. AbstractGaze
            05.02.2018 15:46

            Что не надо это понятно, на то это и есть абстракция. Если посмотрите на тот текст что я цитировал, то там «знание» указано чуть ли не как требование для составления абстракции. Но, я уже понял, что все понял правильно. Просто в статье не понятно зачем на этом сделан акцент.


            1. VolCh
              05.02.2018 18:50

              Я его понял по другому: мы уже знаем что будет за абстракцией 100% ближайшие 10 лет, но всё равно лучше ввести абстракцию.


              1. boblenin
                06.02.2018 00:22

                Лучше сделать работающий продукт.


                1. VolCh
                  06.02.2018 13:14

                  Введение абстракции никак не влияет будет работать первая версия или нет. Ну, считая, что абстракция применена правильно.


                  1. SamDark
                    06.02.2018 14:07

                    Смотря что абстрагировать. Взять те же СУБД. Подход с репозиториями верный, но не самый быстрый. Ну, допустим, сделаем.


                    В реализации репозиториев завязываться на конкретную СУБД по полной и фигачить внутри SQL? А если заменить надо будет через 10 лет… хммм… начинаем смотреть на PostgreSQL и MySQL. Понимаем что мало того, что придётся прижаться и юзать SQL99 и выкинуть половину крутых фич конкретной базы сразу (и это верно для большинства готовых data mapper-ов и AR), так ещё и не совсем тривиальные обёртки написать для типов данных, JSON и так далее.


                    Итого время на реализацию такой обёртки составляет треть от времени реализации начальной версии проекта… а может всё-таки зафиксировать что у нас будет всегда PostgreSQL и не страдать фигнёй?


                    1. VolCh
                      06.02.2018 14:32

                      Мы можем создать абстракцию, максимально приближенную к целевой реализации, например, вынося в интерфейс возможности PostgreSQL, которых в обозримом будущем в MySQL не появится. Но всё-таки абстрагируясь от деталей реализации этой возможности PostgreSQL, например Repository.getNextId => this.query('SELECT next_val('seq_name'))


                      1. SamDark
                        06.02.2018 15:11

                        1. Целевая реализация 10 лет не будет стоять на месте.
                        2. Если отталкиваться только от PostgreSQL, переезд, например, на MySQL обернётся такими костылями, что лучше и не пытаться.


                        1. VolCh
                          06.02.2018 15:46

                          1. Альтернативная тоже не будет, например в MySQL появилась поддержка JSON. С другой стороны, какая-то используемая функциональность может быть объявлена deprecated и потом вообще выпилена, с предложением более эффективного альтернативного способа получения того же результата. А у нас получение результата за абстракцией, в одном месте (в идеале) точечено меняем реализацию и всё.
                          2. А на Oracle можно и попытаться.


                          1. boblenin
                            06.02.2018 16:21
                            +1

                            Если у вас все приложение — это CRUD с напылением бизнесс логики, то начав с репозиториев в первой версии, до миграции на MySQL можно и не дожить. Проект закроют из-за слишком больших затрат.


                            1. VolCh
                              07.02.2018 12:14
                              +1

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


                              • используемая инфраструктура хорошо заточена под них
                              • не предпринимаются попытки написать универсальные реализации, просто обычный SQL-код или обращения к NoSQL хранилищам, а также какие-то ORM/ODM прячутся за фасадом репозитория, не оперирующего терминами выбранной системы хранения или её классом, а оперирующего терминами абстрактного хранилища.

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


                              1. SamDark
                                07.02.2018 13:10

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


                                1. VolCh
                                  07.02.2018 13:16

                                  Относительно легко. Как минимум не нужно будет рыскать по всему коду приложения в поисках SQL :)


                                  А примеры на то и примеры, чтобы лишь демонстрировать идею на понятных всем вещах.


                                  1. SamDark
                                    07.02.2018 13:20

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


                                    Как по мне, пример с заменой СУБД — это один из самых плохих примеров потому как в нём не ясно и спорно всё от и до. Вот пример с отсылкой почты или SMS прост, понятен, мало затратен и имеет смысл в подавляющем большинстве случаев.


                                    1. boblenin
                                      07.02.2018 18:24

                                      Именно. Причем зачастую репозитории либо выраждаются в обертки над ORM, либо наоборот покрываются кучей бизнесс логики. Либо еще хуже — логика уходит на уровень хранимых процедур.

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


                                    1. VolCh
                                      07.02.2018 18:41
                                      +1

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


                                  1. boblenin
                                    07.02.2018 18:21
                                    +1

                                    Будет относительно не легко понять почему все тормозит и как это оптимизировать.


                              1. boblenin
                                07.02.2018 18:20
                                +1

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

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

                                В других случаях репозитории — не гиря на ногах, конечно. Но как минимум наручник пристегнутый на икру.


                                1. VolCh
                                  07.02.2018 18:38

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


                                  Я бы тупой репозиторий с кучей findBy… методов сравнил с рукавицей, а с поддержкой полноценных критериев — с перчаткой.


                                  1. boblenin
                                    07.02.2018 19:11
                                    +1

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


                                    1. VolCh
                                      08.02.2018 18:21

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


                                  1. SamDark
                                    07.02.2018 19:17
                                    +1

                                    "Рукавица" зачастую лучше. У "перчатки" внезапно может оказаться слишком много вариантов её конфигурации и логика конфигурирования полезет вон из репозитория.


                                    1. VolCh
                                      08.02.2018 18:28

                                      Не спорю. Собственно "перчатки" обычно использую только в каких-то особых случаях, и, да, обычно она дырявая получается :)


                          1. SamDark
                            06.02.2018 16:33

                            1. А не достаточно ли нам в этом случае репозиториев? Да, поправить, возможно, придётся не в одном месте, а в 10-20, но это не так затратно, как разработка хорошего слоя абстракции реляционной базы (в рамках одного проекта это не выглядит реалистичным в принципе).
                            2. Тоже есть отличия, но да. Тут уже попытка не будет столь болезненной, если не очень сильно отходить от SQL99.


                            1. VolCh
                              07.02.2018 12:26

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


                              1. SamDark
                                07.02.2018 13:08
                                +1

                                Имею ввиду не абстрагировать внутренности метода репозитория никак. То есть использовать там SQL или пользоваться простой готовой обёрткой. Не пытаться сделать аналог DQL или HQL, стараясь абстрагировать специфичные фичи PostgreSQL на низком уровне.


                                1. VolCh
                                  07.02.2018 13:21

                                  Это, конечно, да. Для подавляющего большинства проектов это будет очень неоправданным расходованием ресурсов. В целом я о более высоуровневых абстракциях. На низком уровне или использовать что-то готовое, или писать низкоуровневый код, спрятанный за достаточно высоким уровнем абстракции. Или комбинировать, например, для простого CRUD брать ORM в качестве основной реализации репозитория, а для тяжелых вещей типа отчётов писать непосредственную работу с базой.


                                  1. boblenin
                                    07.02.2018 18:29
                                    +2

                                    > для простого CRUD брать ORM в качестве основной
                                    > реализации репозитория

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


                                    1. VolCh
                                      07.02.2018 18:49
                                      +1

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


  1. retran
    05.02.2018 12:57
    +3

    но только в последний год осознал, что они означают


    Нет, судя по тексту статьи — все еще не осознали.

    На каждом проекте люди играют разные роли (actor): Аналитик, Проектировщик интерфейсов, Администратор баз данных. Естественно, один человек может играть сразу несколько ролей. В этом принципе речь идет о том, что изменения в модуле может запрашивать одна и только одна роль. Например, есть модуль, реализующий некую бизнес-логику, запросить изменения в этом модуле может только Аналитик, но никак не DBA или UX.


    А если на проекте появляются новые роли — надо все переписывать?

    Это определенно может ввести в ступор. Как можно расширить поведение класса без его модификации?


    Наследование, вариации паттерна «Стратегия» и так далее. При чем тут dll и jar?

    LSP: The Liskov Substitution Principle


    Есть класс Квадрат и есть класс Прямоугольник. Что от чего наследовать?

    Под интерфейсом здесь понимается именно Java, C# интерфейс. Разделение интерфейса облегчает использование и тестирование модулей.


    Так как же их разделять-то?

    Что такое зависимость модулей? Это ссылка на модуль в исходном коде, т.е. import, require и т.п. С помощью динамического полиморфизма в runtime можно обратить эту зависимость.


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

    В общем, в книжке дяди Боба все есть, очень подробно и с примерами кода. А это стоит убрать.


    1. mayorovp
      05.02.2018 13:11

      Есть класс Квадрат и есть класс Прямоугольник. Что от чего наследовать?

      Какое отношение этот вопрос имеет к статье?


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


      1. retran
        05.02.2018 13:15

        Ответ правильный, конечно, и, в том числе, содержит в себе ответ на вопрос «Какое отношение этот вопрос имеет к статье?».


        1. mayorovp
          05.02.2018 13:18

          Я все еще не понимаю. Не могли бы вы пояснить?


          1. retran
            05.02.2018 13:25
            +1

            Это классическая задачка на LSP мимо которой очень сложно пройти, если изучать этот принцип нормально, а не на уровне «Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.»

            Например, ее приводит сам Роберт Мартин — web.archive.org/web/20151128004108/http://www.objectmentor.com/resources/articles/lsp.pdf

            А в википедии ей посвященая отдельная страница — en.wikipedia.org/wiki/Circle-ellipse_problem


            1. mayorovp
              05.02.2018 13:28

              Я знаю что это классическая задача, но что вы хотели сказать когда ее приводили?


              1. retran
                05.02.2018 13:41

                Я хотел намекнуть, что осознание автором принципа LSP далеко не полное, и, что не стоит пользоваться его формулировками.


                1. mayorovp
                  05.02.2018 13:45

                  А что не так с его формулировкой и при чем тут задача про квадрат и прямоугольник?


                  1. retran
                    05.02.2018 14:20

                    Ответил ниже — habrahabr.ru/post/348286/#comment_10653644


                1. mkuzmin Автор
                  05.02.2018 13:45

                  1. retran
                    05.02.2018 14:19

                    Ага, более того — это упрощенное определение из исходной статьи Мартина. Но оно плохое, потому что не говорит о инвариантах и ограничениях типа и не дает ответа на вопрос «Как именно правильно организовать иерархию классов, чтобы LSP соблюдался?».

                    Задача про квадрат и прямоугольник — это классическая задача на которой этот вопрос немного проясняют.

                    А в оригинальной статье Лисков тем временем (http://reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.ps) даны очень четкие определения того, что считать инвариантами и ограничениями и как правильно наследовать.


                    1. mayorovp
                      05.02.2018 14:31

                      А определение и не должно говорить как надо делать чтобы его соблюдать.


                      1. retran
                        05.02.2018 14:49

                        А о том, какие условия должны выполняться, чтобы оно соблюдалось?


                        1. mayorovp
                          05.02.2018 14:51

                          Так с ними-то что не так?


                          1. retran
                            05.02.2018 15:18

                            Ок, давайте такой пример:
                            1. Есть иммутабельный тип.
                            2. Есть функция его использующая.
                            3. Наследуем от иммутабельного типа новый — мутабельный (добавляем сеттеры, например).

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

                            По определению Лисков — нет, потому что нарушены инварианты.


                            1. mayorovp
                              05.02.2018 15:28

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

                              Не любая функция, а только та, которая не полагается на инвариант. Та, которая полагается, использовать уже не сможет, потому что поломается.


                              Определение Мартина использует конструкцию "функции… должны иметь возможность" — т.е. речь идет сразу о всех потенциальных функциях принимающих базовый класс.


                              Таким образом, согласно определению Мартина ваш пример нарушает LSP. Не вижу разницы с определением Лисков.


                              1. retran
                                05.02.2018 15:53

                                Открываем Мартина: «FUNCTIONS THAT USE POINTERS OR REFERENCES TO BASE
                                CLASSES MUST BE ABLE TO USE OBJECTS OF DERIVED CLASSES
                                WITHOUT KNOWING IT.»

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


                                1. mayorovp
                                  05.02.2018 16:09

                                  FUNCTIONS… MUST BE ABLE

                                  Все точно так же как и в русском переводе. Функции должны иметь возможность.


                                  Это вы уже дополняете, потому что знаете о чем правило.

                                  Ничего подобного. Оригинальное определение Лисков я узнал уже после определения Мартина (и забыл сразу же как прочитал, потому что определение Мартина проще запоминается).


                                1. mayorovp
                                  05.02.2018 16:17

                                  Кстати, если разобраться — то как раз оригинальное определение Лисков несет неоднозначность, поскольку в нем не говорится какие именно свойства в нем рассматриваются: мгновенные (т.е. инварианты) или свойства поведения объекта.


                                  В первом случае наследование мутабельного класса от иммутабельного не является нарушением LSP, ведь иммутабельность не является инвариантом типа!


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


                                  1. retran
                                    05.02.2018 16:35

                                    Кстати, если разобраться — то как раз оригинальное определение Лисков несет неоднозначность, поскольку в нем не говорится какие именно свойства в нем рассматриваются: мгновенные (т.е. инварианты) или свойства поведения объекта.


                                    Говорится и очень подробно, как и про инварианты, так и про свойства поведения (она называет это constraints и очень подробно и формально определяет).

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


                                    У Лисков как бы вообще нету ни классов, ни интерфейсов. У нее типы и подтипы (а интерфейс, это очевидно тип) с контрактами. Если конкретный язык не позволяет задавать предусловия и постусловия для интерфейса — это не означает, что их можно игнорировать.

                                    В первом случае наследование мутабельного класса от иммутабельного не является нарушением LSP, ведь иммутабельность не является инвариантом типа!


                                    Как бы из предыдущего следует, что если программист написал в комментарии, что класс иммутабельный — это уже constraint и его надо соблюдать. Даже если еще не существует кода, который бы мог этот constraint нарушить.


                                    1. mayorovp
                                      05.02.2018 17:26

                                      Говорится и очень подробно, как и про инварианты, так и про свойства поведения (она называет это constraints и очень подробно и формально определяет)...

                                      … в своей статье, но не в приведенном определении. То есть для того чтобы просто понять определение Лисков — надо идти и читать первоисточники, определение же Мартина более самодостаточное.


                                      У Лисков как бы вообще нету ни классов, ни интерфейсов. У нее типы и подтипы (а интерфейс, это очевидно тип) с контрактами.

                                      То есть определение Мартина более приближено к практическому программированию?


                                      Как бы из предыдущего следует, что если программист написал в комментарии, что класс иммутабельный — это уже constraint и его надо соблюдать.

                                      А если программист написал в комментарии, что класс может быть изменяемым, но реализация таковой не является?


                                      Будет ли в таком случае иммутабельность provable property?


                                      1. retran
                                        05.02.2018 17:49

                                        приведенном определении


                                        Приведенном где и кем?

                                        То есть определение Мартина более приближено к практическому программированию?


                                        Не понимаю из чего сделан такой вывод. Мы не обсуждали применимость тех или иных определений к практическому программированию. Мы обсуждали их корректность и полноту, из чего и следует применимость. Если вы по Мартиновскому определению сами наследуете мутабельный класс от иммутабельного — это уже о нем говорит все что нужно знать.

                                        А если программист написал в комментарии, что класс может быть изменяемым, но реализация таковой не является?


                                        У вас контракт зависит от реализации?

                                        Извините, но я, пожалуй, перестану тратить свое время.


                                        1. mayorovp
                                          05.02.2018 18:09

                                          У вас контракт зависит от реализации?

                                          Нет, у вас. Нет, но меня смущает слово provable.


                                          Приведенном где и кем?

                                          В Википедии.


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

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


                                          Если вы по Мартиновскому определению сами наследуете мутабельный класс от иммутабельного

                                          Пожалуйста, читайте мои сообщения внимательнее, чтобы не читать в них того чего я не писал.


  1. niamster
    05.02.2018 13:08

    LSP: The Liskov Substitution Principle

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

    Мне кажется, что это определение вводит в заблуждение. Это скорее всего применимо к полиморфизму и ducktyping. Да и про функции в определении принципа нет упоминания.

    Речь идет о типах: подтипы базовых типов не должны изменять свойства(как корректность) программы.
    Например, если есть тип «драйвер базы данных», то подтип «драйвер базы данных с резервным копированием» отвечает принципу, а подтип «драйвер базы данных который всегда отбрасывает данные с невалидными полями» может и не подходить. Тут все зависит от рамок «корректности».


  1. coh
    05.02.2018 13:25
    +4

    Яркий заголовок и интригующее описание.

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

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

    Понимание что такое и главное зачем нужно ООП приходит намного позже “начала использования классов”. Понимание SOLID приходит с опытом разработки, запуска и поддержки проектов.

    Полное осознание приходит уже после того, как вы вовсю используете эти принципы.


    1. Atorian
      05.02.2018 15:09

      Мне понравилось как SOLID разжевал Александр Бындю.
      В свое время именно его примеры вызвали тот самый эффект «Ага! Вот оно как!». Может и вам подойдет, как референс для новичков?


  1. nardin
    05.02.2018 15:02

    Мне очень понравилось как SOLID разберается в книге: Паттерны проектирования на платформе .NET

    А вот «Принципы, паттерны и методики гибкой разработки на языке C#» я бы рекомендовал вдумчиво/осторожно читать. Местами она очень странная.


  1. GreedyIvan
    05.02.2018 17:31

    На каждом проекте люди играют разные роли (actor): Аналитик, Проектировщик интерфейсов, Администратор баз данных. Естественно, один человек может играть сразу несколько ролей. В этом принципе речь идет о том, что изменения в модуле может запрашивать одна и только одна роль. Например, есть модуль, реализующий некую бизнес-логику, запросить изменения в этом модуле может только Аналитик, но никак не DBA или UX.

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

    «Бизнес-логика», кто бы её не запрашивал, есть в любом алгоритме. Она и есть «любой алгоритм» по определению. И в SRP речь идет не о том, что попросить поменять алгоритм может только кто-то один, а то, что модуль реализует алгоритм в отношении одной и только одной сущности. Все канонические примеры как раз показывают, что для соблюдения SRP в алгоритме не должно присутствовать действий над другими сущностями.

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


    1. mkuzmin Автор
      05.02.2018 17:31

      Вы читали первоисточники, которые указаны в статье?


      1. GreedyIvan
        05.02.2018 23:10

        Приведенный в книге пример с тремя методами (расчет зарплаты, отчет по часам и сохранение) является просто более расширенной версией канонического примера с печатью отчета.
        Что есть Работник и метод save? Из-за архитектурного разделения на сущность и хранилище, метод save является частью хранилища, для которого Работник используется лишь как объект, который надо сохранить. Поэтому архитектурно неправильно делать метод save в Работнике, который лишь представляет собой структурированные данные.

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

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

        Хорошая IT система представляет собой конфигуратор из различной функциональности. Где запросы внешнего пользователя удовлетворяются соответствующей конфигурацией.


  1. mamitko
    05.02.2018 17:43

    > SOLID критикует тот, кто думает, что действительно понимает ООП

    Не знаю, как у «того», но, например, у меня основная претензия к SOLID в том, что их практически невозможно использовать как руководство «как надо делать», пока ты не созрел до того, чтобы их понять. А когда созрел, такое руководства тебе уже и не нужны.

    И странно, что общественность игнорирует другие четрые буквы: GRASP. Значительно приземленнее и практичней.


    1. mkuzmin Автор
      05.02.2018 17:45

      В том то и дело, что формулировки принципов выглядят довольно странно.
      И осенью вышла книга CleanArchitecture, которая исправляет эти недостатки.


      Цель статьи привлечь внимание и посоветовать ознакомиться с первоисточниками.


  1. retran
    05.02.2018 18:40
    +1

    Открыл книжку Clean Architecture и полистал, соответственно, покритикую статью более предметно.

    SRP:
    Мартин пишет не о участниках проекта, а о стейкхолдерах (на картинках — CFO, COO, CTO и ответственности типа «рассчитать зарплату», «отчитаться о рабочих часах», etc). То есть речь совсем не о DBA, а пользователях и заказчиках системы и их бизнес-процессах. И с этой точки зрения определение имеет смысл, «генерировать отчет для CFO» — это вполне себе ответственность. А вот причем здесь DBA и почему он диктует компонентам ответственности — совершенно непонятно.

    OCP:
    В книжке Мартин оперирует классами и компонентами. Слово «артефакт» используется только в контексте исходной формулировки принципа от Бертрана Мейера от 1988 года, и, очевидно, что это не jar и не dll.

    LSP:
    Здесь больше нет старой формулировки от Мартина. Только исходная формулировка от Лисков.
    Сразу за ней идет раздел под названием GUIDING THE USE OF INHERITANCE и старая добрая проблема square/rectangle. И дальше уже про интерфейсы и их реализации.


    1. mkuzmin Автор
      05.02.2018 19:22

      Круто, что хоть кто-то открыл эту книжку =)
      Еще советую посмотреть видео. Их можно найти.


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


      OCP
      Определение из статьи — цитата из книжки. Трактовка взята из соответствующего видео с cleancoders.com. Артефакт — это файл, содержащий класс в текстовой форме, dll, и т.п.


      LSP
      В основном, эта часть опять взята с cleancoders.com


  1. Cheros
    05.02.2018 19:11
    -1

    По поводу LSP принципа, мне кажется что вы не совсем правильно поняли.
    Вы приводите пример в четвертом абзаце с Circle и Shape. Естественно что Circle нельзя заменить Shape, но этого и не было написано в принципе:

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

    Тут написано о возможности использовать подтип. То есть заменим
    List<Shape>
    на
    List<Circle>
    и никто не умрет, так как Circle точно имеет все методы Shape, но не наоборот. Вы же хотели сделать наоборот, что естественно нарушает этот принцип и не логично по своей природе.


    1. mkuzmin Автор
      05.02.2018 19:14

      Тут речь про то, что модели не обязаны реализовывать отношения объектов реального мира.


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


      1. mayorovp
        05.02.2018 19:25

        но в программной модели с изменяемыми списками это не так.

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


      1. VolCh
        05.02.2018 19:31

        В реальном мире окружностей вообще нет :) С другой стороны, список окружностей является подмножеством, а не подтипом списка фигур. Это у списка окружностей может быть подтип список фигур, а не наоборот. Наследование расширяет множество значений и допустимых операций супертипа, а не сужает его. Вы, конечно, можете его сузить в реализации, но это нарушит LSP :)


        1. mayorovp
          05.02.2018 19:35

          Нет, список фигур не может быть подтипом списка окружностей — это точно так же нарушит LSP.


          1. VolCh
            06.02.2018 13:02

            Чем это его нарушит? В список фигур мы можем добавить окружность? Можем. Из списка фигур мы можем получить окружность или другие фигуры — её подтипы? Можем.


            1. mayorovp
              06.02.2018 13:23

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


              var list = new List<Shape>(); // Список фигур
              list.Add(new Reclangle()); // Разрешено, потому что прямоугольник - фигура
              func(list);
              
              void func(List<Circle> list) { // Функция принимает список окружностей
                  Circle c = list[0]; // БАМ! Там лежал прямоугольник.
              }


              1. VolCh
                06.02.2018 13:34

                // БАМ! Там лежал прямоугольник.

                А прямоугольник у меня подтип окружности :)


                1. mayorovp
                  06.02.2018 13:35

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


                  1. VolCh
                    06.02.2018 13:40
                    -1

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


    1. mayorovp
      05.02.2018 19:22

      List<Shape> на List<Circle> заменять точно так же нельзя, потому что List<> — инвариантный обобщенный тип.


      Смотрите что было бы, если бы было можно:


      void func(List<Shape> list) {
          list.Add(new Rectangle());
      }
      
      var list = new List<Circle>(); // В Java будет new ArrayList<Circle>();
      foo(list);
      Circle c = list[0]; // БАМ! Прямоугольник - не круг.


      1. mkuzmin Автор
        05.02.2018 19:24

        Именно!


      1. Cheros
        05.02.2018 19:33
        -2

        class Man {
            public void eat() { ... }
        }
        
        class Asian extends Man {
            //...
        }
        
        class European extends Man {
            //...
        }
        
        // метод для всех
        // Каждый человек может есть, поэтому 
        // Man можем заменить на Asian или European 
        void method(Man man) {
            man.eat();
        }
        


        1. mayorovp
          05.02.2018 19:34

          А куда вы List дели?


          1. Cheros
            05.02.2018 19:44
            -2

            Ребята, в том и прикол что если в вашем коде нельзя заменять базовый тип подтипом, то это ваша вина и стоит вспомнить принцип LSP и переписать код.

            void func(List<Shape> list) {
                list.Add(new Rectangle());
                list.Add(new Circle());
            }
            // Всё ок добавили Rectangle и Circle
            // В дальнейшем коде пользуемся лишь теми методами которые доступны Shape,
            // т.е. вызов list[0].getRadius() будет нарушением принципа LSP, т.к. радиус
            // только у окружности или круга.
            // Пользуемся методами доступными всем, например абстрактный метод draw().
            // Каждая фигура может "нарисоваться". Вот и соблюдение принципа.
            


            1. mayorovp
              05.02.2018 19:48

              Нет, вы путаете LSP и статическую типизацию.


              LSP — это требования к классам и их контрактам, а не к коду который их использует. Вызов list[0].getRadius() не может нарушить LSP в принципе. Но LSP может нарушить сама возможность такого вызова.


  1. vba
    05.02.2018 20:12

    Спасибо за статью, такой вопрос по LSP. Вы сказали что


    И есть потомок DoubleStack, который дублирует добавляемые элементы. Естественно, класс DoubleStack нельзя использовать вместо Stack.

    С этим все понятно, но хотелось бы узнать, кто нарушает в таком случае принцип LSP, тот кто его так реализовал или тот кто его так использует? Если первый вариант правильный то для ООП языков это означало бы — избегайте глобальных абстракций (List, Hash, etc). Спасибо.


    1. mkuzmin Автор
      05.02.2018 20:16

      Виноват тот, кто реализовал DoubleStack.


      избегайте глобальных абстракций (List, Hash, etc)

      Несколько странный вывод =)
      Хорошо бы пользоваться соответствующими интерфейсами, а для их выбора поможет ISP.


    1. VolCh
      06.02.2018 13:33

      На самом деле не факт, что LSP нарушается, если в контракте Stack не заявлено, что pop добавляет один и только один элемент, увеличивая length на 1. Если клиенты Stack не делают предположений как работает стэк, не ожидают, что


      l = stack.length();
      stack.push(a);
      stack.push(b);
      assert(l + 2 === stack.length();
      assert(b === stack.pop());
      assert(a === stack.pop()); 

      то нарушения нет.


      1. mayorovp
        06.02.2018 13:36

        Но это будет очень странный контракт стэка…


  1. Eldhenn
    05.02.2018 21:41

    > На старте проекта, мы знаем, что будем использовать реляционную базу данных, и это точно будет Postgresql, а для поиска — ElasticSearch. Мы даже не планируем их менять в будущем.

    Хочу немного дополнить. В будущем может оказаться, что НЕ ВСЕ части проекта используют одну и ту же бд, одну и ту же субд, один и тот же принцип хранения данных. То есть не то, чтобы мы что-то меняли глобально, но внезапно появилась подсистема, в которую часть данных будет поступать из 1С…


  1. bro-dev
    06.02.2018 01:35

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


    1. Naglec
      06.02.2018 01:46

      Да глупости это все, не обращайте внимание.


    1. mayorovp
      06.02.2018 05:52

      А эти принципы и не должны удешевлять разработку, их цель — удешевить сопровождение.


    1. VolCh
      06.02.2018 13:37

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


  1. GreedyIvan
    06.02.2018 14:03
    +1

    Классический пример нарушения. Есть базовый класс Stack, реализующий следующий интерфейс: length, push, pop. И есть потомок DoubleStack, который дублирует добавляемые элементы. Естественно, класс DoubleStack нельзя использовать вместо Stack.

    Вообще-то может, и никаким нарушением принципа это не будет.
    Что в этом случае поламается? То, что зависело от никак незаконтрактованной логики использования базового класса.

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


    1. mayorovp
      06.02.2018 14:28

      Обычно стеком все же называют структуру данных с хорошо известным поведением — FILO (First In — Last Out) без дублирования и пропадания элементов. Даже если стек не финализирован — DoubleStack все равно будет ошибкой, только не компиляции а проектирования.


      1. ookami_kb
        07.02.2018 14:31
        +1

        Обычно он LIFO называется.