Pull to refresh

Comments 91

Возможно недостаточно акцентировал. Пытался выразить две мысли:

  1. Отражение объектов реального мира в классах с поведением, похоже что не лучшая идея

  2. Предложил способ получше.

Мне кажется, что при объяснении принципов ООП преподаватели (иногда по объективным причинам, иногда по незнанию) забывают что мы никогда не создаем (и не можем создать) отражение объективной реальности. Вместо этого, мы в ООП описываем модель (!) этой реальности, существующую только в мыслях проектировщика/проектной команды. И, поскольку модель реальности всегда упрощена, то она отражает только те свойства которые были существенны с точки зрения наблюдателя.

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

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

С первым тезисом согласен. Меня смущает единый объект с данными описывающий модель собаки и сервис-объект, который описывает все поведение собаки. Я предлагаю каждый "метод" описывающий поведение собаки положить в отдельный класс, но в одной папке, и если этот метод возвращает какие-то сведения об объекте, то пускай вернет свой DTO(только с необходимыми полями). Такой подход позволяет иметь в каждом классе-функции только свои внешние зависимости, а каждая DTO имеет только свои поля и в том виде, в котором они нужны, из за чего лишние данные не протекают там где они не нужны.

Это потому что собака живая, и с нашей точки зрения, она должна быть самодостаточна в своем теле. В энтерпрайзе объекты зачастую не живые (счет, партия, клиент-как-сущность, и т.д.). И как «здрасьте» можно иметь некий сервис, который реализует life-cycle определенной сущности. То есть если вы хотите собаку, то идите в DogsService и попросите себе экземпляр. А по завершении жизненного цикла туда же сдайте. :-) И вообще, трогать собаку вы не имеете права, потому что для decopuling вам бы хорошо вообще не иметь понятия как она устроена. Поэтому если вы с ней что-то хотите сделать, то лучше попросите специализированный сервис, чтобы они в рамках своего public interface произвели необходимые манипуляции. :-)

Статьей я хотел намекнуть, что у DogsService просматриваются проблемы с SRP т.к. сервис имеет все зависимости, которые требуются для поддержания инварианта по всем методам изменения состояния собаки. Везде где разработчики взаимодействуют с "собакой", даже по самым простым методам вроде получения года рождения, будет необходимо знать о всей сложности такого сервиса -- о всех зависимостях. Явно это проблема заметна в тестах, где для проверки простого кейса, вроде вычисления возраста собаки, придется инициализировать объект-сервис непропорциональной кейсу сложности.

Ниже в комментариях так же описали еще две проблемы таких сервисов как DogService:

1) со временем сервисы склонным превращаться в god-object
2) если есть задача в одной транзакции и собаку в будку прописать и кол-во жильцов в будке увеличить, то в каком сервисе(сервис собаки или будки) должен лежать такой метод прописки собаки?

В статье я предлагаю распилить сервис на отдельные функции, сложить их в папку с именем DogsService и там где требуется использовать конкретную "функцию-класс". А те функции, которые меняют состояние двух агрегатов вынести отдельно в папку "Services".

Дальше оно уже очень сильно depends. В layered-подходе, у вас есть сервис будки, который может засунуть в будку любую сущность с подходящим интерфейсом, есть сервис собак который выдает собак. А где-то наверху над ними живет еще один сервис, которому вы можете показать собаку и и будку — а он проверит какие-то свои бизнес правила, и либо дернет нижележащие сервисы чтобы они изменили собаку и будку для установления связи «живет в» между ними, либо откажется это делать. Причем в сервисе верхнего уровня могут быть зашиты (или сконфигурированы, или даже подгружены в момент выполнения) некие правила по которым это можно или нельзя делать. В гексагональной архитектуре — это будет решено по-другому, и т.д. Вопрос в модели, и в цели ради которой она построена. Для меня в общем, не очевидно, почему распил сервиса на отдельные функциональные интерфейсы (каждый в своем файле) является положительным моментом… Или, говоря по-другому, я не вижу ничего такого, что мы бы могли сделать после этого действия, но чего не могли бы сделать будь эти методы собраны в одном юните? Ссылку на метод-то никто не отменял… В джаве так и вообще ссылка на метод образует FunctionalInterface — все как вы хотите…

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

