Pull to refresh

Comments 163

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

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

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

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

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

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

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

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

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


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

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

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

OCP тоже соглашусь, принцип то вроде не о DIP, а следующем: «Программные объекты должны быть открыты для расширения и закрыты для модификации», Бертран Мейер, «Построение Объектно-Ориентированного Программного Обеспечения». Если я не ошибаюсь, то речь идёт о том, что не нужно переписывать существующие классы (объекты/структуры etc), а нужно их расширять, то есть, создавать новый тип объекта, extended, который наследует исходный и добавляет к нему нечто новое. Поправьте меня, если я ошибаюсь (сам сейчас изучаю принципы и пытаюсь в них разобраться).

Тут речь о том, что у вас есть файл, разделяемая библиотека, микросервис и т.п. и должна быть возможность расширить его поведение без редактирования, перекомпиляции. А как именно это будет сделано — не особо важно. Можно через dependency injection, monkey patching и т.п.

… а при этом, простите, менять (closed for modification) его поведение теми же способами — нельзя?

modification — это про изменение артефакта, файла, а не про изменение поведения.

Да, действительно, вы правы: "The source code of such a module is inviolate. No one is allowed to make source code changes to it."

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

UFO just landed and posted this here

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

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

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

Технологии не приколачиваются гвоздями к решению.
UFO just landed and posted this here
Попробуйте прочитать процитированный вами абзац целиком, а не только первое предложение.

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


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


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

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

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


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


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


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

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


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

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


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

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

UFO just landed and posted this here

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


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


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


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

А дальше выясняется, что нужны ещё и attachment, и multipart, да и один address не нужен, нужны их три списка… или наоборот, нужно что-то совсем другое для рассылок через телеграм или СМС…

(простите за некропостинг)

Конечно выесняется, мы еще не научились предсказывать будущее.
Ты ожидал чего-то другого? =)

Я пока не понимаю, как это решает SOLID, обещающий вроде бы это решать.

Если отправка письма — это абстракция, и нужно изменить способ с smtp на mailchimp, то меняется только реализация. Даже не нужно перекомпилировать модуль с бизнес логикой.


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

Создание нового интерфейса (а то и не одного), его реализации, и пересаживание всех пользователей старого интерфейса на новый или хотя бы на реализацию обёртки над новым, которую тоже надо будет создать и как-то с новым познакомить… Гемор, но иначе никак даже с SOLID. Да?

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


Абстракции имеют цену.

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

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

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

Ну а по поводу потратить время\деньги — нет ничего хуже, чем проигнорировать процесс сбора требований. Если не изучать вопрос хоть как-нибудь, вы рискуете потратить гораздо больше, чем потратили бы на ресерч.
Это типичный путь архитектора, который приводит к перерасходу бюджета. Цель продукта — заработать денег. Значит надо как можно скорее произвести конкретную реализацию конкретного бизнесс процесса преподнесенного как тезис, получить подтверждение тезиса через прибыль (или другие измеряемые метрики), реализовать новую версию продукта (возможно выбросив, но лучше дополнив оригинальную — OpenClose (не правим, а дополняем… или выбрасываем)).
Не надо нам точно знать как раз. А даже если точно уже знаем, то нужно абстрагироваться от этого знания, хоть как-то, пускай даже эта абстракция прячет разницу между постгри 9.5 и 9.6.
UFO just landed and posted this here

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

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

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

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


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


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

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

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

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


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

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

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

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


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

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


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

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

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

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

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

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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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


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

LSP: The Liskov Substitution Principle


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

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


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

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


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

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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


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


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

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

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

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


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

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

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


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


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

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


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

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


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

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


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

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


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

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


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

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


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

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


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

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


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

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


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

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

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


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

В Википедии.


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

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


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

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

LSP: The Liskov Substitution Principle

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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


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


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


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

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

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

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


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

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

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

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

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

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

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


var list = new List<Shape>(); // Список фигур
list.Add(new Reclangle()); // Разрешено, потому что прямоугольник - фигура
func(list);

void func(List<Circle> list) { // Функция принимает список окружностей
    Circle c = list[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]; // БАМ! Прямоугольник - не круг.
class Man {
    public void eat() { ... }
}

class Asian extends Man {
    //...
}

class European extends Man {
    //...
}

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

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


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

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

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

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


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

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

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


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

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

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

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

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

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

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

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

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

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

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

не совсем

нижний уровень это взаимодействие: ввод-вывод, API, ...

почему нижний? потому что это привязано к контексту и значит тут наиболее часто изменяемый код

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

самый верхний слой (домен) он не зависит от вариантов использования и самый редко изменяемый

итак, если хотите какую-то непрерывную меру то я бы выбрал "независимость от контекста и инструментов", чем меньше зависимость тем выше уровень

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

если в бизнесе есть управляющий сервис, то он выше по абстракции

обслуживающие сервисы ниже по уровню абстракции и больше подвержены изменениям

Sign up to leave a comment.

Articles