Здравствуйте, takTak, Вы писали:
T>пока я ещё вроде ничего про реализацию"сохрани" не говорил: мы что используем: как ты когда-то намекал на Log / Query, т.е. Write & ReadModel, как понимаю, или обычную персистенцию в базу данных ?
Я потерял нить. Вот цитата из вашего ответа:
тот "заказ" в приведённом разрезе, наверное, должен обладать методами "подсчитай цену" и "сохрани"
T>внутренне будет установлен флажок, что ни изменение цены (в том числе и менеджером), ни добавление или удаление позиций недопустимо, в принципе, может, стоит метод переименовать в "оформить заказ" T>ты сам вроде упомянул, что если пользователь- менеджер, то он может изменить цену или я что-то не так понял?
Он может изменить цену при создании заказа. В анемике мы имеем, грубо говоря, два метода:
public OrderId PlaceOrder(userId, resellerId, List<(string sku_id, int qty)> items);
public OrderId PlaceOrder(userId, resellerId, List<(string sku_id, int qty, Decimal price)> items);
Второй доступен только менеджерам.
Где-то невдалеке мы имеем метод
Decimal GetPrice(customerId, resellerId, sku_id);
Он не является частью заказа, т.к. наш UI, естественно, умеет показывать пользователю каталог товаров с ценами на них. На самом деле у нас даже будет метод
Который сразу возвращает весь прайс-лист для конкретного реселлера.
Но это в анемике. А в DDD у нас где будет это всё?
S>>По мне так это плохая идея. Потому, что пересчитывать цены надо не только тогда, когда пользователь что-то добавил или изменил в заказе, но и при изменении списка правил. Кто какие события будет порождать, и кто на какие события будет подписываться? Мне проще вообще не иметь заказов в "предварительном" состоянии. T>т.е. цена может быть изменена задним числом? чего-то я этого не понимаю, это как?
Как раз не может. После PlaceOrder никакого изменения цены не может быть. До PlaceOrder не существует никакого Order. Если бы мы сохранили Order в состоянии "предварительный", то в нём нельзя фиксировать цену — датой заказа считается та, когда он "отправлен". Если мы подготовили его в январе, а потом пришёл новый прайслист, и мы отправили заказ уже в феврале, то оплачивать придётся по ценам февраля.
T>я исхожу из того, что агрегат обладает внутренним состоянием, которое позволяет ему рассчитать цену, не обращаясь самому к базе данных, т.е. при инициализации и изменении состояния он получает всё необходимую для расчётов информацию
Вот этого я и не понимаю. Хочу увидеть пример кода. Потом можно будет попытаться понять, откуда агрегат будет брать это внутреннее состояние. T>если мы ограничим агрегат только тем, что ему нужно: внутренним состоянием, никакого DbContext ему не нужно
Вот как раз тут важны детали.
T> это я тоже не понял, что значит: "не вызывает каскадных изменений" ? если сегодня скидка на трусы на 20 %, а завтра — на всё бытовую технику, а послезавтра — на пылесосы фирмы ххх, то что значит: " без каскадных изменений"
Это значит, что нет никаких "событий" и "подписок". Есть просто метод getPrice(goodId, customerId), который вызывается при загрузке страницы. Где-то у него внутри перебираются pricing Rules, которые матчат товар и кастомера; применяются всякие комбинации. Результат отправляется клиенту и навсегда забывается. Плюс-минус кэширование.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
T>>пока я ещё вроде ничего про реализацию"сохрани" не говорил: мы что используем: как ты когда-то намекал на Log / Query, т.е. Write & ReadModel, как понимаю, или обычную персистенцию в базу данных ? S>Я потерял нить. Вот цитата из вашего ответа: S>
S>тот "заказ" в приведённом разрезе, наверное, должен обладать методами "подсчитай цену" и "сохрани"
ну, переименуй это "сохрани" в "создать заказ" или "оформить заказ" — не знаю, как там у вас что в UI называется
T>>т.е. цена может быть изменена задним числом? чего-то я этого не понимаю, это как? S>Как раз не может. После PlaceOrder никакого изменения цены не может быть. До PlaceOrder не существует никакого Order. Если бы мы сохранили Order в состоянии "предварительный", то в нём нельзя фиксировать цену — датой заказа считается та, когда он "отправлен". Если мы подготовили его в январе, а потом пришёл новый прайслист, и мы отправили заказ уже в феврале, то оплачивать придётся по ценам февраля.
если создание заказа- это размещение заказа, т.е. placeOrder, то что тогда такое "отправить заказ" ? Вот это я вообще не понимаю: "Если мы подготовили его в январе, а потом пришёл новый прайслист, и мы отправили заказ уже в феврале", кто его подготовил?
T>>я исхожу из того, что агрегат обладает внутренним состоянием, которое позволяет ему рассчитать цену, не обращаясь самому к базе данных, т.е. при инициализации и изменении состояния он получает всё необходимую для расчётов информацию S>Вот этого я и не понимаю. Хочу увидеть пример кода. Потом можно будет попытаться понять, откуда агрегат будет брать это внутреннее состояние. T>>если мы ограничим агрегат только тем, что ему нужно: внутренним состоянием, никакого DbContext ему не нужно S>Вот как раз тут важны детали.
нужны ли для калькуляции какие-то вызовы в базу данных или я могу загрузить все интересующие меня аргументы при создании агрегата?
T>> это я тоже не понял, что значит: "не вызывает каскадных изменений" ? если сегодня скидка на трусы на 20 %, а завтра — на всё бытовую технику, а послезавтра — на пылесосы фирмы ххх, то что значит: " без каскадных изменений" S>Это значит, что нет никаких "событий" и "подписок". Есть просто метод getPrice(goodId, customerId), который вызывается при загрузке страницы. Где-то у него внутри перебираются pricing Rules, которые матчат товар и кастомера; применяются всякие комбинации. Результат отправляется клиенту и навсегда забывается. Плюс-минус кэширование.
так когда происходит перебор правил, в системе может появиться изменение, типа сегодня все трусы снова на 5% дороже, ты хочешь сказать что амазон ждёт следующего дня, чтобы скомпилировать правила и перезапустить сервер?
и ещё: покупатель просто каталога всех товаров не видит, он всегда выбирает вначале продавца, и только потом- товары?
Здравствуйте, Sinclair, Вы писали:
S>Прелесть анемиков как раз в том, что нет мучений вроде обратных зависимостей агрегатов от DbContext. Все entity — POCO, все сервисы — stateless.
А как аггрегат может зависеть от DbContext? Он зависит от других сущностей, которые вообще могут быть DTO, т.е. вообще не зависеть от DbContext.
Здравствуйте, takTak, Вы писали:
T>ну, переименуй это "сохрани" в "создать заказ" или "оформить заказ" — не знаю, как там у вас что в UI называется
Ок, переименовал. В любом случае, тот "оформить заказ", который я себе представляю, требует наличия dbContext.
Как без него обойдётся агрегат в DDD —
T>если создание заказа- это размещение заказа, т.е. placeOrder, то что тогда такое "отправить заказ" ? Вот это я вообще не понимаю: "Если мы подготовили его в январе, а потом пришёл новый прайслист, и мы отправили заказ уже в феврале", кто его подготовил?
Ну, так это я пытаюсь догадаться, как работает предлагаемая вами архитектура. Из того, что есть какие-то события типа "пользователь изменил цену", или методы "добавить позицию", я понимаю, что агрегат "заказ" у вас обретает существование ещё до того, как будет "сохранён", или "отправлен" — в общем, до того, как начнётся собственно выполнение заказа.
В обычных интернет-магазинах такие штуки называются чем-то типа "cart" — типа я интерактивно складываю и вынимаю товары из корзинки; а потом, когда я уже готов на выход, корзинка превращается в заказ при помощи операции checkout.
Я не хотел усложнять задачу, т.к. у нас тогда появляется ещё два агрегата (корзина и "элемент корзины"), и мы рискуем вообще не закончить
Поэтому условимся считать, что никакой корзинки нету, и заказ, как таковой, возникает только в момент нажатия на кнопку "заказать". А аналог корзинки у нас эфемерен, существует только в воображении клиента, и нас не интересует.
Ну, типа там какой-то джаваскрипт, интересоваться которым — не барское дело. Наше дело — выставить API для веб-клиента; и с точки зрения этого API до момента "заказать" заказа не существует, а после этого момента изменить уже ничего нельзя.
T>нужны ли для калькуляции какие-то вызовы в базу данных или я могу загрузить все интересующие меня аргументы при создании агрегата?
Ну, это же вы архитектор. Я играю роль заказчика; диктовать вам, когда лазить в базу, я в этой роли не готов. Бизнес-требования я вам задал; если они непонятны — спрашивайте.
А как технический специалист я как раз и хочу понять, что такое этот DDD, и насколько ужасные получаюстя решения при его применении.
T>так когда происходит перебор правил, в системе может появиться изменение, типа сегодня все трусы снова на 5% дороже, ты хочешь сказать что амазон ждёт следующего дня, чтобы скомпилировать правила и перезапустить сервер?
Я не знаю точно, но думаю, что нет. Решительно непонятно, зачем перекомпилировать какие-то правила и перезапускать сервер, когда можно просто добавить новое правило в табличку правил, и оно начнёт учитываться ровно с момента его effectiveStartDate. Давайте закроем тему амазона, чтобы не отвлекаться.
T>и ещё: покупатель просто каталога всех товаров не видит, он всегда выбирает вначале продавца, и только потом- товары?
Да, для упрощения понимания можно предположить, что у каждого продавца — свой "магазин". www.resellerA.com, www.resellerB.com.
Покупатель заходит в какой-то из этих магазинов, и видит ту версию каталога, которую мы отдаём соответствующему магазину (продавцу, reseller в родной номенклатуре).
С точки зрения покупателя не существует такой штуки, как "каталог всех товаров", есть только "каталог товаров у продавца resellerA".
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sharov, Вы писали:
S>А как аггрегат может зависеть от DbContext? Он зависит от других сущностей, которые вообще могут быть DTO, т.е. вообще не зависеть от DbContext.
Вот я и пытаюсь понять, как агрегат отвяжется от DbContext, когда ему пытаются приделать методы, выполнение которых требует бегать в базу.
Пока что прояснения не наступает. Те примеры, которые приводили в этом топике — это обнять и плакать, типа lazy load, не к ночи будь помянут.
Либо просто авторы тактично уходят от вопроса "откуда в account возьмётся его overdraftPolicy". Ну, она типа просто есть — и всё.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
S>>А как аггрегат может зависеть от DbContext? Он зависит от других сущностей, которые вообще могут быть DTO, т.е. вообще не зависеть от DbContext. S>Вот я и пытаюсь понять, как агрегат отвяжется от DbContext, когда ему пытаются приделать методы, выполнение которых требует бегать в базу.
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, Sharov, Вы писали:
S>>Через репозиторий, вестимо. S>Омг. А зачем нам репозиторий в дополнение к DbContext?
Здравствуйте, Sharov, Вы писали:
S>Чтобы отвязаться от DbContext напрямую.
А, ну так-то да. Как бы ещё побольше кода написать, ага. Про эффективность комментировать не буду, пока не увижу код
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sharov, Вы писали:
S>>>Через репозиторий, вестимо. S>>Омг. А зачем нам репозиторий в дополнение к DbContext? S>Чтобы отвязаться от DbContext напрямую.
А это вообще зачем нужно? Самоцель? Я понимаю когда вводятся абстракции для чего-то. Но чаще (в личном опыте) — репозитории существуют сами ради себя, более того половина из них вообще не репозитории.
T>>ну, переименуй это "сохрани" в "создать заказ" или "оформить заказ" — не знаю, как там у вас что в UI называется S>Ок, переименовал. В любом случае, тот "оформить заказ", который я себе представляю, требует наличия dbContext. S>Как без него обойдётся агрегат в DDD —
если смотреть исторически, то агрегат в ддд вообще никак не связан с какой-то там базой данных, агрегат отвечает только за поведение и валидацию тех сущностей, которые находятся под его попечительством: допустим у тебя есть заказ, цена и условия которого могут измениться в будущем, но только при наличии каких-то условий, тогда пользователь, условно говоря, отправляет команду "изменить заказ с атрибутами: номер заказа , наименование операции "потребовать 25% скидки", пользовательский номер",тогда command handler переправляет агрегату событие с упомянутыми атрибутами, агрегат берёт номер заказа, загружает себя (вовсе необязательно из реляционной базы данных) из стримового потока, производит или не производит операцию, сохраняет себя в стримовом потоке, отправляет сообщение "я изменился, новая цена : хх" , и вот уже event handler, который подписан на это событие, вытаскивает аргументы и асинхронно сохраняет информацию в реляционной или ещё какой-то там базе данных, к которой обращается клиентская часть
но можно и посмотреть, до чего народ дошёл сейчас, без event sourcing, может чего и придумали, я лет 5 уже ничего подобного не делал
T>>если создание заказа- это размещение заказа, т.е. placeOrder, то что тогда такое "отправить заказ" ? Вот это я вообще не понимаю: "Если мы подготовили его в январе, а потом пришёл новый прайслист, и мы отправили заказ уже в феврале", кто его подготовил? S>Ну, так это я пытаюсь догадаться, как работает предлагаемая вами архитектура. Из того, что есть какие-то события типа "пользователь изменил цену", или методы "добавить позицию", я понимаю, что агрегат "заказ" у вас обретает существование ещё до того, как будет "сохранён", или "отправлен" — в общем, до того, как начнётся собственно выполнение заказа. S>В обычных интернет-магазинах такие штуки называются чем-то типа "cart" — типа я интерактивно складываю и вынимаю товары из корзинки; а потом, когда я уже готов на выход, корзинка превращается в заказ при помощи операции checkout. S>Я не хотел усложнять задачу, т.к. у нас тогда появляется ещё два агрегата (корзина и "элемент корзины"), и мы рискуем вообще не закончить S>Поэтому условимся считать, что никакой корзинки нету, и заказ, как таковой, возникает только в момент нажатия на кнопку "заказать". А аналог корзинки у нас эфемерен, существует только в воображении клиента, и нас не интересует. S>Ну, типа там какой-то джаваскрипт, интересоваться которым — не барское дело. Наше дело — выставить API для веб-клиента; и с точки зрения этого API до момента "заказать" заказа не существует, а после этого момента изменить уже ничего нельзя.
а вот не надо упрощать и чего-то придумывать, тогда мы вообще никогда не разберёмся, есть "карта покупок", ну так пусть и будет,
теперь опять к нашим баранам: когда пользователь залогинился и увидел список товаров, то он увидел их с ценой со скидками и надбавками или эти цены появляются только тогда, когда покупатель добавил их к "карзине для покупок" ?
T>>нужны ли для калькуляции какие-то вызовы в базу данных или я могу загрузить все интересующие меня аргументы при создании агрегата? S>Ну, это же вы архитектор. Я играю роль заказчика; диктовать вам, когда лазить в базу, я в этой роли не готов. Бизнес-требования я вам задал; если они непонятны — спрашивайте. S>А как технический специалист я как раз и хочу понять, что такое этот DDD, и насколько ужасные получаюстя решения при его применении.
если делать по кошерному, я не имею права из агрегата тянуть какие-то вещи из левой для меня базы, все эти вещи либо должны быть мне доставлены либо должны быть частью моей предметной области, тогда я их могу либо у других агрегатов запросить, либо как-то скомпоновать с помощью какого-то domain service
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, Sharov, Вы писали:
S>>нормально будет? Или он только хорош, когда у нас есть n (например, n=100) сущностей? S>Я вообще хотел бы посмотреть на применение DDD к какому-нибудь небольшому и понятному проекту. Всё, что я видел из практики до сих пор — это кровавые слёзы, убедительно доказывающие "так делать не надо". S>Вот, прямо сейчас пилю информационную архитектуру для простенькой системы онлайн-заказов. Вроде бы почти всё уже в голове сложилось, но мучают сомнения: S>1. А правильно ли я всё придумал S>2. Как к этому правильному прийти через DDD
DDD это просто описание инварианта системы и некоторые танцы с бубном, что бы этот инвариант гарантировать используя ООП. Работает на любом размера проекта, но не на любом размере надо использовать все что там есть. Если из него выкинуть ооп часть то все становиться сильно проще и логичнее.
S>Вот мне интересно, какие объекты будут появляться в DDD, и как они будут работать. Будет ли rich object model, или анемика?
Вот все перечисленные объекты и должны быть в модели. Буквально.
При этом модель должна быть непротиворечивой и все типы всегда валидны, следуя принципу make invalid state unrepresentable. Вот это и будет твоя модель — внутренний круг онион-архитектуры.
Потом добавляешь слой бизнес логики, который позволят трансформировать модель из одного состояния в другое. Типа как ордер превращается в валидный ордер, а валидный в оплаченный итп. Зачем оно так делает в данном случае несущественно, главное чтобы инвариант сохранялся.
В третьем слое — application logic, собственное сами сценарии, как и когда что работает. По-хорошему, это просто композиция функций из предыдущего слоя. И последний слой это инфраструктура или порты, который связывает все это с внешним миром. Там живут все DbConnection и прочие файлы. Зависимости всегда направлены внутрь. Собственно и все. Легко тестируется, легко поддерживать, легко понять что происходит, сломать трудно. Отлично масштабируется. Если калькулятор, то это все что надо, если кровавый энтерпрайз, то это один из BC. Как-то так.
Здравствуйте, Poopy Joe, Вы писали:
S>>Вот мне интересно, какие объекты будут появляться в DDD, и как они будут работать. Будет ли rich object model, или анемика? PJ>Вот все перечисленные объекты и должны быть в модели. Буквально.
Это как раз понятно. Непонятно, какие будут методы у этих объектов. PJ>При этом модель должна быть непротиворечивой и все типы всегда валидны, следуя принципу make invalid state unrepresentable. Вот это и будет твоя модель — внутренний круг онион-архитектуры.
Утопический принцип. Валидность состояния — это миф. Она имеет место только в рамках конкретного сценария. Вот, скажем, заказ в состоянии "новый" вполне может содержать в себе позиции, которых нет на складе. Ну, нету и нету.
А вот, например, зарезервировать заказ можно только в том случае, если всё заказанное готово к резерву.
Опять же — в состоянии "новый" заказ может не иметь данных о покупателе, но для того, чтобы сделать его "оплаченным" эти данные необходимы.
Чтобы перевести его в статус "в доставке" нам нужно иметь в заказе не просто адрес доставки, а адрес, распознанный нашим шиппинг-партнёром.
Как вы собираетесь описывать все эти ограничения при помощи системы типов?
Либо у нас будет 2^N типов "заказ", и трансформации будут менять тип заказа. Либо понятие "invalid" у нас сузится до несуществующего, и тип "заказ" будет разрешать вообще всё, убрав все статические ограничения. Кроме тривиальщины — ну там, типа "заказ не может содержать 0 позиций". И то есть риск, что придётся убрать и это ограничение под давлением обстоятельств.
PJ>Потом добавляешь слой бизнес логики, который позволят трансформировать модель из одного состояния в другое. Типа как ордер превращается в валидный ордер, а валидный в оплаченный итп. Зачем оно так делает в данном случае несущественно, главное чтобы инвариант сохранялся.
То есть всё же имеем 2^N типов? PJ>В третьем слое — application logic, собственное сами сценарии, как и когда что работает. По-хорошему, это просто композиция функций из предыдущего слоя. И последний слой это инфраструктура или порты, который связывает все это с внешним миром. Там живут все DbConnection и прочие файлы. Зависимости всегда направлены внутрь. Собственно и все. Легко тестируется, легко поддерживать, легко понять что происходит, сломать трудно. Отлично масштабируется. Если калькулятор, то это все что надо, если кровавый энтерпрайз, то это один из BC. Как-то так.
Отлично. Давайте применим эти абстрактные рассуждения к рассматриваемой задаче. Какие классы (или функции) у нас будут в "модели", какие — в "бизнес логике", какие — в "application logic".
Дьявол в деталях. Направление зависимостей, loose coupling и tight cohesion упоминают поклонники всех типов дизайна.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, takTak, Вы писали:
T>если смотреть исторически, то агрегат в ддд вообще никак не связан с какой-то там базой данных, агрегат отвечает только за поведение и валидацию тех сущностей, которые находятся под его попечительством: допустим у тебя есть заказ, цена и условия которого могут измениться в будущем, но только при наличии каких-то условий, тогда пользователь, условно говоря, отправляет команду "изменить заказ с атрибутами: номер заказа , наименование операции "потребовать 25% скидки", пользовательский номер",тогда command handler переправляет агрегату событие с упомянутыми атрибутами, агрегат берёт номер заказа, загружает себя (вовсе необязательно из реляционной базы данных) из стримового потока, производит или не производит операцию, сохраняет себя в стримовом потоке, отправляет сообщение "я изменился, новая цена : хх" , и вот уже event handler, который подписан на это событие, вытаскивает аргументы и асинхронно сохраняет информацию в реляционной или ещё какой-то там базе данных, к которой обращается клиентская часть
Это предполагает наличие изменяемой сущности "заказ". Как мы уже неоднократно обсудили в этом топике, в рамках нашей задачи такой штуки нет.
T>а вот не надо упрощать и чего-то придумывать, тогда мы вообще никогда не разберёмся, есть "карта покупок", ну так пусть и будет, T>теперь опять к нашим баранам: когда пользователь залогинился и увидел список товаров, то он увидел их с ценой со скидками и надбавками или эти цены появляются только тогда, когда покупатель добавил их к "карзине для покупок" ?
Пользователь сразу видит список товаров с их ценами. Скидки/надбавки ему не видны — это закрытые подробности. Всё, что он видит — конкретную сумму. T>>>нужны ли для калькуляции какие-то вызовы в базу данных или я могу загрузить все интересующие меня аргументы при создании агрегата? T>если делать по кошерному, я не имею права из агрегата тянуть какие-то вещи из левой для меня базы, все эти вещи либо должны быть мне доставлены либо должны быть частью моей предметной области, тогда я их могу либо у других агрегатов запросить, либо как-то скомпоновать с помощью какого-то domain service
Ну, вот делайте по кошерному. Математику того, как формируется цена, я объяснил. Вот и расскажите, кто, откуда, и что будет тянуть, и как он это будет считать.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
S>Это как раз понятно. Непонятно, какие будут методы у этих объектов.
Те которые сохраняют инвариант модели. Если у тебя есть тип цена, то у него будут методы создать и вычесть скидку.
PJ>>При этом модель должна быть непротиворечивой и все типы всегда валидны, следуя принципу make invalid state unrepresentable. Вот это и будет твоя модель — внутренний круг онион-архитектуры. S>Утопический принцип. Валидность состояния — это миф.
Нет, это не миф и пракрасно работает.
S>Она имеет место только в рамках конкретного сценария. Вот, скажем, заказ в состоянии "новый" вполне может содержать в себе позиции, которых нет на складе. Ну, нету и нету. S>А вот, например, зарезервировать заказ можно только в том случае, если всё заказанное готово к резерву.
Не понял в чем тут суть возражения. Я ровно это и написал.
S>Опять же — в состоянии "новый" заказ может не иметь данных о покупателе, но для того, чтобы сделать его "оплаченным" эти данные необходимы. S>Чтобы перевести его в статус "в доставке" нам нужно иметь в заказе не просто адрес доставки, а адрес, распознанный нашим шиппинг-партнёром. S>Как вы собираетесь описывать все эти ограничения при помощи системы типов?
Вот так и буду. Я не понимаю где ты видишь проблему? NewOrder, ValidatedOrded, PaidOrder и функции NewOrder -> <ValidatedOrded, Error>, ValidatedOrded -> <PaidOrder, Error>
S>Либо у нас будет 2^N типов "заказ", и трансформации будут менять тип заказа.
Именно так.
PJ>>Потом добавляешь слой бизнес логики, который позволят трансформировать модель из одного состояния в другое. Типа как ордер превращается в валидный ордер, а валидный в оплаченный итп. Зачем оно так делает в данном случае несущественно, главное чтобы инвариант сохранялся. S>То есть всё же имеем 2^N типов?
Я не знаю откуда взялась эта формула, но ты пишешь ее так как будь-то это много или плохо. Много типов это хорошо.
Identify data invariant
Check invariant with types
Prove your code respects the invariant (using more types)
Repeat
S>Отлично. Давайте применим эти абстрактные рассуждения к рассматриваемой задаче. Какие классы (или функции) у нас будут в "модели", какие — в "бизнес логике", какие — в "application logic".
В модели те, которые сохраняют инвариант модели. В BL которые обеспечивают логику модели, т.е. ValidateOrder : (NewOrder -> <ValidatedOrded, Error>), в AL use cases: AddArticle : ((Atricle, NewOrder) -> NewOrder), Checkout, etc
Здравствуйте, Poopy Joe, Вы писали:
PJ>Нет, это не миф и пракрасно работает.
Хотелось бы более убедительных доказательств.
PJ>Вот так и буду. Я не понимаю где ты видишь проблему? NewOrder, ValidatedOrded, PaidOrder и функции NewOrder -> <ValidatedOrded, Error>, ValidatedOrded -> <PaidOrder, Error>
PJ>>>Потом добавляешь слой бизнес логики, который позволят трансформировать модель из одного состояния в другое. Типа как ордер превращается в валидный ордер, а валидный в оплаченный итп. Зачем оно так делает в данном случае несущественно, главное чтобы инвариант сохранялся. S>>То есть всё же имеем 2^N типов? PJ>Я не знаю откуда взялась эта формула, но ты пишешь ее так как будь-то это много или плохо. Много типов это хорошо.
Как откуда? Тип "ValidatedOrder" — это миф, т.к. нет концепции валидности, отдельной от конкретных сценариев.
При помощи типов мы можем описать лишь наличие либо отсутствие каких-то атрибутов.
Ну, то есть у нас есть
— наличие позиций в заказе
— наличие позиций заказа на складе
— готовность позиций заказа к отгрузке ("скомплектованность")
— наличие payment info в заказе
— наличие проведённой оплаты в заказе
— наличие адреса доставки в заказе
— наличие валидного адреса доставки в заказе
— состояние заказа "в доставке"
— состояние заказа "доставлен"
Вот у нас девять бинарных признаков. В патологическом случае это даёт 2^9 возможных комбинаций, для каждой из которых потенциально нужен свой тип. Итого — 512 типов, от "пустой заказ" до "заказ оплачен и доставлен".
Понятно, что некоторые комбинации в природе не встречаются — мы можем свернуть это пространство во что-то более компактное.
Ну, например, заметив, что не бывает такого, чтобы заказ уехал в доставку, минуя фазу зарезервированности, мы можем превратить 9 бинарных признаков в 4 признака с бОльшим количеством значений.
Тем не менее, у нас по-прежнему количество типов растёт экспоненциально. И операций типа "отгрузить" у нас получается не одна, потому что отгрузка ReadyOrder переводит его в ShippedOrder, а отгрузка PaidReadyOrder переводит его в PaidShippedOrder.
В какой системе типов мы можем это отразить? На всякий случай я рекомендую к прочтению серию статей Эрика Липперта "о колдунах и воинах", https://ericlippert.com/2015/05/11/wizards-and-warriors-part-five/.
Там подробно изложена моя точка зрения на попытки зафиксировать динамические ограничения в статической системе типов. PJ>В модели те, которые сохраняют инвариант модели. В BL которые обеспечивают логику модели, т.е. ValidateOrder : (NewOrder -> <ValidatedOrded, Error>), в AL use cases: AddArticle : ((Atricle, NewOrder) -> NewOrder), Checkout, etc
Давайте конкретно. Задача есть? Есть. Перейдём от абстрактных рассуждений про валидность ордеров, и попробуем нарисовать иерархию классов для нашего простого случая с шестью сущностями, двумя сценариями, и четырьмя бизнес-правилами.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, Poopy Joe, Вы писали:
PJ>>Нет, это не миф и пракрасно работает. S>Хотелось бы более убедительных доказательств.
Есть куча статей и видео на эту тему. Даже тут что-то приводилось. Если ты все это отказываешься смотреть, то что ты от меня ждешь?
PJ>>Вот так и буду. Я не понимаю где ты видишь проблему? NewOrder, ValidatedOrded, PaidOrder и функции NewOrder -> <ValidatedOrded, Error>, ValidatedOrded -> <PaidOrder, Error> S>
S>Как откуда? Тип "ValidatedOrder" — это миф, т.к. нет концепции валидности, отдельной от конкретных сценариев. S>При помощи типов мы можем описать лишь наличие либо отсутствие каких-то атрибутов. S>Ну, то есть у нас есть S>- наличие позиций в заказе S>- наличие позиций заказа на складе S>- готовность позиций заказа к отгрузке ("скомплектованность") S>- наличие payment info в заказе S>- наличие проведённой оплаты в заказе S>- наличие адреса доставки в заказе S>- наличие валидного адреса доставки в заказе S>- состояние заказа "в доставке" S>- состояние заказа "доставлен" S>Вот у нас девять бинарных признаков. В патологическом случае это даёт 2^9 возможных комбинаций, для каждой из которых потенциально нужен свой тип. Итого — 512 типов, от "пустой заказ" до "заказ оплачен и доставлен".
Че? Да, валидность, разумеется, зависит от твоих задач. Сферической валидности в вакууме не существует. Но причем тут бинарные признаки? Заказ валиден когда ты можешь выполнить. 9 там признаков или 109 совершенно несущественно. Два типа, ну три максимум.
NewOrder -> <ValidOrder, IncompleteOrder, Error>. IncompleteOrder -> <ValidOrder, IncompleteOrder, Error> Или тебе просто хочется доводить все до абсурда?
S>Тем не менее, у нас по-прежнему количество типов растёт экспоненциально. И операций типа "отгрузить" у нас получается не одна, потому что отгрузка ReadyOrder переводит его в ShippedOrder, а отгрузка PaidReadyOrder переводит его в PaidShippedOrder.
Вот данном случае все зависит от бизнеса опять же. В обычном случае PaidShippedOrder нафиг не нужен, потому что Shipped может получиться только из Paid. Но если возможно оплата после отправки, то да будет PaidShipped и UnpaidShipped. Но никакой экспоненты тут не будет.
В любом случае все это будет, выраженное типами или просто кодом, если только ты не собираешься рассылать бесплатно. Разница только в том, что типы дают гарантии, иначе не скомпилируется, а обычный код нет.
S>Там подробно изложена моя точка зрения на попытки зафиксировать динамические ограничения в статической системе типов.
Можно я не буду отвлекаться на теоретическое обоснование невозможности того, что я делаю каждый день?
Я так понимаю, на самом деле у тебя есть точка зрения, которую ты не собираешься менять и обсуждать что-то бессмысленно?
PJ>>В модели те, которые сохраняют инвариант модели. В BL которые обеспечивают логику модели, т.е. ValidateOrder : (NewOrder -> <ValidatedOrded, Error>), в AL use cases: AddArticle : ((Atricle, NewOrder) -> NewOrder), Checkout, etc S>Давайте конкретно. Задача есть? Есть. Перейдём от абстрактных рассуждений про валидность ордеров, и попробуем нарисовать иерархию классов для нашего простого случая с шестью сущностями, двумя сценариями, и четырьмя бизнес-правилами.
Я изложил принципы. Попробуй описать свою систему используя их и уже будем обсуждать конкретику. Иначе, если ты все сходу отвергаешь, как невозможное, то конструктивно обсуждать нечего.
Здравствуйте, Sinclair, Вы писали:
S>Здравствуйте, Poopy Joe, Вы писали:
PJ>>При этом модель должна быть непротиворечивой и все типы всегда валидны, следуя принципу make invalid state unrepresentable. Вот это и будет твоя модель — внутренний круг онион-архитектуры. S>Утопический принцип. Валидность состояния — это миф. Она имеет место только в рамках конкретного сценария. Вот, скажем, заказ в состоянии "новый" вполне может содержать в себе позиции, которых нет на складе. Ну, нету и нету. S>А вот, например, зарезервировать заказ можно только в том случае, если всё заказанное готово к резерву. S>Опять же — в состоянии "новый" заказ может не иметь данных о покупателе, но для того, чтобы сделать его "оплаченным" эти данные необходимы. S>Чтобы перевести его в статус "в доставке" нам нужно иметь в заказе не просто адрес доставки, а адрес, распознанный нашим шиппинг-партнёром. S>Как вы собираетесь описывать все эти ограничения при помощи системы типов?
на самом деле есть возможности. посмотрите например (там на F# правда, на C# так изящно неполучится, точнее можно — но это уже будет не так удобно, и даже настолько неудобно что плюсы начнут теряться). https://www.youtube.com/watch?v=Up7LcbGZFuo