Плюсы которые я могу выделить в моем подходе:

1) Для тестирование отдельного метода, не требуется создавать сложный объект со всеми зависимостями;

2) Небольшие удобные файлы с "функциями", которые могут быть отредактированы отдельно друг от друга. Это кажется не значимо, но на деле, очень удобно.

Недокрутили мысль, к сожалению( Изобрели велосипед и рассказали о каком-то рафинированном CQRS

Добрый день, расскажите как можно было бы докрутить. Наш подход вырос именно из CQRS. Поделив приложение(монолит) на изолированные сервисы, встал вопрос о том, как организовывать код внутри сервисов. Мы решили сохранить гранулярность от CQRS, когда каждая функция это отдельный класс, но при этом нет необходимости в разделение на command и query.

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

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

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

Это должен быть метод условного менеджера пользователей или класса-обертки над хранилищем.

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

Страницы тоже не могут знать про пользователя.

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

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

Для этого даже есть паттерн DCU - представление процесса как объекта.

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

Верно ли я понял вас, что влияние на внутреннее состояние объекта вы предлагает упаковать в один класс, чтобы всегда иметь одно место в коде ответственное за изменение этого состояния?

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

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

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

Если "антипаттерны" показывают характеристики лучше, чем идиоматичный код, то это не антипаттерны. В этом случае ваши идиомы являются антипаттернами.

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

Расскажите пожалуйста, когда синглтон это отличное решение.

Когда у него нет состояния, а его функции делают нерациональной абстрагирование для тестов

Синглтон без состояния это же просто набор статических функций, не?

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

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

Давайте поподробнее про "одно место, которое будет следить за инвариантами".

Сначала простой вариант. нам нужны инварианты в рамках одного класса.

Известные мне ORM поддерживают возможность перехватить событие записи объектов в базу и туда можно навесить свою валидацию. В C# уже готовый интерфейс есть для этого https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.ivalidatableobject который еще и в UI поддерживается.

Более того, можно эту валидацию в виде check constraints в базе. Тогда инварианты нарушить будет невозможно.

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

Как сделать это "одно место", если "не более трех позиций в заказе" это не "инвариант", а правило, которое меняется. Например разрешить больше трех позиций ВИП-клиентам или во время распродаж?

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

Ваш пример отличный.
Как раз хорошо показывает и ограничения ООП (например на стыках в БД) и прагматичный подход.
Я не знаю, как будет лучше)

Вычисления еще при доступе к базе - это как раз пример не ООП, но нормального и практичного подхода.

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

По поводу вашего примера - Я бы сделал так:
- объекты следят только за простыми инвариантами - в заказе должен быть телефон и сумма, заказ не должен быть пустым и статус должен соответствовать другим аттрибутам (например заказ может быть без адреса на ранних стадиях, но после приема в работу адрес должен быть)
Они стараются не допускать перевода себя в недопустимый стейт.
- Правила для многих объектов удобнее формировать не от объектов, а от процессов, их описывающих.
Соответственно получится что-то похожее на описанное в статье - один класс на один процесс, в котором объекты заказа, клиента и его счета - параметры.
Конечно полное абсолютное поведение системы будет чуть размазанным, но правила будут сосредоточены на правилах.
Альтернатива - какой нибудь мегакласс с доступом всех ко всем, тоже так себе.
- Если не получается собрать объекты в рациональные сущности, ну, значит договариваться с действующими лицами на нерациональные, синтетические сущности, которые хотя бы будут понятны действующим лицам.
Например условный МенеджерПокупкиИПравилАкций.
Если с точки зрения понятных абстракций объект так себе - ну, он может быть прост и полезен с другой стороны, например, быть плоским списком, который легко настраивать и расширять менеджерам клиента.

