Я сейчас занимаюсь написанием одного простого приложения для себя, хотелось бы следовать принципам DDD при его проектировании. Хотел бы привести сценарий использования из этого приложения, требующего более или менее нетривиальной бизнес-логики и задать тебе по нему несколько вопросов.
Вот сценарий:
Начало нового периода (Term). Новый период может быть начат, если текущий незавершенный (IsPending == true) период содерижит по крайней мере одну операцию (Operation). Для нового периода должна быть определенена сумма денег, которой располагает пользователь в данный момент. При начале нового периода, этот период становится текущим, а тот, который был текущим до этого, закрывается (для него определяются окончательная сумма - это текущая сумма на руках, и дата завершения - сегодня).
Вот вопросы:
Есть идея сделать класс Term с единственным открытым конструктором, принимающим текущую сумму денег в качестве аргумента. В этом классе определить метод Start(), который будет: находить среди всех периодов текущий, проверять, что число операций в этом текущем периоде больше нуля, и если это так, то закрывать его (проставлять дату окончания и сумму окончания) и делать this текущим.
- А что, если операций для текущего периода нет, как в этом случае оповестить вызывающую сторону (exception, еще как-то)?
- По идее, текущий период нужно определять с помощью какого-нибудь TermRepository, либо перебором всех периодов и поиском такого, у которого IsPending = true, либо сделать специальный метод GetPendingTerm у репозитория. Это вообще нормально, что в сущность Term как-то придется инжектировать репозиторий?
- В качестве OR/M планирую использовать Linq to Sql, для простоты. Он сделает мне сущность Term по таблице в БД. Нужно ли мне определять еще один класс Term, доменный, либо определять бизнес-логику прямо в сгенерированном классе от Linq to Sql?
Пишите варианты решения в комментарии или мне на почту. Код можно скидывать на http://pastebin.com, желательно с указанием в какой сборке будет лежать конкретный класс. Когда наберём достаточно вариантов, я отдельным постом выделю основные идеи и подходы к решению по этому сценарию.
Продолжение и пример реализации в статье Учебный пример по DDD. Пример реализации
1. Да, exception
ОтветитьУдалить2. В entity ничего инжектировать нельзя! Если у вас появляется желание это сделать - это значит что вы что-то делаете неправильно. В данном случае старт нового периода - это cross-cutting concern. Иными словами - операция, которая затрагивает несколько aggregation roots. Что в свою очередь означает, что обработку этой операции необходимо вынести из домена. Как именно это сделать? Я бы рекомендовал реализовать это с помощью Domain events (http://www.udidahan.com/2008/08/25/domain-events-take-2/ и http://www.udidahan.com/2009/06/14/domain-events-salvation/)
3. По правде говоря, назвать Linq To Sql ORM у меня язык не поворачивается. Я строго рекомендую не использовать его для проектов, которые подразумевают хоть какое-то развитие. Вместо него следует использовать NHibernate или (с чем я знаком гораздо меньше) Entity Framework (обязательно в сочетании с Code-First моделью). Однако для меня NHibernate выглядит гораздо более предпочтительней. Соответственно в таком случаю вопросы про сгенерированный код отпадудт сами собой.
Без бизнес-аналиста сложно чето сказать. Но я бы начал с того что над Term надо чето выше. Предположим что это будет UserTerms (оно теперь и будет рут аггрегатом), соотвевенно проперти CurrentTerm, OldTerms. Метод Start будет у UserTerms, вся инфа у него теперь есть.
ОтветитьУдалить1. Допустим у пользователя (User) есть ссылка на текущий период (Term):
ОтветитьУдалитьpartial class User { public Term CurrentTerm {get; private set; }}
Тогда отпадает необходимость в поле IsPending и инжекции репозитория куда-либо.
2. Допустим у пользователя есть коллекция предыдущих периодов:
partial class User {
private ICollection{Term} previousTerms;
public IEnumerable{Term} PreviousTerms {get{return previousTerms; }}
3. У периода нужен метод проверки на то, что он может быть закрыт:
partial class Term { public bool CanClose() { return operations.Any(); } }
4. У периода есть метод закрыть период
partial class Term { public void Close() {/*...*/ } }
5. Тогда операция закрытия будет выглядеть следующим образом:
partial class User {
public void CloseCurrentTerm() {
if(CurrentTerm.CanClose() == false)
throw new DomainException (); // тут на свой выбор
CurrentTerm.Close();
previousTerms.Add(CurrentTerm);
CurrentTerm = new Term();
}
}
Весь код:
http://pastebin.com/aZcZb4kG
Возможно, вместо выкидывания исключения, необходимо вернуть статус завершения операции (bool)
Возможно, метод CloseCurrentTerm нужно вынести в сервис
Куда будет лучше выложить код, который присылают на почту?
ОтветитьУдалитьЯ могу, например, на Google Code в свой SVN.
Наверное сделал бы как-то так)
ОтветитьУдалитьhttp://pastebin.com/yUxkYwFi
Вероятней всего, если менеджер и модель лежит в одной сборке, отдельно от остального проекта, то конструктор и метод Close можно сделать internal, так как снаружи этот функционал вроди как не нужен.
ЗЫ: @Александр Бындю: лучше в Google Code наверное. или тот же pastebin.com. нагляднее.
@Gengzu
ОтветитьУдалитьА если вызвать метод Close без вызова создания нового периода?
> лучше в Google Code наверное. или тот же pastebin.com. нагляднее
Там целые солюшены с несколькими проектами, в pastebin.com будет не очень. Да, я видимо в Google Code выложу.
@Александр Бындю: ну, тогда можно что бы Close сам возвращал новый объект Term. тогда мы не сможем создать новый не закрыв старый, и наоборот.
ОтветитьУдалитьдругое дело сохранит их кто-то после этого, или нет. а может сохранит но какой-то один из них...)
@Александр Бындю, нет смысла решать эту задачу, т.к. она уже решена автором. Большинство в своем решении будет опираться на решение, описанное в самой задаче, при этом наследуя все его ошибки.
ОтветитьУдалить@hazzik
ОтветитьУдалитьНу ты же предложил свое решение :) Пусть другие тоже попробуют.
На самом деле @hazzik прав - задача сформулирована некорректно. С точки зрения DDD важно понимать предметную область, однако в данном примере это довольно затруднительно сделать. Мое решение было основано на том, что Term - это aggregation root, однако с точки зрения бизнеса это вполне может быть не так.
ОтветитьУдалитьСпасибо всем за ответы, особенно @hazzik: его вариант мне понравился больше всего. Вопросы:
ОтветитьУдалить1) В какой точке текущее состояние доменных объектов будет сохранятся в базу? Какой-то сервис, в который инжектируется репозиторий?
2) Допустим предполагается, что на уровне базы не будет таблице Users, таким образом, сущность "User", если так можно выразиться, чисто доменная. Вся необходимая информация для User хранится в таблицах Terms, Operations. Каким образом в коде, в этом случае, происходит отображение объектов User на данные в базе? (Какой-то специальный репозиторий, делегирующий репозитории для Term, Operation)?
По поводу некорректно сформулированной задачи. Мне кажется, что в данном случае выступаю в роли заказчика (а не бизнес-аналитика), а заказчики зачастую предъявляют неполные или противоречивые требования. Пожалуйста, задавайте вопросы, которые скорректируют требования. К сожалению, в данной ситуации я слышу недовольства, но не слышу конкретных вопросов или предложений.
@Сергей Соловьев
ОтветитьУдалитьзаказчики не изъясняются в названиях классов и их свойствах. В их пониманиях есть срок (или какой-то период времени), есть понимание о категориях пользователей, которые могут выполнять определенные операции. В общем они изъясняются в терминах предметной области, а не программирования. Соответственно я бы хотел видеть задачу, описанную именно в терминах предметной области.
Изучаю DDD. Не могу придумать как правильно реализовать следующие правила.
ОтветитьУдалитьИмеются 3 роли: Админ, модератор, прользователь.
Имеется Анкета.
Данные в анкете могут редактировать только пользователи Администраторы и Модераторы прикрепленые к анкете. Так же в системе существуют обычне пользователи которые не могут редактировать анкеты, но просмотривать информацию из них могут.
Не могу додуматся где запрещать редактирования данных при сохроняемости обекта, или как то в программе.
У кого нибудь есть идеи, буду признателен. Можно на мыло писать nomit007@gmail.com
Придумал такое http://pastebin.com/aKGSiFxD , незнаю на сколько адекватно.
ОтветитьУдалить