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)
solver
05.02.2018 11:49-1Как так получается? «Придумал принципы SOLID Роберт Мартин (Uncle Bob)», но как минимум один из принципов называется «LSP: The Liskov Substitution Principle»?
Вам не кажется, что вы не разобравшись в первоисточниках вводите людей взаблуждение?SamDark
05.02.2018 12:34+1Придумал, а точнее взял Coupling/Cohesion, вывел из них красивые буквы SOLID и сделал их модными именно Роберт Мартин. Тут ошибки нет.
solver
05.02.2018 12:55Ну то, что он придумал название для существующих принципов никто не спорит)
В статье же сказано, что он придумал сами принципы, а это не соответсвует действительности.
ookami_kb
06.02.2018 13:26Вот как раз красивые буквы SOLID вывел Michael Feathers
SamDark
06.02.2018 13:51Похоже на то, но вообще это одна контора же, ThoughtWorks. Они зарабатывают на консалтинге и им нужно было чтобы старый добрый cohesion/coupling стал новым модным SOLID.
a4k
05.02.2018 11:52SRP: The Single Responsibility Principle
То есть если метод состоит из 3k+ строк он соответствует этому принципу главное что бы изменения запрашивались от одной роли?
В этом принципе речь идет о том, что изменения в модуле может запрашивать одна и только одна роль. Например, есть модуль, реализующий некую бизнес-логику, запросить изменения в этом модуле может только Аналитик, но никак не DBA или UX.
OCP: The Open Closed Principle
Зачем же тогда отдельно выделены dependency inversion principle?
…
Используя dependency inversion, наш модуль объявляет только интерфейс отправки уведомлений, но не реализацию.mayorovp
05.02.2018 12:04То есть если метод состоит из 3k+ строк он соответствует этому принципу главное что бы изменения запрашивались от одной роли?
Да, такое возможно. Но маловероятно — обычно методы такого размера притягивают к себе дополнительные ответственности в процессе эволюции кода.
a4k
05.02.2018 12:54Автор исключил дополнительную ответственность из принципа
mayorovp
05.02.2018 13:07Там ее и не было…
a4k
05.02.2018 13:12Да, такое возможно. Но маловероятно — обычно методы такого размера притягивают к себе дополнительные ответственности в процессе эволюции кода.
Это же вы написалиmayorovp
05.02.2018 13:15Если автор не использует какого-то термина — то мне его тоже нельзя использовать? Хорошо, переформулирую.
Обычно методы такого размера притягивают к себе изменения от разных ролей.
a4k
05.02.2018 13:26Ну уменьшите количество строк до 500, вероятность того что изменения запрошены от одной роли будут намного выше.
Если вы пишете обычно, значит описанная мной ситуация возможна.
Так все же такой метод соответствует SRP?mayorovp
05.02.2018 13:27Точный ответ на этот вопрос возможен только для конкретного метода, а вы его не привели.
boblenin
06.02.2018 00:10Сомневаюсь. Если у вас есть метод, который реализует state machine для всех возможных бизнесс процессов в компании, то вносить изменения в него будет бизнесс аналитик, только вот сопровождать его будет тем еще счастьем.
VolCh
05.02.2018 15:21Чем больше строк в методе, тем больше вероятность, что в нём есть дополнительные ответственности, но это лишь вероятность.
boblenin
06.02.2018 00:13Именно. Плюс все еще ухудшается тем, что ответственный то с ролью может быть один, но заниматься он может большим количеством вещей. Метод формально удовлетворяющий требованию будет кошмаром.
SamDark
05.02.2018 12:36+2Зачем же тогда отдельно выделены dependency inversion principle?
Потому что не получилась бы красивая аббревиатура :) В SOLID часть принципов прилично накладываются на остальные.
vasIvas
05.02.2018 11:56-1Например, есть модуль, реализующий некую бизнес-логику, запросить изменения в этом модуле может только Аналитик, но никак не DBA или UX.
Тогда, если двум объектам понадобится изменить один объект, то они попросят сделать это Аналитика.
А разве это не нарушит принцип, так как аналитика будут дергать сразу двое?
И вроде уже давно разобрались что именно эта формулировка как раз и не верная…mayorovp
05.02.2018 11:59Объект не может о чем-то попросить Аналитика, потому что Аналитик — роль в команде, а не объект и не модуль :-)
vasIvas
05.02.2018 14:58говорится что «запросить изменения в этом модуле может только Аналитик». Но если никто не может попросить актора что-то сделать, то как тогда он понимает что нужно что-то вызвать?
mayorovp
05.02.2018 15:00Вас кто-то просил писать этот комментарий? Если нет — то как вы поняли что вам это нужно сделать?
vasIvas
05.02.2018 16:32А играющий роль может просить другого играющего роль что-то сделать?
То есть два других актора могут попросить одного Аналитика что-то сделать?VolCh
05.02.2018 18:48+1Могут. Но к разработчику он должен обратиться не в форме "меня тут попросили...", а в форме "сделай мне...". А разработчик ему может ответить "не буду, это не твой уровень абстракции" :)
boblenin
06.02.2018 00:16Все верно, но «сделай мне» говорит не аналитик, а «product owner» хотя бы в SCRUM. Который занимается управлением бэклогом. А значит, согласно, данному в статье определению — фигачим в метод все что хотим, все-равно придет все через того же самого «product owner»-a.
VolCh
06.02.2018 13:13Не совсем. Если говорить о скраме, и вообще о роялях в создании и эксплуатации инженерных систем, то в данной интерпретации у каждой сущности системы должен быть только один стейкхолдер.
boblenin
06.02.2018 16:17Да это понятно. Только вот как вы представляете трансляцию пожеланий скажем от 4-го клерка 128-го отделения банка? Должен ли product owner в user story писать кто источник информации? Всегда ли product owner будет это знать?
Это создает такой избыточный геморой на всем этапе анализа, что не в сказке сказать. А все ради того чтобы программист чуть-чуть по-другому структурировал код. Сдается мне — это фантастика.VolCh
07.02.2018 12:06Вряд ли 4-й клерк 128-го отделения банка вообще знает о существовании product owner. Непосредственному начальству сообщит или в саппорт. И так или иначе пожелание дойдёт до стейкхолдера (например, директора операционного департамента), который либо передаст его product owner, либо получит от него это пожелание для утверждения/отклонения.
boblenin
07.02.2018 18:05Ну так у директора пожеланий может быть в стольких областях. 10 экранами логики в одном модуле не обойдетесь.
VolCh
07.02.2018 18:14Тут речь не о том, что все пожелания директора опердепа в одном модуле, а о "списке" модулей, изменения в которые вносятся только с его подачи с одной стороны, а, с другой, отклоняются, если они относятся к другому модулю. Ну или принимаются другие меры, оставляющие за каждым стейкхолдером его "личные" модули, например, перераспределение обязанностей в бизнес-процессах или переразбиения на модули.
boblenin
07.02.2018 18:32Ну вот я вчера только с director of operations спорил по поводу требований пришедших от него. В одном тикете все от изменения биллинга, до флагов в базе данных и отправки и формата email-ов.
Согласно такой логики комманда должна написать некий объект, который будет заниматься биллингом, нотификациями и управленем базой данных.VolCh
07.02.2018 18:46Ну, скорее или кучу объектов таких в каждом из модулей, которая (куча) будут принадлежать ему (директору), или отсылать его (хорошо звучит...) к стейкхолдерам модулей в которые он хочет влезть.
boblenin
07.02.2018 21:13Так ведь согласно описанию необходимым условием является один стэйкхолдер. Директор Опс-ов. По закону Мерфи разработчик сделает все одним объектом. В лучшем виде с dependency injection, в виде command pattern.
Объявит интерфейс, со 100500 зависимостями и единственным методом Run(). Сделает одну уникальную его реализацию в которую запихнет вызов репозитория, который в свою очередь тоже будет реализовывать Command pattern. И после 10 вызовов зависимостей, которые только и делают что вызывают зависимости — внизу будет хранимая процедура, которая посчитает биллинг, проставит флаги и отправит email. И вот эта процедура и будет соответствовать SOLID.
Сдается мне, что в статье на задана достаточность условия.
mayorovp
06.02.2018 13:25Хоть Product owner и управляет бэклогом, но сами задачи в бэклоге исходят не от него.
AbstractGaze
05.02.2018 11:57-2На старте проекта, мы знаем, что будем использовать реляционную базу данных, и это точно будет Postgresql, а для поиска — ElasticSearch.
На старте это хорошо, и как часто то, что, на старте доходит до конца проекта? Вы на старте привязываетесь, и не можете ничего поменять потому что:
1. это SOLID
2. это потребует больших затрат времени
3. выбросить уже написанное, а это время/деньги.
Что по этому принципу можно писать? программы? продукты? или системы? Если только программы то тогда еще более менее понятно. Но продукты и системы пишутся не один месяц, и много меняется. Как вы по этим принципам подстраиваетесь под реалии и новые требования клиентов?c4boomb
05.02.2018 12:19+1Вы бы сначала почитали
SOLID говорит строить зависимости на абстракциях.
Ваши репозитории зависят на AbstractPersistor или PersistorInterface.
В начале реализовали PostgreSQLPersistor который имплементирует абстракцию. Захотелось использовать MariaDB имплементировали MariaPersistor и с помощью dependency injection подменили.
SOLID вам только помогает в этом вопросе
Atorian
05.02.2018 12:27+2В том то и дело, что следование этим принципам минимизирует такие работы. Достаточно будет переписать низкоуровневые классы доступа в БД. При этом бизнес логика и логика самого приложения будут не затронуты.
А по поводу 3-го пункта — так надо иногда делать, ибо выбранная в начале пути технология может не обеспечить удовлетворения новых требований, предъявляемых проекту. Если этого не сделать — вы просто навредите проекту.
Технологии не приколачиваются гвоздями к решению.AbstractGaze
05.02.2018 12:32Тогда я совсем не понял, зачем нам надо точно знать что мы будем использовать, ведь по большому счету мы можешь использовать в начале просто заглушку для бд? И почему мы не можем запланировать замену в будущем, если все достаточно легко меняется?
Можно этот момент пояснить?mayorovp
05.02.2018 12:34Попробуйте прочитать процитированный вами абзац целиком, а не только первое предложение.
mkuzmin Автор
05.02.2018 12:37История следующая. Обычно все говорят, зачем тебе абстракции, если ты и так знаешь что будешь использовать условный Postgres. У тебя вопрос — полная противоположность.
Для начала работы как раз нужно использовать заглушку, но понимать, что примерно будет использоваться потом. И спроектировать адекватные абстракции.
Если ты ожидаешь, что будет использоваться реляционная база, а потом окажется, что можно использовать только key-value, то абстракции нужно будет менять и переписывать проект.
SamDark
05.02.2018 12:43Откладывание слишком большого числа технических решений ведёт к страшному усложнению взаимодействия простых вроде бы компонентов (переслоёности), увеличению времени на разработку прототипа, повышению порога входа в проект, невозможности использования уникальных для конкретной реализации фич и другим гадостям. К тому же, абстракции постоянно текут.
mkuzmin Автор
05.02.2018 12:56Можно провести такую аналогию. Заказчик говорит: Нужно отправить письмо, если (тут набор бизнес требований). Т.е. он не говорит, я хочу отправить письмо через Mailchimp. Он закрывается абстракцией и откладывает решение о реализации.
Про проводу протекания абстракций. Текут абстракции, вроде TCP, которые обязуются обеспечить надежность поверх ненадежных технологий. Если выдернуть провод, TCP естественно не доставит пакет.
Все зависит от архитектора и его компетенций. Само-собой можно такого напроектировать.
Просмотри первоисточники, там подробно разобраны твои вопросы.
SamDark
05.02.2018 13:06В случае письма у нас:
- Требования ясны с самого начала.
- Абстракция довольно проста (стоимость внедрения стремится к нулю).
- Мотивация смены последующего способа отправки, как и его вероятность высока и понятна.
Очевидно, что тут не сделать небольшую абстракцию — преступление.
В случае с той же СУБД всё уже не так просто.
mkuzmin Автор
05.02.2018 13:11Про случай СУБД, советую посмотреть https://github.com/darkleaf/building-application
SamDark
05.02.2018 13:26Да ясно, что clean architecture и всё такое. Это не единственный подход к проектированию.
AbstractGaze
05.02.2018 12:44Меня скорее смутило другое. Допустим я создаю абстракцию, но при этом до ее создания я должен точно знать что буду использовать (как написано в статье)
Чтобы точно знать, я должен решить и возможно изучить вопрос — т.е. потратить время/деньги. В случае же применения заглушки сразу — я не буду тратить время. Знать, что мне там точно нужно, получиться само собой после реализации логики и т.п.
Или я все равно что, то недопонимаю?mkuzmin Автор
05.02.2018 12:50Чтобы вставить заглушку — нужно объявить интерфейс(абстракцию).
Давай пример попроще.
Допустим, нужно отправлять уведомления.
Мы объявляем интерфейс вродеIMailSender
с одним методом voidSend(address, title, content)
.
Далее описываем бизнес-логку. В тесах используем mock/stub/fake object с интерфейсом IMailSender.
Нам важно примерно знать что будет, а не точно.
Найди книжку Clean Architecture, там все доступно объясняется.
mkuzmin Автор
05.02.2018 12:57посмотри еще вот это: https://github.com/darkleaf/building-application
там пример на clojure, но все доступно объясняется
Atorian
05.02.2018 15:02Так в этом и прелесть абстракций. Вам не надо знать что именно вы будете использовать. Заглушка === Interface === Абстракция. Мне кажется вы все правильно поняли.
Другое дело, что не все абстракции одинаково полезны.
Про ту же СУБД. Мы можем ввести абстракцию TableGateway или Repository. Обе они прячут имплементацию БД. Но первая, скорее всего, треснет, как только надо будет поменять тип хранилища с реляционого на ключ-значение. Но никаких проблем в смене MySQL на другую SQL базу не возникнет. А Repository будет жить даже в любом случае, но нужен ли он в приложении — другой вопрос.
Ну а по поводу потратить время\деньги — нет ничего хуже, чем проигнорировать процесс сбора требований. Если не изучать вопрос хоть как-нибудь, вы рискуете потратить гораздо больше, чем потратили бы на ресерч.
boblenin
06.02.2018 00:21Это типичный путь архитектора, который приводит к перерасходу бюджета. Цель продукта — заработать денег. Значит надо как можно скорее произвести конкретную реализацию конкретного бизнесс процесса преподнесенного как тезис, получить подтверждение тезиса через прибыль (или другие измеряемые метрики), реализовать новую версию продукта (возможно выбросив, но лучше дополнив оригинальную — OpenClose (не правим, а дополняем… или выбрасываем)).
VolCh
05.02.2018 15:27Не надо нам точно знать как раз. А даже если точно уже знаем, то нужно абстрагироваться от этого знания, хоть как-то, пускай даже эта абстракция прячет разницу между постгри 9.5 и 9.6.
AbstractGaze
05.02.2018 15:46Что не надо это понятно, на то это и есть абстракция. Если посмотрите на тот текст что я цитировал, то там «знание» указано чуть ли не как требование для составления абстракции. Но, я уже понял, что все понял правильно. Просто в статье не понятно зачем на этом сделан акцент.
VolCh
05.02.2018 18:50Я его понял по другому: мы уже знаем что будет за абстракцией 100% ближайшие 10 лет, но всё равно лучше ввести абстракцию.
boblenin
06.02.2018 00:22Лучше сделать работающий продукт.
VolCh
06.02.2018 13:14Введение абстракции никак не влияет будет работать первая версия или нет. Ну, считая, что абстракция применена правильно.
SamDark
06.02.2018 14:07Смотря что абстрагировать. Взять те же СУБД. Подход с репозиториями верный, но не самый быстрый. Ну, допустим, сделаем.
В реализации репозиториев завязываться на конкретную СУБД по полной и фигачить внутри SQL? А если заменить надо будет через 10 лет… хммм… начинаем смотреть на PostgreSQL и MySQL. Понимаем что мало того, что придётся прижаться и юзать SQL99 и выкинуть половину крутых фич конкретной базы сразу (и это верно для большинства готовых data mapper-ов и AR), так ещё и не совсем тривиальные обёртки написать для типов данных, JSON и так далее.
Итого время на реализацию такой обёртки составляет треть от времени реализации начальной версии проекта… а может всё-таки зафиксировать что у нас будет всегда PostgreSQL и не страдать фигнёй?
VolCh
06.02.2018 14:32Мы можем создать абстракцию, максимально приближенную к целевой реализации, например, вынося в интерфейс возможности PostgreSQL, которых в обозримом будущем в MySQL не появится. Но всё-таки абстрагируясь от деталей реализации этой возможности PostgreSQL, например
Repository.getNextId => this.query('SELECT next_val('seq_name'))
SamDark
06.02.2018 15:11- Целевая реализация 10 лет не будет стоять на месте.
- Если отталкиваться только от PostgreSQL, переезд, например, на MySQL обернётся такими костылями, что лучше и не пытаться.
VolCh
06.02.2018 15:46- Альтернативная тоже не будет, например в MySQL появилась поддержка JSON. С другой стороны, какая-то используемая функциональность может быть объявлена deprecated и потом вообще выпилена, с предложением более эффективного альтернативного способа получения того же результата. А у нас получение результата за абстракцией, в одном месте (в идеале) точечено меняем реализацию и всё.
- А на Oracle можно и попытаться.
boblenin
06.02.2018 16:21+1Если у вас все приложение — это CRUD с напылением бизнесс логики, то начав с репозиториев в первой версии, до миграции на MySQL можно и не дожить. Проект закроют из-за слишком больших затрат.
VolCh
07.02.2018 12:14+1По опыту, введение репозиториев практически пренебрежимо увеличивает затраты на начальную разработку в двух случаях:
- используемая инфраструктура хорошо заточена под них
- не предпринимаются попытки написать универсальные реализации, просто обычный SQL-код или обращения к NoSQL хранилищам, а также какие-то ORM/ODM прячутся за фасадом репозитория, не оперирующего терминами выбранной системы хранения или её классом, а оперирующего терминами абстрактного хранилища.
Естественно, в предположении что члены команды достаточно хорошо понимают что это такое и как его реализовывать, пускай даже большинство не понимают зачем.
SamDark
07.02.2018 13:10Да, так будет работать и в этом есть смысл, но это далеко не "легко заменить одну базу на другую", как любят приводить в пример.
VolCh
07.02.2018 13:16Относительно легко. Как минимум не нужно будет рыскать по всему коду приложения в поисках SQL :)
А примеры на то и примеры, чтобы лишь демонстрировать идею на понятных всем вещах.
SamDark
07.02.2018 13:20Нужно будет рыскать по всем репозиториям, да. Это лучше, чем вообще везде.
Как по мне, пример с заменой СУБД — это один из самых плохих примеров потому как в нём не ясно и спорно всё от и до. Вот пример с отсылкой почты или SMS прост, понятен, мало затратен и имеет смысл в подавляющем большинстве случаев.
boblenin
07.02.2018 18:24Именно. Причем зачастую репозитории либо выраждаются в обертки над ORM, либо наоборот покрываются кучей бизнесс логики. Либо еще хуже — логика уходит на уровень хранимых процедур.
И тогда надо будет рыскать по всем репозиториям, всем процедурам и всему коду, который все это вызывает.
VolCh
07.02.2018 18:41+1Согласен. СУБД слишком сложные, чтобы просто получить легкую замену одной на другой, используя их максимально полно. А частота этого примера провоцирует множественное создание велосипедов, типа это же легко, сейчас замутим.
boblenin
07.02.2018 18:21+1Будет относительно не легко понять почему все тормозит и как это оптимизировать.
boblenin
07.02.2018 18:20+1> Естественно, в предположении что члены команды
> достаточно хорошо понимают что это такое и как его
> реализовывать, пускай даже большинство не понимают
> зачем.
Если у нас есть идеально работающая комманда, в которой все разработчики хорошо обучены и сработаны и плюс под управлением тех. лида, который знает что делает — то как мне сдается даже без формализованых SOLID принципов все будет хорошо. Даже без следования оным их код будет функциональным и поддерживаемым.
В других случаях репозитории — не гиря на ногах, конечно. Но как минимум наручник пристегнутый на икру.VolCh
07.02.2018 18:38Ну, принципы SOLID как бы и направлены на улучшение поддерживаемости. К ним, как и к паттернам нередко приходишь сам, а потом узнаешь, что всю жизнь разговаривал прозой :) Главная заслуга их создателей, что они дали и популяризировали краткие и запоминающиеся имена решениям, к которым многие приходят методом проб и ошибок.
Я бы тупой репозиторий с кучей findBy… методов сравнил с рукавицей, а с поддержкой полноценных критериев — с перчаткой.
boblenin
07.02.2018 19:11+1Ну так мы с вами договорились (вы начали, а я поддержал) до того, что принципы SOLID работают только в рамках хорошей комманды. В рамках которой все будет работать.
VolCh
08.02.2018 18:21Я немного не то имел в виду. В хорошей команде скорее всего SOLID принципы в большой мере соблюдаются даже без формальных знаний о них. В "плохой" их надо внедрять "из под палки", чтобы обеспечить приемлемое качество кода.
SamDark
07.02.2018 19:17+1"Рукавица" зачастую лучше. У "перчатки" внезапно может оказаться слишком много вариантов её конфигурации и логика конфигурирования полезет вон из репозитория.
VolCh
08.02.2018 18:28Не спорю. Собственно "перчатки" обычно использую только в каких-то особых случаях, и, да, обычно она дырявая получается :)
SamDark
06.02.2018 16:33- А не достаточно ли нам в этом случае репозиториев? Да, поправить, возможно, придётся не в одном месте, а в 10-20, но это не так затратно, как разработка хорошего слоя абстракции реляционной базы (в рамках одного проекта это не выглядит реалистичным в принципе).
- Тоже есть отличия, но да. Тут уже попытка не будет столь болезненной, если не очень сильно отходить от SQL99.
VolCh
07.02.2018 12:26- Не очень понял. Имеете в виду создание конкретных репозиториев без реализации ими абстрактного интерфейса репозитория? Если так, то, с одной стороны, это незначительно экономит время по сравнению с реализацией, с другой — лишь незначительно экономит, а защищает от протечек абстракции лучше.
SamDark
07.02.2018 13:08+1Имею ввиду не абстрагировать внутренности метода репозитория никак. То есть использовать там SQL или пользоваться простой готовой обёрткой. Не пытаться сделать аналог DQL или HQL, стараясь абстрагировать специфичные фичи PostgreSQL на низком уровне.
VolCh
07.02.2018 13:21Это, конечно, да. Для подавляющего большинства проектов это будет очень неоправданным расходованием ресурсов. В целом я о более высоуровневых абстракциях. На низком уровне или использовать что-то готовое, или писать низкоуровневый код, спрятанный за достаточно высоким уровнем абстракции. Или комбинировать, например, для простого CRUD брать ORM в качестве основной реализации репозитория, а для тяжелых вещей типа отчётов писать непосредственную работу с базой.
boblenin
07.02.2018 18:29+2> для простого CRUD брать ORM в качестве основной
> реализации репозитория
Вот почему-то большинство писателей репозиториев, результат работы которых доводилось видеть, не рассматривает это как вариант.VolCh
07.02.2018 18:49+1Сам удивляюсь. Особенно когда люди объясняют введение репозиториев отвязкой от реализаций, инфраструктуры, фреймворков и т. п. и тут же пишут этот репозиторий как наследник либы, размером большим чем весь остальной код со всеми остальными зависимостями.
retran
05.02.2018 12:57+3но только в последний год осознал, что они означают
Нет, судя по тексту статьи — все еще не осознали.
На каждом проекте люди играют разные роли (actor): Аналитик, Проектировщик интерфейсов, Администратор баз данных. Естественно, один человек может играть сразу несколько ролей. В этом принципе речь идет о том, что изменения в модуле может запрашивать одна и только одна роль. Например, есть модуль, реализующий некую бизнес-логику, запросить изменения в этом модуле может только Аналитик, но никак не DBA или UX.
А если на проекте появляются новые роли — надо все переписывать?
Это определенно может ввести в ступор. Как можно расширить поведение класса без его модификации?
Наследование, вариации паттерна «Стратегия» и так далее. При чем тут dll и jar?
LSP: The Liskov Substitution Principle
Есть класс Квадрат и есть класс Прямоугольник. Что от чего наследовать?
Под интерфейсом здесь понимается именно Java, C# интерфейс. Разделение интерфейса облегчает использование и тестирование модулей.
Так как же их разделять-то?
Что такое зависимость модулей? Это ссылка на модуль в исходном коде, т.е. import, require и т.п. С помощью динамического полиморфизма в runtime можно обратить эту зависимость.
Нет, зависимость — это зависимость между двумя любыми структурными блоками программы (функциями, классами, etc).
В общем, в книжке дяди Боба все есть, очень подробно и с примерами кода. А это стоит убрать.mayorovp
05.02.2018 13:11Есть класс Квадрат и есть класс Прямоугольник. Что от чего наследовать?
Какое отношение этот вопрос имеет к статье?
Ну хорошо, попробую ответить на него. В иммутабельном виде Квадрат должен быть наследником Прямоугольника, потому что квадрат — это просто прямоугольник специального вида. В изменяемом виде ни один из этих классов не может быть наследником другого — это нарушит LSP.
retran
05.02.2018 13:15Ответ правильный, конечно, и, в том числе, содержит в себе ответ на вопрос «Какое отношение этот вопрос имеет к статье?».
mayorovp
05.02.2018 13:18Я все еще не понимаю. Не могли бы вы пояснить?
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_problemmayorovp
05.02.2018 13:28Я знаю что это классическая задача, но что вы хотели сказать когда ее приводили?
retran
05.02.2018 13:41Я хотел намекнуть, что осознание автором принципа LSP далеко не полное, и, что не стоит пользоваться его формулировками.
mayorovp
05.02.2018 13:45А что не так с его формулировкой и при чем тут задача про квадрат и прямоугольник?
mkuzmin Автор
05.02.2018 13:45это определение из Википедии: https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%B8%D0%BD%D1%86%D0%B8%D0%BF_%D0%BF%D0%BE%D0%B4%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B8_%D0%91%D0%B0%D1%80%D0%B1%D0%B0%D1%80%D1%8B_%D0%9B%D0%B8%D1%81%D0%BA%D0%BE%D0%B2
retran
05.02.2018 14:19Ага, более того — это упрощенное определение из исходной статьи Мартина. Но оно плохое, потому что не говорит о инвариантах и ограничениях типа и не дает ответа на вопрос «Как именно правильно организовать иерархию классов, чтобы LSP соблюдался?».
Задача про квадрат и прямоугольник — это классическая задача на которой этот вопрос немного проясняют.
А в оригинальной статье Лисков тем временем (http://reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.ps) даны очень четкие определения того, что считать инвариантами и ограничениями и как правильно наследовать.mayorovp
05.02.2018 14:31А определение и не должно говорить как надо делать чтобы его соблюдать.
retran
05.02.2018 14:49А о том, какие условия должны выполняться, чтобы оно соблюдалось?
mayorovp
05.02.2018 14:51Так с ними-то что не так?
retran
05.02.2018 15:18Ок, давайте такой пример:
1. Есть иммутабельный тип.
2. Есть функция его использующая.
3. Наследуем от иммутабельного типа новый — мутабельный (добавляем сеттеры, например).
По определению Мартина — это корректный код, функция может использовать мутабельный тип вместо базового иммутабельного.
По определению Лисков — нет, потому что нарушены инварианты.mayorovp
05.02.2018 15:28По определению Мартина — это корректный код, функция может использовать мутабельный тип вместо базового иммутабельного.
Не любая функция, а только та, которая не полагается на инвариант. Та, которая полагается, использовать уже не сможет, потому что поломается.
Определение Мартина использует конструкцию "функции… должны иметь возможность" — т.е. речь идет сразу о всех потенциальных функциях принимающих базовый класс.
Таким образом, согласно определению Мартина ваш пример нарушает LSP. Не вижу разницы с определением Лисков.
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.»
Нету здесь речи ни о всех потенциальных функциях ни о функциях полагающихся на инвариант. Это вы уже дополняете, потому что знаете о чем правило. И прочитано это может быть как угодно (о чем и речь, собственно).mayorovp
05.02.2018 16:09FUNCTIONS… MUST BE ABLE
Все точно так же как и в русском переводе. Функции должны иметь возможность.
Это вы уже дополняете, потому что знаете о чем правило.
Ничего подобного. Оригинальное определение Лисков я узнал уже после определения Мартина (и забыл сразу же как прочитал, потому что определение Мартина проще запоминается).
mayorovp
05.02.2018 16:17Кстати, если разобраться — то как раз оригинальное определение Лисков несет неоднозначность, поскольку в нем не говорится какие именно свойства в нем рассматриваются: мгновенные (т.е. инварианты) или свойства поведения объекта.
В первом случае наследование мутабельного класса от иммутабельного не является нарушением LSP, ведь иммутабельность не является инвариантом типа!
Ах да, а еще оригинальный принцип подстановки Лисков не применим к интерфейсам, поскольку у интерфейса не может быть своих доказуемых свойств.
retran
05.02.2018 16:35Кстати, если разобраться — то как раз оригинальное определение Лисков несет неоднозначность, поскольку в нем не говорится какие именно свойства в нем рассматриваются: мгновенные (т.е. инварианты) или свойства поведения объекта.
Говорится и очень подробно, как и про инварианты, так и про свойства поведения (она называет это constraints и очень подробно и формально определяет).
Ах да, а еще оригинальный принцип подстановки Лисков не применим к интерфейсам, поскольку у интерфейса не может быть своих доказуемых свойств.
У Лисков как бы вообще нету ни классов, ни интерфейсов. У нее типы и подтипы (а интерфейс, это очевидно тип) с контрактами. Если конкретный язык не позволяет задавать предусловия и постусловия для интерфейса — это не означает, что их можно игнорировать.
В первом случае наследование мутабельного класса от иммутабельного не является нарушением LSP, ведь иммутабельность не является инвариантом типа!
Как бы из предыдущего следует, что если программист написал в комментарии, что класс иммутабельный — это уже constraint и его надо соблюдать. Даже если еще не существует кода, который бы мог этот constraint нарушить.mayorovp
05.02.2018 17:26Говорится и очень подробно, как и про инварианты, так и про свойства поведения (она называет это constraints и очень подробно и формально определяет)...
… в своей статье, но не в приведенном определении. То есть для того чтобы просто понять определение Лисков — надо идти и читать первоисточники, определение же Мартина более самодостаточное.
У Лисков как бы вообще нету ни классов, ни интерфейсов. У нее типы и подтипы (а интерфейс, это очевидно тип) с контрактами.
То есть определение Мартина более приближено к практическому программированию?
Как бы из предыдущего следует, что если программист написал в комментарии, что класс иммутабельный — это уже constraint и его надо соблюдать.
А если программист написал в комментарии, что класс может быть изменяемым, но реализация таковой не является?
Будет ли в таком случае иммутабельность provable property?
retran
05.02.2018 17:49приведенном определении
Приведенном где и кем?
То есть определение Мартина более приближено к практическому программированию?
Не понимаю из чего сделан такой вывод. Мы не обсуждали применимость тех или иных определений к практическому программированию. Мы обсуждали их корректность и полноту, из чего и следует применимость. Если вы по Мартиновскому определению сами наследуете мутабельный класс от иммутабельного — это уже о нем говорит все что нужно знать.
А если программист написал в комментарии, что класс может быть изменяемым, но реализация таковой не является?
У вас контракт зависит от реализации?
Извините, но я, пожалуй, перестану тратить свое время.mayorovp
05.02.2018 18:09У вас контракт зависит от реализации?
Нет, у вас.Нет, но меня смущает слово provable.
Приведенном где и кем?
В Википедии.
Мы не обсуждали применимость тех или иных определений к практическому программированию. Мы обсуждали их корректность и полноту, из чего и следует применимость.
Я утверждаю, что оба корректны и полны, но определение Мартина проще для понимания без специальных знаний.
Если вы по Мартиновскому определению сами наследуете мутабельный класс от иммутабельного
Пожалуйста, читайте мои сообщения внимательнее, чтобы не читать в них того чего я не писал.
niamster
05.02.2018 13:08LSP: The Liskov Substitution Principle
Имеет сложное математическое определение, которое можно заменить на: Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
Мне кажется, что это определение вводит в заблуждение. Это скорее всего применимо к полиморфизму и ducktyping. Да и про функции в определении принципа нет упоминания.
Речь идет о типах: подтипы базовых типов не должны изменять свойства(как корректность) программы.
Например, если есть тип «драйвер базы данных», то подтип «драйвер базы данных с резервным копированием» отвечает принципу, а подтип «драйвер базы данных который всегда отбрасывает данные с невалидными полями» может и не подходить. Тут все зависит от рамок «корректности».
coh
05.02.2018 13:25+4Яркий заголовок и интригующее описание.
Пришел за откровением, часто пытаюсь объяснить эти принципы на пальцах. К сожалению, очередная из многих статей, объясняющих что такое ООП, SOLID…
Начинающему студенту / программисту все это пустой звук, можно зазубрить но тяжело осознать.
Понимание что такое и главное зачем нужно ООП приходит намного позже “начала использования классов”. Понимание SOLID приходит с опытом разработки, запуска и поддержки проектов.
Полное осознание приходит уже после того, как вы вовсю используете эти принципы.Atorian
05.02.2018 15:09Мне понравилось как SOLID разжевал Александр Бындю.
В свое время именно его примеры вызвали тот самый эффект «Ага! Вот оно как!». Может и вам подойдет, как референс для новичков?
nardin
05.02.2018 15:02Мне очень понравилось как SOLID разберается в книге: Паттерны проектирования на платформе .NET
А вот «Принципы, паттерны и методики гибкой разработки на языке C#» я бы рекомендовал вдумчиво/осторожно читать. Местами она очень странная.
GreedyIvan
05.02.2018 17:31На каждом проекте люди играют разные роли (actor): Аналитик, Проектировщик интерфейсов, Администратор баз данных. Естественно, один человек может играть сразу несколько ролей. В этом принципе речь идет о том, что изменения в модуле может запрашивать одна и только одна роль. Например, есть модуль, реализующий некую бизнес-логику, запросить изменения в этом модуле может только Аналитик, но никак не DBA или UX.
SRP подразумевает не подчинение чьи-то требованиям, а манипуляция только одним актором.
«Бизнес-логика», кто бы её не запрашивал, есть в любом алгоритме. Она и есть «любой алгоритм» по определению. И в SRP речь идет не о том, что попросить поменять алгоритм может только кто-то один, а то, что модуль реализует алгоритм в отношении одной и только одной сущности. Все канонические примеры как раз показывают, что для соблюдения SRP в алгоритме не должно присутствовать действий над другими сущностями.
Иными словами для реализации действий с несколькими сущностями мы объявляем обобщенную мета-сущность, в которой реализуем алгоритм с вызовом соответствующих методов каждой сущности. Если нам нужно поменять этот мета-алгоритм, мы меняем локику этой мета-сущности. Если нужно поменять алгоритм модификации какой-то конкретной сущности, то лезем в код этой конкретной сущности, ничего не зная и даже не подозревая, что эта сущность может участвовать как составная в каком-то более общем алгоритме.mkuzmin Автор
05.02.2018 17:31Вы читали первоисточники, которые указаны в статье?
GreedyIvan
05.02.2018 23:10Приведенный в книге пример с тремя методами (расчет зарплаты, отчет по часам и сохранение) является просто более расширенной версией канонического примера с печатью отчета.
Что есть Работник и метод save? Из-за архитектурного разделения на сущность и хранилище, метод save является частью хранилища, для которого Работник используется лишь как объект, который надо сохранить. Поэтому архитектурно неправильно делать метод save в Работнике, который лишь представляет собой структурированные данные.
То же самое касается и других взаимоотношений: Работник и отчет отработанных часов, Работник и расчет зарплаты. Эти методы используют Работника как источник первичных данных для расчета, а не для реализации внутренней бизнес-логики самого Работника. Поэтому эти методы должны реализовываться в отдельных сущностях, а не в самом Работнике.
Пример с ролями в компании, которые могут запрашивать бизнес-логику, мне не кажется полноценным. Тот же расчет зарплаты использует отчет по отработанным часам в качестве входных данных. И вполне нормально, если этому отчету нужна другая реализация данного отчета по отработанным часам, то реализовать эту вторую логику в «отчетах», дав на выбор, какую реализацию отчета использовать. А не завязываться исключительно на роли и кто что запросил, то там и реализуем.
Хорошая IT система представляет собой конфигуратор из различной функциональности. Где запросы внешнего пользователя удовлетворяются соответствующей конфигурацией.
mamitko
05.02.2018 17:43> SOLID критикует тот, кто думает, что действительно понимает ООП
Не знаю, как у «того», но, например, у меня основная претензия к SOLID в том, что их практически невозможно использовать как руководство «как надо делать», пока ты не созрел до того, чтобы их понять. А когда созрел, такое руководства тебе уже и не нужны.
И странно, что общественность игнорирует другие четрые буквы: GRASP. Значительно приземленнее и практичней.mkuzmin Автор
05.02.2018 17:45В том то и дело, что формулировки принципов выглядят довольно странно.
И осенью вышла книга CleanArchitecture, которая исправляет эти недостатки.
Цель статьи привлечь внимание и посоветовать ознакомиться с первоисточниками.
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. И дальше уже про интерфейсы и их реализации.
mkuzmin Автор
05.02.2018 19:22Круто, что хоть кто-то открыл эту книжку =)
Еще советую посмотреть видео. Их можно найти.
SRP
DBA тут при том, что есть шлюз к базе, и актором, запрашивающим изменения будет DBA.
То, что вы привели — действительно цитата из книжки, я же привел свою трактовку.
OCP
Определение из статьи — цитата из книжки. Трактовка взята из соответствующего видео с cleancoders.com. Артефакт — это файл, содержащий класс в текстовой форме, dll, и т.п.
LSP
В основном, эта часть опять взята с cleancoders.com
Cheros
05.02.2018 19:11-1По поводу LSP принципа, мне кажется что вы не совсем правильно поняли.
Вы приводите пример в четвертом абзаце с Circle и Shape. Естественно что Circle нельзя заменить Shape, но этого и не было написано в принципе:
Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
Тут написано о возможности использовать подтип. То есть заменим
наList<Shape>
и никто не умрет, так какList<Circle>
Circle
точно имеет все методыShape
, но не наоборот. Вы же хотели сделать наоборот, что естественно нарушает этот принцип и не логично по своей природе.mkuzmin Автор
05.02.2018 19:14Тут речь про то, что модели не обязаны реализовывать отношения объектов реального мира.
Т.е. в реальном мире список окружностей является подтипом списка фигур, но в программной модели с изменяемыми списками это не так.
mayorovp
05.02.2018 19:25но в программной модели с изменяемыми списками это не так.
Достаточно сделать список неизменяемым — и он станет ковариантным.
VolCh
05.02.2018 19:31В реальном мире окружностей вообще нет :) С другой стороны, список окружностей является подмножеством, а не подтипом списка фигур. Это у списка окружностей может быть подтип список фигур, а не наоборот. Наследование расширяет множество значений и допустимых операций супертипа, а не сужает его. Вы, конечно, можете его сузить в реализации, но это нарушит LSP :)
mayorovp
05.02.2018 19:35Нет, список фигур не может быть подтипом списка окружностей — это точно так же нарушит LSP.
VolCh
06.02.2018 13:02Чем это его нарушит? В список фигур мы можем добавить окружность? Можем. Из списка фигур мы можем получить окружность или другие фигуры — её подтипы? Можем.
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]; // БАМ! Там лежал прямоугольник. }
VolCh
06.02.2018 13:34// БАМ! Там лежал прямоугольник.
А прямоугольник у меня подтип окружности :)
mayorovp
06.02.2018 13:35Вы это серьезно? Ну тогда подставьте вместо прямоугольника любую другую фигуру которая не является подтипом окружности.
VolCh
06.02.2018 13:40-1Серьёзно. Из реального, пускай и не коммерческого опыта пришёл к выводу, что идея геометрического моделирования через наследование всего от окружности имеет свои плюсы.
mayorovp
05.02.2018 19:22List<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]; // БАМ! Прямоугольник - не круг.
Cheros
05.02.2018 19:33-2class Man { public void eat() { ... } } class Asian extends Man { //... } class European extends Man { //... } // метод для всех // Каждый человек может есть, поэтому // Man можем заменить на Asian или European void method(Man man) { man.eat(); }
mayorovp
05.02.2018 19:34А куда вы List дели?
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(). // Каждая фигура может "нарисоваться". Вот и соблюдение принципа.
mayorovp
05.02.2018 19:48Нет, вы путаете LSP и статическую типизацию.
LSP — это требования к классам и их контрактам, а не к коду который их использует. Вызов
list[0].getRadius()
не может нарушить LSP в принципе. Но LSP может нарушить сама возможность такого вызова.
vba
05.02.2018 20:12Спасибо за статью, такой вопрос по LSP. Вы сказали что
И есть потомок DoubleStack, который дублирует добавляемые элементы. Естественно, класс DoubleStack нельзя использовать вместо Stack.
С этим все понятно, но хотелось бы узнать, кто нарушает в таком случае принцип LSP, тот кто его так реализовал или тот кто его так использует? Если первый вариант правильный то для ООП языков это означало бы — избегайте глобальных абстракций (List, Hash, etc). Спасибо.
mkuzmin Автор
05.02.2018 20:16Виноват тот, кто реализовал DoubleStack.
избегайте глобальных абстракций (List, Hash, etc)
Несколько странный вывод =)
Хорошо бы пользоваться соответствующими интерфейсами, а для их выбора поможет ISP.
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());
то нарушения нет.
Eldhenn
05.02.2018 21:41> На старте проекта, мы знаем, что будем использовать реляционную базу данных, и это точно будет Postgresql, а для поиска — ElasticSearch. Мы даже не планируем их менять в будущем.
Хочу немного дополнить. В будущем может оказаться, что НЕ ВСЕ части проекта используют одну и ту же бд, одну и ту же субд, один и тот же принцип хранения данных. То есть не то, чтобы мы что-то меняли глобально, но внезапно появилась подсистема, в которую часть данных будет поступать из 1С…
bro-dev
06.02.2018 01:35Есть какое то статистическое обоснование того что такие принципы удешевляют разработку? Со стороны кажется что программисты как фанатики следуют каким то правилам, потому что так делают более крутые программисты, или потому что так в умных книжках написано.
mayorovp
06.02.2018 05:52А эти принципы и не должны удешевлять разработку, их цель — удешевить сопровождение.
VolCh
06.02.2018 13:37Смотря что вы называете разработкой. Написать одну версию "под ключ" по подробному ТЗ, сдать и забыть, а если придётся дорабатывать, то вторую версию писать с нуля — тут эти принципы могут удорожить разработку.
GreedyIvan
06.02.2018 14:03+1Классический пример нарушения. Есть базовый класс Stack, реализующий следующий интерфейс: length, push, pop. И есть потомок DoubleStack, который дублирует добавляемые элементы. Естественно, класс DoubleStack нельзя использовать вместо Stack.
Вообще-то может, и никаким нарушением принципа это не будет.
Что в этом случае поламается? То, что зависело от никак незаконтрактованной логики использования базового класса.
Вот если у нас законтрактован результат работы цепочки вызовов push и pop, то и потомок обязан будет выполнять этот контракт. Иначе он не потомок. Позволяют ли средства языка описывать подобные контракты — это другой вопрос. Как частное решение для классов, в контракте которых подразумевается конкретная логика их использования, языки могут предоставлять возможность их финализации, на уровне синтаксиса запрещая создавать от них потомки.mayorovp
06.02.2018 14:28Обычно стеком все же называют структуру данных с хорошо известным поведением — FILO (First In — Last Out) без дублирования и пропадания элементов. Даже если стек не финализирован — DoubleStack все равно будет ошибкой, только не компиляции а проектирования.
mkuzmin Автор
Если вы знакомы с Clean Architecture и не боитесь Clojure, то стоит просмотреть мой проект
yvm
Вот вы умничаете а у вас XSS в демке…