Я не то чтобы ярый апологет ООП, Я скорее за практичность.
Там где можно - используем, там где нет - адаптируем.
Но множество архитектур, уходящих подальше от ООП показались мне еще более ядерными, неудобными и склонными к ошибкам.
Люди описывающие преимущества чистых функциональных языков не смогли донести до меня, как Я могу этим сделать продукт и не испытывать боли.
Так же часто заметно натягивание совы на глобус - если условные наборы обработчиков и команд хорошо ложатся на сервера (потому что там запрос-ответ между базой и клиентом пользователя), то для приложений с полным стейтом (Я больше специализируюсь на клиентских играх) это может стать адом.

Осталось пару шагов и придумаете 1С

Я не знаком с тем как 1С устроено, в чем вы видите сходство?

Каждый объект хранится в БД при этом поддерживается несколько СУБД. Но из всех прелестей ООП остается только инкапсуляция и околополиморфизм =). Т.е. нельзя изменить существующие абстракции никак, потому что нет наследования, как такового.

И управляется это все скриптовым языком.

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

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

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

Рад, что концептуально мы с вами согласны. О каких строковых литералах идет речь, имя вьюшки и имя метода куда редиректнуть?

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

Как лучше сделать имя вьюшки я не знаю, если знаете, то подскажите. Редирект поправил, так компилятор поможет.

А почему не воспользоваться стандартом(ми), там и способы описания и их взаимодействие явно прописаны.

ГОСТ Р МЭК 62264-2-2016 ИНТЕГРАЦИЯ СИСТЕМ УПРАВЛЕНИЯ ПРЕДПРИЯТИЕМ Часть 2 Объекты и атрибуты
ГОСТ Р МЭК 62264-2-2016 ИНТЕГРАЦИЯ СИСТЕМ УПРАВЛЕНИЯ ПРЕДПРИЯТИЕМ Часть 2 Объекты и атрибуты
ISO 10303-239-2008 Системы автоматизации производства и их интеграция.  ПРЕДСТАВЛЕНИЕ ДАННЫХ ОБ ИЗДЕЛИИ И ОБМЕН ЭТИМИ ДАННЫМИ.  Часть 239.Прикладные протоколы. Поддержка жизненного цикла изделий
ISO 10303-239-2008 Системы автоматизации производства и их интеграция. ПРЕДСТАВЛЕНИЕ ДАННЫХ ОБ ИЗДЕЛИИ И ОБМЕН ЭТИМИ ДАННЫМИ. Часть 239.Прикладные протоколы. Поддержка жизненного цикла изделий
картинки
гост р 19440
гост р 19440
ГОСТ Р 57309-2016 (ИСО 16354:2013) РУКОВОДЯЩИЕ ПРИНЦИПЫ ПО БИБЛИОТЕКАМ ЗНАНИЙ И БИБЛИОТЕКАМ ОБЪЕКТОВ
ГОСТ Р 57309-2016 (ИСО 16354:2013) РУКОВОДЯЩИЕ ПРИНЦИПЫ ПО БИБЛИОТЕКАМ ЗНАНИЙ И БИБЛИОТЕКАМ ОБЪЕКТОВ

А.3 Проблемы системной инженерии и пространство решений

В данном контексте системная инженерия несет ответственность за общую деятельность по разработке продуктов, проекта/конструкции, которые можно испытывать, изготавливать, технически поддерживать, контролировать, распределять и выводить из эксплуатации. Кроме того, необходимо принимать во внимание подготовку персонала к работе, техническую поддержку, распространение продукции (включая установку и т.д.) и ее вывод из эксплуатации. Основные задачи системного проектирования заключаются в удовлетворении ожиданий клиентов и стратегии предприятия, а социальные, правовые и геополитические ограничения требует структурированного процесса изучения различных вариантов в системных альтернативах для гарантии того, что разрабатывается действительно экономически эффективное, практичное проектное решение. На рисунке А.2 схематически показано пространство проблем, которое необходимо изучить и хорошо понять, чтобы приступить к разработке продукт-решения.

