Хабр Курсы для всех
РЕКЛАМА
Практикум, Хекслет, SkyPro, авторские курсы — собрали всех и попросили скидки. Осталось выбрать!
Передача OrdersContext в метод доменного объекта нарушает принцип разделения ответственности, т.к. класс Order в этом случае содержит информацию о том, как он сохраняется в базе.
public virtual void RemoveLine(OrderLine line, OrdersContext db)
{
Lines.Remove(line);
db.OrderLines.Remove(line);
}
— Entity Framework побуждает разработчиков использовать идентификаторы. Частично из-за того, что это дефолтный способ для того, чтобы ссылаться на связанные сущности, частично из-за того, что все примеры в документации используют именно этот подход. В NHibernate, напротив, дефолтный способ объявления ссылки на связанную сущность — это ссылка на объект этой сущности
Customer customer = CreateCustomer(dto);
session.Save(customer);
ids.Add(customer.Id);
Если вы используете для этой задачи Entity Framework, то он будет вставлять записи в БД по мере сохранения их в сессии для того, чтобы получить Id, т.к. единственная доступная стратегия генерации целочисленных идентификаторов в EF — database identity.
Работа с закешированными объектами
public Order(Customer customer, OrdersContext context)
{
context.Entry(customer).State = EntityState.Unchanged;
Customer = customer;
}
… устанавливает зависимость между доменной и инфраструктурной логикой
что необходимость отложенности загрузки зависит от места использования
foreach(var order in db.Orders(customerLoadOption: LoadOption.DontLoad))
{
//NullReferenceException при попытке обратиться к order.Customer
Console.WriteLine("{0}: {1}", order.Id, order.Customer.Name);
}
foreach(var order in db.Orders(customerLoadOption: LoadOption.Load))
{
//никакого NullReferenceException
//зато из базы - одним запросом - вытащены все данные по заказам и покупателям
Console.WriteLine("{0}: {1}", order.Id, order.Customer.Name);
}
foreach(var order in db.Orders().Select(o => new {orderId = o.Id, customerName = o.Customer.Name}))
{
//из базы вытащено ровно два поля
Console.WriteLine("{0}: {1}", order.orderId, order.customerName);
}
foreach(var order in db.Orders(customerLoadOption: LoadOption.Lazy))
{
//никакого NullReferenceException
//зато для каждого заказа в цикле делается дополнительное обращение в БД
Console.WriteLine("{0}: {1}", order.Id, order.Customer.Name);
}
foreach(var order in db.Orders(customerLoadOption: LoadOption.Lazy))
{
if (order.IsSuspicious)
Console.WriteLine("{0}: {1}", order.Id, order.Customer.Name);
}
public class Account
{
...
public double GetTaxes()
{
if (SomeComplexCondition())
{
GetTaxes(this.DomainTrasactions) // load this.DomainTransaction lazily
}
else
{
GetTaxes(this.ForiegnTrasactions) // load this.ForiegnTrasactions lazily
}
}
...
}
Именно Lazy, т.е. отложенная загрузка, или Partial — когда грузится только то, что нужно для работы?
Если первое, то вы не могли бы привести пример?
Если из-за lazy будет много догрузок, я это замечу и добавлю в запрос join (обычно до этого не доходит: если понятно, что поле будет использоваться и это не справочник, join сразу пишется в запрос)
var qOrder = DetachedCriteria.For<Order>();
var qClient = qOrder.CreateCriteria("Client");
qOrder.Add(Restriction.Gt("Sum", 1000000));
var OrdersWithClients = Session.List<Order>(qOrder);Соответственно если в процессе операции вы решите отменить эту операцию, то вам придется удалить все сохраненные к этому моменту записи вручную.
Минутка некропостинга
Не выкупаю смысл использования целочисленных идентификаторов (если конечно это не легаси)? Почему не использовать uuid/guid который будет 100% уникальным, не создаст проблем в будущем, и может генерироваться через конструктор или иными способами.. т.е. будет заранее вам известен.
Я не предъявляю претензий, но хочу понять "лучшие практики", но вижу в треде споры о вкусовщине.
Пример 3: Read-only коллекция связанных сущностей
LinesInternal (тогда можно будет конфигуратор вынести за пределы класса), либо (б) построения выражения для маппинга не через лямбду, а вручную.Пример 4: Паттерн Unit of Work
public IList<int> MigrateCustomers(IEnumerable<CustomerDto> customerDtos, CancellationToken token)
{
List<Customer> customers = new List<Customer>();
using (var db = new CustomerDbContext())
{
foreach (CustomerDto dto in customerDtos)
{
token.ThrowIfCancellationRequested();
Customer customer = CreateCustomer(dto);
customers.Add(db.Add(customer))
}
db.SaveChangesAsync(token).Wait(); //метод снаружи не асинхронный - а зря
}
return customers.Select(c => c.Id).ToList();
}
Пример 5: Работа с закешированными объектами
[...]
С Entity Framework вы не можете присвоить новому объекту ссылку на detached объект. Если вы напишете код как на примере выше, EF попытается вставить клиента в БД, т.к. он не был приаттачен к текущему контексту.
Изоляция доменной логики означает, что доменные классы могут взаимодействовать только с другими доменными классами
Теория DDD красива только в теории. На практике сделать прикладную логику только на взаимодействии классов, которые не знают о базе данных — невозможно, кроме самых примитивных моделей.
Как распределеть отвественность между сервисами, репозитариями и сущностями?
Кто вызвает сущности\репозитарии\сервисы?
Как раз интересует кто кого вызывает в связке app services — domain services — domain entities — repository. Я видел трехдневный холивар на эту тему, причем не в интернатах а вживую.
Вот требуется вывести список заказов с суммами заказов, рассчитаными на основе OrderLines. Вопрос: где должен быть расчет — в entity, domain service или repository?
А как вы собираетесь «изолировать логику» проверки уникальности? Например надо не продать два билета на одно место на концерт.
Или как сделать целостность на несколько сущностей? Например не создавать заказ, если на складе больше нет данной позиции.
в DDD нет нормальной возможности выразить всю логику в терминах операций с доменными объектами.Почитайте Эванса, помимо доменного уровня в DDD есть и другие уровни. Не нужно запихивать всю логику в доменный уровень.
1) Как распределеть отвественность между сервисами, репозитариями и сущностями?У Эванса подробно и детально разжёвано, кто и к кому обращается и какой слой за что несёт ответственность. Процитирую отрывок: «уровни прикладных операций и предметной области обращаются к СЛУЖБАМ, предоставляемым инфраструктурным уровнем.»
2) Кто вызвает сущности\репозитарии\сервисы?
У Эванса, кстати, нет ответа на эти вопросы.
И в какой уровень по вашему нужно запихнуть проверку уникальности?Если мы проверяем, не продан ли билет на это место перед тем, как начать создание и сохранение билета, то это бизнес-логика приложения: «нельзя продавать два билета на одно и то же место». Проверка в процессе сохранения того, что мы не пытаемся сохранить в персистентное хранилище два билета на одно и то же место — это уже не бизнес-логика приложения, а всего лишь необходимость, возникающая из-за того, что несколько клиентов могут попытаться почти одновременно создать два билета на одно место. А так как это не бизнесс-логика и не координирование задач, то, согласно DDD, проверку уникальности в момент сохранения нужно поместить в инфраструктурный уровень. Процитирую определение Эванса, которое он даёт инфраструктурному уровню: инфраструктурный уровень обеспечивает непосредственную техническую поддержку для верхних уровней: передачу сообщений на операционном уровне, непрерывность существования объектов на уровне модели, вывод элементов управления на уровне пользовательского интерфейса и т.д. Итак, проверка должна находиться в инфраструктурном уровне.
расчет суммы заказа по позициям где должен выполняться?Расчёт суммы заказа это бизнес-логика, она должна находиться в доменном уровне.
Таким образом, проверка уникальности должна находиться в коде персистентного хранилища
Расчёт суммы заказа это бизнес-логика, она должна находиться в доменном уровне.
Прочитайте лучше книжку Эванса. Как я и говорил, он все эти моменты довольно подробно разжёвывает, в том числе объясняет когда следует использовать доменные сервисы.
Ну собственно что и требовалось доказать, важная часть логики (на самом деле САМАЯ важная часть) выпала из структуры DDD вообще.Инфраструктурный уровень, по вашему, не имеет ничего общего со структурой DDD? DDD — это концепция проектирования программ, и понятие инфраструктурного уровня — часть этой концепции.
Вы не ответили на конкретный вопрос, это будет класс сущности, domain service или репозиторий?Алгоритм расчёта суммы заказа будет или в доменной сущности, или в доменном сервисе — в зависимости от предметной области и сложности самого расчёта.
Алгоритм расчёта суммы заказа будет или в доменной сущности, или в доменном сервисе — в зависимости от предметной области и сложности самого расчёта.
DDD не описывает инфраструктурный уровень если что. Максимум что описывает DDD — репозиторий, как компонент между инфраструктурой и доменом.Повторюсь, DDD — это концепция проектирования. Цитируя Эванса, DDD — это «система взглядов и подходов». Один из таких подходов — разделить приложение на уровни, в том числе на инфраструктурный уровень. И репозиторий не является компонентом между инфраструктурным и доменным уровнями.
Теория DDD красива только в теории.
Расчет — как обычно просто сумма по позициямТогда в доменной сущности.
Повторюсь, DDD — это концепция проектирования.Это болтология.
Тогда в доменной сущности.И вы только что сделали получение списка заказов в разы медленнее, чем стоило бы.
Пример 1: Удаление дочерней сущности из корня агрегатаОбъясните мне простую вещь. Зачем грузить сущность заказа и все его позиции, чтобы удалить одну позицию?
Пример 2: Ссылка на связанную сущностьЭто просто вранье. В EF ты почему-то рассматриваешь Code First, который очевидно генерит схему по свойствам и нужно все поля выписывать в объектах, чтобы сделать мэпинг. А в database first вполне можно замапить связанную сущность без поля внешнего ключа. (также как в NH) Более того, в EF первой версии только так и работало.
Пример 3: Read-only коллекция связанных сущностейВо-первых это настолько редкий кейс, что любой программист суммарно напишет гораздо меньше кода для реализации всех таких кейсов, чем ты написал для этого примера.
protected virtual ICollection<OrderLine> LinesInternal { get; set; }
public virtual IReadOnlyList<OrderLine> Lines
{
get { return LinesInternal.ToList(); }
}
private IList<OrderLine> _lines;
public virtual IReadOnlyList<OrderLine> Lines
{
get { return _lines.ToList(); }
}
Пример 4: Паттерн Unit of WorkПрости, но это просто бред. Это в NH надо вызывать сохранение объекта в сессии, чтобы id генерировались. А в EF этого делать не надо. Поэтому в EF надо только оставить SaveChanges в конце цикла и все. Даже транзакция не нужна.
Пример 5: Работа с закешированными объектамиСнова выдаешь желаемое за действительное.
Как можно видеть, по умолчанию, в Entity Framework нужно добавить дополнительное свойство с идентификатором
С NHibernate вы можете выбрать стратегию Hi/Lo, так что записи просто не будут сохраняться в БД до момента закрытия сессии. Идентификаторы в этом случае генерируются на клиенте, так что нет необходимости сохранять записи в БД для того, чтобы получить их.
Entity Framework 6 (7) vs NHibernate 4: взгляд со стороны DDD