SOLID

    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 способствуют откладыванию принятия технических решений и разделению труда программистов.

    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 150
      –2

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


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

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

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

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

                +1

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

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

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

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

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

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


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

                        0
                        Ну уменьшите количество строк до 500, вероятность того что изменения запрошены от одной роли будут намного выше.
                        Если вы пишете обычно, значит описанная мной ситуация возможна.
                        Так все же такой метод соответствует SRP?
                          +1
                          Точный ответ на этот вопрос возможен только для конкретного метода, а вы его не привели.
                            0
                            А что вам даст код? Что вы там хотите увидеть?
                            Ответ на критерий по которому автор определяет соответствие кода принципу SRP я дал.
                              +1
                              Я вам уже написал: метод на 3к строк может как удовлетворять этому критерию, так и нет. Что еще вы хотите услышать?
                          0
                          Сомневаюсь. Если у вас есть метод, который реализует state machine для всех возможных бизнесс процессов в компании, то вносить изменения в него будет бизнесс аналитик, только вот сопровождать его будет тем еще счастьем.
                      0
                      Чем больше строк в методе, тем больше вероятность, что в нём есть дополнительные ответственности, но это лишь вероятность.
                        0
                        Именно. Плюс все еще ухудшается тем, что ответственный то с ролью может быть один, но заниматься он может большим количеством вещей. Метод формально удовлетворяющий требованию будет кошмаром.
                    +2
                    Зачем же тогда отдельно выделены dependency inversion principle?

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

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

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

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

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

                        0
                        говорится что «запросить изменения в этом модуле может только Аналитик». Но если никто не может попросить актора что-то сделать, то как тогда он понимает что нужно что-то вызвать?
                          0
                          Вас кто-то просил писать этот комментарий? Если нет — то как вы поняли что вам это нужно сделать?
                            0
                            А играющий роль может просить другого играющего роль что-то сделать?
                            То есть два других актора могут попросить одного Аналитика что-то сделать?
                              0

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

                                +1

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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


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

                                      +6

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

                                        0

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


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


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


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

                                          +3

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


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

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


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

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

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


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


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


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

                                          –1

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


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

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

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

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

                                            Ну а по поводу потратить время\деньги — нет ничего хуже, чем проигнорировать процесс сбора требований. Если не изучать вопрос хоть как-нибудь, вы рискуете потратить гораздо больше, чем потратили бы на ресерч.
                                            +1
                                            Это типичный путь архитектора, который приводит к перерасходу бюджета. Цель продукта — заработать денег. Значит надо как можно скорее произвести конкретную реализацию конкретного бизнесс процесса преподнесенного как тезис, получить подтверждение тезиса через прибыль (или другие измеряемые метрики), реализовать новую версию продукта (возможно выбросив, но лучше дополнив оригинальную — OpenClose (не правим, а дополняем… или выбрасываем)).
                                            0
                                            Не надо нам точно знать как раз. А даже если точно уже знаем, то нужно абстрагироваться от этого знания, хоть как-то, пускай даже эта абстракция прячет разницу между постгри 9.5 и 9.6.
                                              0
                                              Что не надо это понятно, на то это и есть абстракция. Если посмотрите на тот текст что я цитировал, то там «знание» указано чуть ли не как требование для составления абстракции. Но, я уже понял, что все понял правильно. Просто в статье не понятно зачем на этом сделан акцент.
                                                0

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

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

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

                                                      0

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


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


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

                                                        0

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

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

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


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

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

                                                                  0

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

                                                                    0

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


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

                                                                      +1

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


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

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

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

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

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

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

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

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


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

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

                                                                              0

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

                                                                                0

                                                                                В плохой понимают только S и забывают про Cohesion. Лучше бы лапшу писали. Рефакторить её проще...

                                                                              +2

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

                                                                                +1

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

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

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

                                                                              +1

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

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

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

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

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


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

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


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

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


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

                                                  LSP: The Liskov Substitution Principle


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

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


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

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


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

                                                  В общем, в книжке дяди Боба все есть, очень подробно и с примерами кода. А это стоит убрать.
                                                    0
                                                    Есть класс Квадрат и есть класс Прямоугольник. Что от чего наследовать?

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


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

                                                      0
                                                      Ответ правильный, конечно, и, в том числе, содержит в себе ответ на вопрос «Какое отношение этот вопрос имеет к статье?».
                                                        0
                                                        Я все еще не понимаю. Не могли бы вы пояснить?
                                                          +1
                                                          Это классическая задачка на LSP мимо которой очень сложно пройти, если изучать этот принцип нормально, а не на уровне «Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.»

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

                                                          А в википедии ей посвященая отдельная страница — en.wikipedia.org/wiki/Circle-ellipse_problem
                                                            0
                                                            Я знаю что это классическая задача, но что вы хотели сказать когда ее приводили?
                                                              0

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

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

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

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

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

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

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

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

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


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


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

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

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

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


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

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

                                                                                  0

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


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


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

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


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

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


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

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


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

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


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

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


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

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


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

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


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

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


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

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


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

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

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


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

                                                                                          В Википедии.


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

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


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

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

                                                      0
                                                      LSP: The Liskov Substitution Principle

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

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

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

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

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

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

                                                        Полное осознание приходит уже после того, как вы вовсю используете эти принципы.
                                                          +1
                                                          Мне понравилось как SOLID разжевал Александр Бындю.
                                                          В свое время именно его примеры вызвали тот самый эффект «Ага! Вот оно как!». Может и вам подойдет, как референс для новичков?
                                                          0
                                                          Мне очень понравилось как SOLID разберается в книге: Паттерны проектирования на платформе .NET

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

                                                                  0

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


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


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


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

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

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

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


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

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

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

                                                                        0

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

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

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

                                                                              0

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


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

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

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

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

                                                                        0

                                                                        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]; // БАМ! Прямоугольник - не круг.
                                                                          0

                                                                          Именно!

                                                                            –3
                                                                            class Man {
                                                                                public void eat() { ... }
                                                                            }
                                                                            
                                                                            class Asian extends Man {
                                                                                //...
                                                                            }
                                                                            
                                                                            class European extends Man {
                                                                                //...
                                                                            }
                                                                            
                                                                            // метод для всех
                                                                            // Каждый человек может есть, поэтому 
                                                                            // Man можем заменить на Asian или European 
                                                                            void method(Man man) {
                                                                                man.eat();
                                                                            }
                                                                            
                                                                              +1
                                                                              А куда вы List дели?
                                                                                –2
                                                                                Ребята, в том и прикол что если в вашем коде нельзя заменять базовый тип подтипом, то это ваша вина и стоит вспомнить принцип LSP и переписать код.
                                                                                void func(List<Shape> list) {
                                                                                    list.Add(new Rectangle());
                                                                                    list.Add(new Circle());
                                                                                }
                                                                                // Всё ок добавили Rectangle и Circle
                                                                                // В дальнейшем коде пользуемся лишь теми методами которые доступны Shape,
                                                                                // т.е. вызов list[0].getRadius() будет нарушением принципа LSP, т.к. радиус
                                                                                // только у окружности или круга.
                                                                                // Пользуемся методами доступными всем, например абстрактный метод draw().
                                                                                // Каждая фигура может "нарисоваться". Вот и соблюдение принципа.
                                                                                
                                                                                  0

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


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

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

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

                                                                          0

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


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

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

                                                                            0

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


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

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

                                                                              0

                                                                              На самом деле не факт, что 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()); 

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

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

                                                                              Хочу немного дополнить. В будущем может оказаться, что НЕ ВСЕ части проекта используют одну и ту же бд, одну и ту же субд, один и тот же принцип хранения данных. То есть не то, чтобы мы что-то меняли глобально, но внезапно появилась подсистема, в которую часть данных будет поступать из 1С…
                                                                                0
                                                                                Есть какое то статистическое обоснование того что такие принципы удешевляют разработку? Со стороны кажется что программисты как фанатики следуют каким то правилам, потому что так делают более крутые программисты, или потому что так в умных книжках написано.
                                                                                  0
                                                                                  Да глупости это все, не обращайте внимание.
                                                                                    +1
                                                                                    А эти принципы и не должны удешевлять разработку, их цель — удешевить сопровождение.
                                                                                      0

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

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

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

                                                                                      Вот если у нас законтрактован результат работы цепочки вызовов push и pop, то и потомок обязан будет выполнять этот контракт. Иначе он не потомок. Позволяют ли средства языка описывать подобные контракты — это другой вопрос. Как частное решение для классов, в контракте которых подразумевается конкретная логика их использования, языки могут предоставлять возможность их финализации, на уровне синтаксиса запрещая создавать от них потомки.
                                                                                        0
                                                                                        Обычно стеком все же называют структуру данных с хорошо известным поведением — FILO (First In — Last Out) без дублирования и пропадания элементов. Даже если стек не финализирован — DoubleStack все равно будет ошибкой, только не компиляции а проектирования.
                                                                                          +2

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

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

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