Для начала отделим мух от котлет.

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

Моделировать предметную область, то есть объекты физического мира с которыми оперирует программа, с помощью системы типов ОО-языка называется Domain Driven Design (ака DDD). Вернее DDD описывает в целом подход к анализу и проектированию, а моделирование предметной области с помощью типов называется DDD pattern language.

Хотя и до появления термина DDD многократно в книгах встречались попытки моделировать объекты реального мира с помощью ООП (у Буча например), но именно Эванс с Фаулером ввели в обращение понятие DDD.

Так вот:

DDD pattern language не работает на любой нетривиальной программе программе.

Можете ссылаться.

Причины банальные.

Во-первых, при более двух классов доменной модели неизбежно возникает вопроса где должны располагаться методы, использующие три и более разнотипных объекты. Например у нас есть Заказ, Покупатель и Магазин, где должен располагаться метод, реализующий логику получения Покупателем Заказа в Магазине?

Во-вторых, DDD-классы очень быстро превращаются в полу-god-object, содержащие в себе все методы-бизнес логики. Например Заказ может быть Сформирован, Проверен, Отгружен, Доставлен, Получен покупателем. В итоге в DDD-класс стекается куча вызовов из разных мест, это описание очень походит на антипаттерн god-object.

Адепты DDD все время стремятся написать Земля.Копайся() вместо моделирования экскаватора.

Справедливости ради в DDD pattern language есть средства борьбы со сложностью создаваемой DDD. Domain Services, Application Services и прочие сервисы. Суть их в том, что они выносят логику работы из DDD-классов. Все эти сервисы очень даже объектно-ориентированы, используют инкапсуляцию и полиморфизм, иногда даже наследование. Их строят с применением ОО-паттренов: стратегии, фабрики, фасады, chain-of-responsibility иногда даже рекурсивная композиция и интерпретаторы.

До нормального дизайна остается всего один шаг - убрать вызовы к DDD-классам и сделать так, чтобы контроллеры\команды вызывали Application Services, те вызывали Domain Services, они в свою очередь Infrastructure Services и оперировали с данными во внешнем хранилище. Если оперирование с данными проще сделать с помощью ORM, который отражает данные на классы, то так и надо делать.

Спасибо за развернутый комментарий, все по делу, совершенно правы, что разделяете ООП и DDD. Я готов по пунктам ответить. Тизер: мне кажется обе проблемы DDD, что вы описали можно решить.

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

Мы долго мучались с этим вопросом, но оказалось, что в книге по DDD есть точный ответ на это вопрос -- метод надо назвать доменным сервисом и он не относится ни к одному из агрегатов. В заметке выше я предлагаю такие "функции" положить в отдельную папку под именем Services.

  1. По второму вопросу: "Во-вторых, DDD-классы очень быстро превращаются в полу-god-object, содержащие в себе все методы-бизнес логики.".

Вы правы, если методы одного агрегата упаковывать в один класс, то именно это и происходит и это ужасно и чревато кучей проблем, одна из, которых это миллион зависимостей у таких god-object-ов. Частично это решится за счет перового пункта, когда эти классы перестанут менять состояния друг друга, но основная идея из статьи в том чтобы перестать складывать все методы агрегата в один класс, а положить каждый метод в отдельный класс(в одной папке). Оба этих пункта решают проблема god-object-а.

Если придерживаться такого подхода, то не приходится городить сложные схемы работы с состоянием и получается выразить DDD проект в коде, так чтобы код был читабельный и поддерживаемый. Я даже скажу больше, это статья именно про решение проблемы good-object при реализации DDD проекта на ООП языках.

Решение проблемы - отказаться от DDD, строить системы "сверху вниз", сжечь книги буча и эванса, бить себя по лбу при желании написать Земля.Копайся()

