Re[11]: DDD для небольших проектов.
От: Sinclair Россия https://github.com/evilguest/
Дата: 14.02.20 12:28
Оценка:
Здравствуйте, Poopy Joe, Вы писали:
PJ>Не каждое. Каждое которое можно отправить. Ну так оно и есть особое. Ты не можешь отправить и закрыть. Где-то надо сохранить, чтобы убедиться, что деньги получены. Это состояние в любом случае будет в твоей стейт-машине, иначе оно просто не будет правильно работать.
Отож.

PJ>Какая разница? Это BL.

Нууу, как же — большая разница.

PJ>Так одно состояние может быть комплексным. Есть есть IncompleteOrder, то в этом состоянии заказ может оставать до тех пор пока он физически не сможет перейти в состояние Valid

PJ>допуcтим
PJ>
PJ>namespace Model
PJ>{
PJ>    public class NewOrder
PJ>    {
PJ>        public NotEmptyList<Article> Articles { get; set; }    
PJ>        public Optional<Customer> Customer { get; set; }
PJ>    }
    
PJ>    public struct IncompleteOrder
PJ>    {
PJ>        public NotEmptyList<Article> Articles { get; set; }
PJ>        public Customer Customer { get; set; }
PJ>        public Optional<Address> Address{ get; set; }
PJ>        public Optional<StockReference> Reserved { get; set; } 
PJ>    }
    
PJ>    public class ValidOrder
PJ>    {
PJ>        public ValidOrder(NotEmptyList<Article> article, Address address, Customer customer, StockReference reference){}

PJ>        public NotEmptyList<Article> Articles { get; }
PJ>        public Address Address{ get; }
PJ>        public Customer Customer { get; }
PJ>        public Optional<StockReference> Reserved { get; } 
PJ>    }

PJ>    public class PaidOrder
PJ>    {
PJ>        public PaidOrder(ValidOrder order, PaymentInfo paymentInfo){}
PJ>    }

PJ>    public class UnpaidOrder
PJ>    {
PJ>        public UnpaidOrder(ValidOrder order, ExpectedPaymentInfo paymentInfo){}
PJ>    }

PJ>    public class ShippableOrder
PJ>    {
PJ>        public OneOf<PaidOrder, UnpaidOrder> Order {get;}
PJ>        public ShippableOrder(PaidOrder order){}
PJ>        public ShippableOrder(UnpaidOrder order){}
PJ>    }
    
PJ>    public class ShippedOrder
PJ>    {
PJ>        public ShippedOrder(ShippableOrder order){}
PJ>    } 

PJ>    public class DeliveredOrder
PJ>    {
PJ>        public DeliveredOrder(ShippedOrder order){}
PJ>    } 
    
PJ>}
PJ>

Прекрасно. Всё начинает проясняться.
PJ>Есть заказ, который по сути просто список позиций. Покупатель может быть залогинен, а может и нет. Но чтобы начать выполнение он должен быть залогинен. Поэтому поле Customer в IncompleteOrder и Valid уже не опциональное. Если все нужное есть, то получаем Valid, если нет, то Incomplete с пустыми полями там где нет данных. Нетрудно вывести, что именно надо запросить у пользователя или где-то еще. IncompleteOrder можно гонять по всей системе он совершенно безопасен, потому что послать можно только ShippableOrder, а что бы его получить надо получить Valid в котором опциональных полей нет. Тебе просто придется написать правильную бизнес-логику, чтобы это скомпилировалось. Каждый из типов отражает именно бизнес реальность. Нет никах 512ти типова заказа. В каждом отдельном случае логика совершенно однозначна и понятна, хотя может и потребовать каких-то оптимизаций, это совершенно неважно.
Ну, вот у нас уже получилось 8 типов заказа — ровно 2-в-степени-количество-степеней-свободы.
И эта модель у нас теряет информацию: после отправки у нас уже нет никаких данных о том, был ли заказ оплачен. Ну, то есть наверное можно попробовать это починить, добавив в ShippedOrder и DeliveredOrder свойство Order по образцу ShippableOrder. Но не факт, что этого достаточно: если мы хотим потребовать платёж-при-получении от наших Shippable(UnpaidOrder), то как будет выглядеть сигнатура конструктора DeliveredOrder?
Надо полагать, будет что-то типа
        public DeliveredOrder(ShippedPaidOrder order){} // тут paymentInfo и так есть
        public DeliveredOrder(ShippedUnpaidOrder order, PaymentInfo paymentInfo){} // а тут надо её предоставить

Может, я чего-то не понимаю, но у нас только что класс ShippedOrder развалился надвое. А заодно придётся развалить пополам и класс ShippableOrder.
Итого — уже 10 классов. И это мы ещё не начали думать о возвратах. Либо мы переносим данные из статического типа в динамическое состояние — как вы поступили с ShippableOrder.
Теперь, чтобы узнать, нуждается ли ShippableOrder в оплате, придётся делать рантайм-анализ:
var paymentInfo = (shippableOrder.Order is PaidOrder paidOrder) ? paidOrder.PaymentInfo : UI.RequestPaymentInfo();

PJ>А я показал, что это не так.
Пока что вы подтверждаете мои опасения.
S>>Давайте вы напишете код NewOrder, ShippedOrder, PaidOrder, и PaidShippedOrder. А то я решительно не понимаю, что вы имеете в виду. Вот вы написали, что в этих типах будут методы, "которые сохраняют инвариант модели". Поскольку инварианты у PaidOrder и PaidShippedOrder частично совпадают, то я не вижу нормального способа избежать дублирования кода.
PJ>Кода чего? Это просто данные. Модель.
Ну как же. Вот у вас почти у всех ордеров есть конструкторы. Вы их так пишете, как будто достаточно перечислить аргументы, и всё заработает. Ну дак ведь нет — надо же весь этот бойлерплейт писать руками:
public ValidOrder(NotEmptyList<Article> articles, Address address, Customer customer, StockReference reference)
{
  Articles = articles ?? throw new ArgumentNullException(nameof(articles));
  Address = address ?? throw new ArgumentNullException(nameof(address));
  Customer = customer ?? throw new ArgumentNullException(nameof(customer));
  Reserved = reference ?? throw new ArgumentNullException(nameof(reference)); 
}

Это, ессно, в предположении, что сами NotEmptyList<T>, Address, Customer, и StockReference прилагают аналогичные усилия при конструировании, чтобы не дать присвоить булшит.
При этом компилятор всё ещё ни за чем из этого не следит — я могу запросто забыть проинициализировать свойство в конструкторе, и оно прекрасно останется null. В итоге код компилируется, хотя и падает при первом же юнит тесте.
S>>Кто из них Влашин? Я всё, что мне давали текстового, прочитал.
PJ>Тот который говорит про F#.
Ок, почитаем. Выглядит многообещающе.

PJ>Так мне тоже, но вот ты, не стесняясь, требуешь примеров кода, хотя на них времени надо больше, чем на просто текст.

К сожалению, без кода вести технические дискуссии бессмысленно.

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

Ну ок. Модель — это, вроде бы, entities. Агрегаты в вашей версии DDD есть? Или нету?
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.