Учебный пример по DDD

22 января 2011 г.

На почту написал Сергей Соловьев:

Я сейчас занимаюсь написанием одного простого приложения для себя, хотелось бы следовать принципам 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. Пример реализации

    14 комментариев:

    1. 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 выглядит гораздо более предпочтительней. Соответственно в таком случаю вопросы про сгенерированный код отпадудт сами собой.

      ОтветитьУдалить
    2. Без бизнес-аналиста сложно чето сказать. Но я бы начал с того что над Term надо чето выше. Предположим что это будет UserTerms (оно теперь и будет рут аггрегатом), соотвевенно проперти CurrentTerm, OldTerms. Метод Start будет у UserTerms, вся инфа у него теперь есть.

      ОтветитьУдалить
    3. 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 нужно вынести в сервис

      ОтветитьУдалить
    4. Куда будет лучше выложить код, который присылают на почту?

      Я могу, например, на Google Code в свой SVN.

      ОтветитьУдалить
    5. Наверное сделал бы как-то так)

      http://pastebin.com/yUxkYwFi

      Вероятней всего, если менеджер и модель лежит в одной сборке, отдельно от остального проекта, то конструктор и метод Close можно сделать internal, так как снаружи этот функционал вроди как не нужен.

      ЗЫ: @Александр Бындю: лучше в Google Code наверное. или тот же pastebin.com. нагляднее.

      ОтветитьУдалить
    6. @Gengzu

      А если вызвать метод Close без вызова создания нового периода?

      > лучше в Google Code наверное. или тот же pastebin.com. нагляднее

      Там целые солюшены с несколькими проектами, в pastebin.com будет не очень. Да, я видимо в Google Code выложу.

      ОтветитьУдалить
    7. @Александр Бындю: ну, тогда можно что бы Close сам возвращал новый объект Term. тогда мы не сможем создать новый не закрыв старый, и наоборот.
      другое дело сохранит их кто-то после этого, или нет. а может сохранит но какой-то один из них...)

      ОтветитьУдалить
    8. @Александр Бындю, нет смысла решать эту задачу, т.к. она уже решена автором. Большинство в своем решении будет опираться на решение, описанное в самой задаче, при этом наследуя все его ошибки.

      ОтветитьУдалить
    9. @hazzik

      Ну ты же предложил свое решение :) Пусть другие тоже попробуют.

      ОтветитьУдалить
    10. На самом деле @hazzik прав - задача сформулирована некорректно. С точки зрения DDD важно понимать предметную область, однако в данном примере это довольно затруднительно сделать. Мое решение было основано на том, что Term - это aggregation root, однако с точки зрения бизнеса это вполне может быть не так.

      ОтветитьУдалить
    11. Спасибо всем за ответы, особенно @hazzik: его вариант мне понравился больше всего. Вопросы:
      1) В какой точке текущее состояние доменных объектов будет сохранятся в базу? Какой-то сервис, в который инжектируется репозиторий?
      2) Допустим предполагается, что на уровне базы не будет таблице Users, таким образом, сущность "User", если так можно выразиться, чисто доменная. Вся необходимая информация для User хранится в таблицах Terms, Operations. Каким образом в коде, в этом случае, происходит отображение объектов User на данные в базе? (Какой-то специальный репозиторий, делегирующий репозитории для Term, Operation)?

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

      ОтветитьУдалить
    12. @Сергей Соловьев
      заказчики не изъясняются в названиях классов и их свойствах. В их пониманиях есть срок (или какой-то период времени), есть понимание о категориях пользователей, которые могут выполнять определенные операции. В общем они изъясняются в терминах предметной области, а не программирования. Соответственно я бы хотел видеть задачу, описанную именно в терминах предметной области.

      ОтветитьУдалить
    13. Изучаю DDD. Не могу придумать как правильно реализовать следующие правила.


      Имеются 3 роли: Админ, модератор, прользователь.
      Имеется Анкета.
      Данные в анкете могут редактировать только пользователи Администраторы и Модераторы прикрепленые к анкете. Так же в системе существуют обычне пользователи которые не могут редактировать анкеты, но просмотривать информацию из них могут.


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


      У кого нибудь есть идеи, буду признателен. Можно на мыло писать nomit007@gmail.com

      ОтветитьУдалить
    14. Придумал такое http://pastebin.com/aKGSiFxD , незнаю на сколько адекватно.

      ОтветитьУдалить

    Моя книга «Антихрупкость в IT»

    Как достигать результатов в IT-проектах в условиях неопределённости. Подробнее...