Думаю что проблема не в книге Эванса, а в том что "мы"(программисты) пишем "Земля.Копайся()" в книге он не настаивает(не призывает) на к такому подходу, а явно говорит, что его книга не про тактику реализации на конкретной парадигме, а про подход проектирования.

Мне было бы интересно, если бы вы сказали решает ли подход, описанный в коментарии выше, проблемы которые вы упомянули.

Именно призывает и настаивает. Как и Буч в своей книге ООАД (на самом деле с него все началось). Это их парадигма и подход к проектированию. Перечитайте.

Отделение данных от алгоритмов обработки этих данных конечно решает проблемы DDD. Потому что это и есть отказ от DDD. Мы просто начинаем применять ООП в другом месте - в моделировании алгоритмов, а не в моделировании данных.

Я предлагаю даже не начинать DDD если это нетривиальный пример.

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

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

Небольшая метафора:

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

Но рукой все равно удобнее.

Я вот на этот тезис из книги опираюсь.

А вся остальная книга про то как надо делать в java.

Вы правы, отсюда огромная путаница между DDD и ООП, как будто одно должно быть выражено в другом. Я делаю скидку автору, книга была выпущенна в 2007 года, уже почти 20 лет прошло, а писалась наверное еще раньше. В те года "клиенты" были толстые, БД медленные(из за дисков), о stateless подходе мало кто говорил, а парадигма ООП казалась ответом на все вопросы, поэтому автор так много уделил ей внимание.

Но сейчас, мы можем взять лучшее из книги - это методы проектирования и используя новые языки, функциональные принципы,  stateless подход и микросервисы наконец то реализовать DDD как оно может работать!

Эванс книгу выпустил в 2003, а фаулер упоминал Domain Model в POEAA в 2001. Но история началась гораздо раньше, с Гради Буча.

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

Вполне возможно для некоторых классов программ это так и есть. Например для симуляций или АСУТП - там у каждого компонента будет свое поведение.

Фаулер в POEAA описал паттрен domain model, где подход Буча применялся для корпоративных приложений. Описал он его поверхностно, не особо углубляясь в недостатки. Далее они с эвансом написали книгу про DDD, где первая глава про анализ, а остальные про DDD pattern language.

В POEAA кстати было и про стейтлесс, и про веб, и query builder, и современные БД. Так что вряд ли получится сказать, что вознесение DDD было обусловлено внешними факторами.

Буч, кстати, переобулся. В последней редакции OOAD не пишет что Чайник.Закипеть() - отличное решение. Он рассматривает реалистичные примеры приложений и иерархии классов их реализующие. И гораздо чаще на страницах книги можно встретить Экскаватор.Копать(земля), чем Земля.Копайся().

Что если можно сделать по другому. Что если можно сделать функцию-объект-сервис "ВыкопатьЗемлюмыЭкскаватором(ЭксковаторИД, ЗемляИД)" и это функция гарантированно изменит состояние всех агрегатов валидно.

Я бы даже продолжил мысль сразу дальше, что если есть веб-серсив, у которого есть http endpoint который принимает ЭксковаторИД и ЗемляИД и гарантирует что действие будет выполнено правильно.

Зачем нам вообще нужно это ООП для этой задачи?

Экскаватор это не объект данных, а сервис, у которого нет наблюдаемого состояния. Так что нет семантической разницы между Копать(Эксковатор, Земля) и Эксковатор.Копать(Земля), но второй вариант лучше тестируется и имеет выше discoverability.

ООП для данной задачи в том, что Экскаватор это объект со своей структурой, декомпозицией. Земля это объект предметной области с Identity. Это означает что нельзя передать в метод невалидный ЗемляИД и, соответственно, не надо внутри метода Копать устраивать дополнительные проверки.

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

ООП предложило совместить данные и поведение. Одним из посылов было соблюдение инвариантов класса. В вашем случае знание инвариантов надо размазывать по всем функциям и не забыть поменять когда бизнес меняется. Но это нарушает принцип DRY и cohesion выходит не очень сильной.

Размещать инварианты чуть ли не в персистанс слое (как предлагалось здесь в комментах)? Ну это на любителя , примерно как строить систему на триггерах и хранимках - можно забыть о тестабилити

Подскажите, где вам кажется нарушается принцип DRY?

Надо проверку инвариантов повторять в каждой функции, как я понял. Хотя если это просто вынос метода в отдельный класс, то немногое в этом отношении меняется, но тогда я не понимаю профита от такого выноса.

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

Подскажите, а как у вас проверяется инвариант.

Если не брать полуэкспериментальное проектирование по контракту, то также в коде и тестами.

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

Мы эту папочку с функциями-классами упаковываем в сервис, и только этот сервис знает пароль от БД где лежит состояние агрегатов этого домена. Считаем что все методы этого сервиса(api) обязаны менять состояние агрегата валидно и должны быть затестированы - ~100% покрытие тестами.

Пока только такие способы нашил.

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

"Строить систему на триггерах и хранимках" -- мне казалось, подход, который я предлагаю, прямо противоположный. Все методы изменения состояния явно описаны в коде, независимы друг от друга, да еще и отделены от слоя доступа к данным(data layer). Требуется явный и прямой вызов "функции" изменения состояния, который не удасться скрыть.

Мне кажется, что в тезисе "Одним из посылов[ООП(я добавил)] было соблюдение инвариантов класса." и кроется наше с вами различие взглядов, мне кажется, это ошибочный тезис, тем более если речь идет об сущностях из DDD. ООП, похоже, было про алгебраические типы, вроде int, double, которые между собой независимы и состояние у них крайне ограниченно, если это вообще моно назвать состоянием.

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

https://medium.com/javascript-scene/the-forgotten-history-of-oop-88d71b9b2d9f

ООП было про гуй и имитационное моделирование

Хотя:

Alan Kay also saw objects as algebraic structures, which make certain mathematically provable guarantees about their behaviors:

“My math background made me realize that each object could have several algebras associated with it, and there could be families of these, and that these would be very very useful.”
~ Alan Kay

ООП, похоже, было про алгебраические типы, вроде int, double,

я удивлен, что вы как и многие говорят про ОПП, и при этом не в курсе базы.

Класс - это пользовательский тип данных, т.е. расширение языка позволяющие программисту создать свои типы данных.

Тип данных — класс данных, характеризуемый членами класса и операциями, которые могут быть к ним применены (ISO/IEC/IEEE 24765-2010). Тип данных — категоризация абстрактного множества возможных значений, характеристик и набор операций для некоторого атрибута (IEEE Std 1320.2-1998).

Соответственно, ООП лишь сопоставило, членов класса состоянию объекта, а операции над атрибутами поведению объекта.

Все что говорите вы и другие, это фактически антииООП, это превращение классов в контейнеры для функций.

В заметке я как раз и пытаюсь сказать, что, похоже, пора пересмотреть то как мы применяем ООП. Обратите внимание на года стандартов, что вы привели в качестве аргументов, одному из них больше 25 лет, а второму 13.

Если выражаться вашими словами, то я действительно говорю об антииООП.

Остаётся только вопрос что есть инвариант и является ли правило "заказ должен содержать не более трёх позиций" инвариантом класса заказа для приложения интернет-магазина.

Это инвариант для агрегата заказ, если нет особо других вводных.

То есть если вы подняли из базы заказ с 4 позициями должна вылететь ошибка?

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

Можно запретить часть методов, то есть поднимать можно, а процессить нельзя.

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

Это не ответ на вопрос. В базе может произойти что угодно.

Можно запретить часть методов, то есть поднимать можно, а процессить нельзя.

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

Интереснее другой вопрос: как написать код, чтобы не забыть вызвать логику валидации? Кто помешает добавить метод бизнес-логики, где нарушается инвариант и не вызывает проверку?

А является ли инвариантом какого-то класса правило "не более трёх позиций в заказе, если клиент не ВИП"?

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

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

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

Учитывая ваш коммент выше оказывается что даже простые "инварианты" фактически являются не инвариантами, а пред- и пост-условиями операций.

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

Более того, эти пред и пост условия можно выразить в виде запросов к бд (с помощью ef) и даже не поднимать объекты в память

Можно фрагментирован бизнес-логику как угодно. Но зачем? Тестабилити снижается, maintainability снижается, cohesion снижается. Одно дело когда из-за компромисса мы это делаем, но не понимаю зачем это делать нормой.

Снижается по сравнению с чем?

Вернемся к простому случаю. Бизнес-правило - в одном заказе не больше трех позиций.

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

Кроме того: мы используем ORM, аля EF, который использует свойства объекта или конструктор для создания. Это значит что мы не можем проверять правила в конструкторе.

У нас остается один вариант: проверять правила при выполнении действия.

Итак у нас получается цепочка вызовов: Контроллер -> Объект.Действие -> Проверка. Естественно каждая стрелка может заключать еще 100500 вызовов, это не имеет значения

Я предлагаю заменять это на: Контроллер -> Правила.Проверка(объект).

Что это дает:

  • Меньше косвенность вызовов и меньше приседаний с инъекцией зависимостей в Объект.

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

  • Проверка может оперировать на состоянием объекта в памяти, а сделать запрос в базу.

  • Первый вариант может только выбросить исключение при непрохождении проверки, а второй вариант может возвращать true или false и использоваться для деактивации элементов интерфейса ДО выполнения действия пользователем.

Тестируемость обоих вариантов одинаковая. У второго даже выше, так как для создания Объекта ему надо передать кучу зависимостей, которые нужны не только для проверки. Формальные метрики при преобразовании первого варианта во второй не меняются (проверял). В целом если отказаться от инъекции сервисов в Domain Object можно значительно сократить объемы кода.

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

А объект.Действие() где? В сервисах?

Имеет ли валидатор и сервисы доступ к внутреннему устройству объекта? Правильно ли я понимаю, что вы топите за анемичную модель?

А объект.Действие() где? В сервисах?

Да, но это не имеет значения. Действие с Объектом подчиняется той же логике что и правила. Чем сложнее действие, тем хороших способов поместить его в объект.

Имеет ли валидатор и сервисы доступ к внутреннему устройству объекта?

К внутреннему - нет. Использует тот же публичный интерфейс, который используется например для вывода списка объектов на экран. В варианте Объект.Действие() при достаточно сложном действии оно будет перенесено в сервис и будет иметь такой же доступ к объекту, как и Правило, написанное вне Объекта.

Правильно ли я понимаю, что вы топите за анемичную модель?

Я топлю за то, чтобы писать меньше, а работало лучше.

Если общедоступного интерфейса недостаточно для проверки и для действия?

Сможете привести реалистичный рабочий пример?

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

  1. Его нельзя сохранить в базу с помощью EF. Чтобы можно было сохранить нужно как минимум read-only свойство

  2. У вас получается логика основанная на исключениях. Если единственный способ узнать что действие выполнить нельзя - вызвать Объект.Действие() , то вы никак не сможете скрыть\деактивировать элементы управления в UI. Даже try-pattern не поможет.

  3. Вы не сможете логику действия вынести в отдельный класс.

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

Для персистанс-слоя можно использовать дто.

Любая раскрытая информация вредит инкапсуляции, потому что рано или поздно люди начнут обращаться к внутрянке мойЗаказ.элементы[0]…

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

DTO делать там, где можно без него, это антипаттерн. Прямое увеличение объема, связности и снижение mantainability index. Вы на ровном месте сделали код хуже, а преимущества крайне сомнительны.

рано или поздно люди начнут обращаться к внутрянке мойЗаказ.элементы[0]

А чего в этом плохого? И кто помешает также обращаться к dto?

Но это опять половина беды. Вводя dto между domain и persistence вы больше не можете использовать механизм запросов, предоставляемых orm.

То есть мало того, что код стал хуже, он ещё стал значительно медленнее.

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

В последнем абзаце согласен с автором.

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

Потому что не бывает задач "переписать все функции для работы с БД" или "доработать все обработчики HTTP" зачем их тогда группировать?

Напротив, 95% всех задач - это либо сделать/доработать новую операцию (добавить операцию по смене статуса заказа, добавить новый фильтр в список заказов). Либо найти баг/оптимизировать (тормозит выдача списка заказов, 404 при попытке смене заказа). И вот для решения таких задач хочется чтобы ВЕСЬ код, который реализует эту операцию находился в одной папке и кроме кода, относящегося к этой операции в этой папке больше ничего не было.


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

Такая аналогия: вы увлекаетесь разными видами спорта и есть 2 подхода к хранению снаряжения:

  1. Шкаф для шлемов, шкаф для курток, шкаф для обуви, комната/гараж для основных снарядов

  2. Делить по видам спорта: чулан для велосипеда а рядом сумка с велотуфлями, велошлемом и велоформой. Гараж для мотоцикла + ящик для мотоформы. Шкаф для коньков и хоккейной формы.

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

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

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

В каждом слое объект выглядит не так, как в других. Это касается отображения, поведения, хранения и у каждого слоя несколько вариантов и сам он делится на еще несколько.

А так, отправили объект в очередь, оттуда он разлетелся в БД, сайт, прочим партнерам. И вот это уже можно не просто поддерживать, но и развивать и масштабировать.

Я же наоборот сказал, что не бывает задач переписать все функции для работы с БД, поэтому и объединять их (в один слой) не нужно.

Объединять нужно то, что используется вместе а разделять - то что вместе не используется и не связано. хендлер1-бизнес логика1-запрос в базу1 для одной конкретной апишки всегда используется вместе, поэтому их надо объединить. А хендлер2-бизнес логика2-запрос в базу2 другой апишки не связан с хендлер1-бизнес логика1-запрос в базу1 поэтому их надо отделить на самом верхнем уровне.

Да, невнимательно прочитал.

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

В моем понимание все БЛ должны иметь общий интерфейс совместимый с общим интерфейсом БД и тогда с этим можно работать.

Похоже на https://jimmybogard.com/vertical-slice-architecture/
С зависимостями интересная штука. В контроллерах уже можно передавать зависимости параметрами - возможно, что-то такое можно придумать и просто для вызова методов

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

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

Ну перестали же повсеместно писать свои ДОС и браузеры.

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

Наверное уже пораньше, 15 лет назад, был очередной виток споров между функциональщиками и инперативщиками. Процедурщики и ООПки, наверное, спорили 30 лет назад, в 80х ) когда C++ вынужден был делать zero cost of abstraction.

А что плохого что у класса 1 экземпляр, его задача это сокрытие сложности, если надо скрыть ее один раз, ну и хорошо.

Наследование вообще спорная вещь, все меньше вижу ему применение(речь не про интерфейсы как контракты). Тем более, когда мы все ближе к миру с discriminated union и exhaustive pattern matching, где у наследования еще меньше причин.

Что такое настоящий динамический объект?

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

когда в классе 1 экземпляр все хорошо если экземпляров программы не миллионы по всему миру, иначе в конечном итоге за читабельность кода заплатит экология)

Вы считаете что инстанцирование классов как-то заметно сказывается на потребление ресурсов?

если бы сим-карты были сделаны с поддержкой ООП, то нам бы пришлось переплавить всю луну на телефоны

Нет, дотнет от скуки всё на структуры переводит.

Сэкономлю много времени. Можно открыть для себя MediatR и уже посоревноваться в специальной Олимпиаде по теме его обоснованного использования. Совершенно то же самое, только пользоваться проще. Там тебе и OpenClose из коробки. Дальше CQRS, EventSourcing etc...

Никакой новизны в материале нет, сочувствую.

Действительно есть сходство с MediatR и CQRS, но, на мой взгляд, есть и ряд значимых отличия.

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

Sign up to leave a comment.

Articles