Главная страница   /   3.3. Проблемно-ориентированное программирование (DDD) (ASP.NET MVC 4

ASP.NET MVC 4

ASP.NET MVC 4

Адам Фриман

3.3. Проблемно-ориентированное программирование (DDD)

Мы уже описали, как доменная модель представляет реальный мир в вашем приложении, она содержит представления ваших объектов, процессов и правил. Доменная модель является сердцем MVC приложения, а все остальное, в том числе представления и контроллеры – это всего лишь средства для взаимодействия с доменной моделью.

ASP.NET MVC не диктует того, какая технология должна использоваться для доменной модели. Вы вольны выбрать любую технологию, которая будет взаимодействовать с .NET Framework, и тут есть много вариантов. Тем не менее, ASP.NET MVC предлагает инфраструктуру и соглашения, чтобы помочь подключить классы доменной модели к контроллерам и представлениям, а также к самому MVC Framework. Есть три ключевые функциональные возможности:

  • Связывание данных модели является функцией, которая автоматически заполняет объекты модели, используя входные данные, как правило, отправленные из HTML формы.
  • Метаданные модели позволяют описать фреймворку смысл классов модели. Например, вы можете предоставить читабельное описание их свойств или дать подсказки о том, как они должны отображаться. MVC Framework может автоматически представить изображение или редактор UI для классов модели в представлениях.
  • Валидация, которая выполняется во время связывания данных и применяет правила, которые могут быть определены как метаданные.

Мы кратко коснулись связывания данных и валидации, когда мы строили наше первое MVC приложение в главе 2, и мы вернемся к этим темам и дальнейшему их изучению в главах 22 и 23. На данный момент, мы собираемся отложить реализацию ASP.NET MVC в сторону и подумать о доменном моделировании как о самостоятельной деятельности. Мы собираемся создать простую доменную модель с помощью .NET и SQL Server, используя некоторые основные технологические приемы из мира DDD.

Построение доменной модели

У вас, наверное, уже был опыт мозгового штурма по созданию доменной модели. Обычно сюда включены разработчики, бизнес эксперты, большое количество кофе, печенья и ручки с маркерами. Через некоторое время люди в комнате приходят к начальному общему знаменателю, и тогда возникает первый проект доменной модели. (Мы опускаем рассказ о многочасовых разногласиях. Достаточно сказать, что разработчики будут тратить первые часы, требуя от бизнес экспертов реальных задач, а не взятых из научной фантастики, в то время как бизнес эксперты будут выражать удивление и обеспокоенность, что время и смета расходов аналогичны тем, что NASA требует, чтобы достичь Марса. Кофе имеет важное значение в решении таких противоречий и в таком противостоянии, в конечном итоге, мочевой пузырь у всех будет настолько полным, что все пойдут на компромисс, и наметится прогресс в решении задачи).

Вы могли заканчивать чем-то похожим, что изображено на рисунке 3-5 и что является отправной точкой для данного примера – простая доменная модель для аукционного приложения.

Рисунок 3-5: Первый набросок модели для аукционного приложения

Эта модель содержит набор элементов Members, каждый из которых содержит набор элементов Bids. Каждый Bid предназначен для одного Item, а каждый Item может содержать несколько Bid от разных Members.

Повсеместно используемый язык

Ключевым преимуществом реализации вашей доменной модели в качестве отдельного компонента является то, что вы можете установить по своему выбору язык и терминологию. Вам стоит попытаться найти терминологию для ее объектов, операций и отношений, которые будут понятны не только разработчикам, но и бизнес экспертам. Мы рекомендуем вам адаптировать доменную терминологию, когда модель уже существует. Например, если то, что разработчик будет называть пользователями и ролями (users и roles), известно в домене как агенты и разрешения (agents и clearances), то мы рекомендуем вам принять последний вариант в вашей доменной модели.

При моделировании таких понятий, для которых у экспертов нет нужной терминологии, вам нужно прийти к общему соглашению о том, как вы будете их называть, создавая повсеместно используемый язык, который будет проходить через доменную модель.

Разработчики, как правило, говорят на языке кода – именами классов, названиями таблиц баз данных и так далее. Бизнес эксперты не понимают эти термины, да они и не должны. Бизнес эксперт с небольшими техническими знаниями – это опасная вещь, потому что он будет постоянно фильтровать требования через свое понимание того, на что способна технология. И если так случится, вы не получите истинного понимания того, что нужно бизнесу.

Этот подход также помогает избежать в приложении сверхобобщения. Программисты, как правило, зачастую хотят смоделировать все возможные реалии бизнеса, а не конкретно то, что требует бизнес. В модели аукциона такое могло бы произойти, если бы мы изменили members и items общими понятиями resources и relationships. Когда мы создаем доменную модель, которая не совсем соответствует моделируемому домену, мы упускаем возможность получить любую реальную картину бизнес процесса и, в будущем, представление бизнес процесса может оказаться неправильным. Эти ограничения не являются запретом, они являются маяком, который направляет вашу работу в правильное русло.

Связь между повсеместно используемым языком и доменной моделью не должна быть поверхностной: DDD эксперты предполагают, что любые изменения в повсеместно используемом языке должны приводить к изменению модели. Если вы позволите модели не быть синхронизированной с доменом, вы создадите промежуточный язык, который будет различаться у модели и домена, и это может в дальнейшем привести к серьезным проблемам. Вы создадите специальный класс людей, которые могут говорить на обоих языках, и тогда они начнут фильтровать требования, основываясь на своем неполном понимании обоих языков.

Агрегаты и упрощение

На рисунке 3-5 представлена хорошая отправная точка для нашей доменной модели, но тут нет никаких полезных указаний о реализации модели с использованием C# и SQL Server. Если мы загрузим в память элемент Member, должны ли мы также загрузить Bids и Items, связанные с ним? И если это так, нужно ли нам загрузить все другие Bids для этих Items, а также Members, которые связаны с этими Bids? Когда мы удаляем объект, должны ли мы удалить также связанные с ним объекты, и если да, то какие? Если мы выберем для реализации хранилище документов вместо реляционной базы данных, какая коллекция объектов будет представлять собой единый документ? Мы этого не знаем, а наша доменная модель не дает нам никаких ответов на эти вопросы.

В данном случае DDD предлагает организовать доменные объекты в группы, называемые агрегатами. На рисунке 3-6 показано, как мы можем объединить объекты в нашей доменной модели аукциона.

Рисунок 3-6: Аукционная доменная модель с агрегатами

Суть агрегата заключается в группировке нескольких объектов доменной модели: есть ключевая сущность (root entity), которая используется для определения цельного агрегата, и она действует как «босс» для валидации и сохранения операций. Агрегат рассматривается как единое целое с точки зрения изменения данных, поэтому мы должны создать агрегаты, которые представляют собой отношения, имеющие смысл в контексте доменной модели, и создать операции, которые логически соответствуют реальным бизнес процессам: иными словами, мы должны создавать агрегаты, группируя объекты, которые изменяются как группа.

Ключевое правило DDD гласит, что объекты за пределами конкретного экземпляра агрегата могут быть связаны только с root entity, а не с любым другим объектом внутри агрегата (на самом деле, идентичность не корневого объекта должна быть уникальной только в пределах своего агрегата). Это правило подтверждает представление о работе с объектами внутри агрегата как с единым целым.

В нашем примере Members и Items являются ключевыми сущностями агрегата, в то время как Bids могут быть доступны только в контексте элемента Item, который является корневым в своем агрегате. Элементы Bids могут быть связаны с элементами Members (которые являются основными сущностями, root entity), но Members не могут непосредственно ссылаться на Bids (поскольку они не являются корневыми).

Одно из преимуществ агрегатов состоит в том, что они упрощает систему отношений между объектами в доменной модели, и часто они могут дать дополнительное понимание природы домена, который моделируется. В сущности, создание агрегатов ограничивает отношения между объектами доменной модели, так что они становятся больше похожими на отношения, которые существуют в реальном домене. В листинге 3-1 показано, как выглядит наша доменная модель, выраженная в C#.

Листинг 3-1: Аукционная доменная модель, выраженная в C#
public class Member
{
	public string LoginName { get; set; } // Уникальный ключ
	public int ReputationPoints { get; set; }
}

public class Item
{
	public int ItemID { get; private set; } // Уникальный ключ
	public string Title { get; set; }
	public string Description { get; set; }
	public DateTime AuctionEndDate { get; set; }
	public IList<Bid> Bids { get; set; }
}

public class Bid
{
	public Member Member { get; set; }
	public DateTime DatePlaced { get; set; }
	public decimal BidAmount { get; set; }
}

Обратите внимание, как мы легко смогли понять однонаправленный характер отношений между Bids и Members. Мы также смогли смоделировать некоторые другие ограничения, например, элементы Bids остаются неизменными (это представляет собой общую стратегию аукциона, когда ставки не могут быть изменены, как только они сделаны). Применение агрегации позволило нам создать более полезную и точную доменную модель, которую мы смогли с легкостью представить в C#.

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

С другой стороны, они накладывают ограничения, которые иногда могут показаться искусственными, потому что зачастую они и есть искусственные. Агрегаты обычно появляются в базах данных документа, но они не являются родным понятием в SQL Server и в большинстве инструментов ORM. Поэтому если вы хотите их хорошо реализовать, вашей команде понадобится дисциплина и эффективный обмен информацией.

Определение репозиториев

В какой-то момент нам нужно будет обеспечить постоянство нашей доменной модели: это, как правило, осуществляется с помощью реляционных, объектных или документальных баз данных. Постоянство не является частью нашей доменной модели – это самостоятельное или ортогональное понятие в нашем принципе разделения понятий. Это означает, что мы не хотим смешивать код, который обрабатывает постоянство, с кодом, который определяет доменную модель.

Обычным способом обеспечить разделение между доменной моделью и системой постоянства является определение репозиториев – это объекты представления основной базы данных (или хранилища файлов, которое вы выбрали). Вместо того чтобы работать непосредственно с базой данных, доменная модель вызывает методы, определенные в репозитории, который в свою очередь делает запросы в базу данных для хранения и извлечения данных модели. Это позволяет изолировать модель от реализации постоянства.

Соглашение заключается в определение отдельной модели данных для каждого агрегата, потому что агрегаты являются естественными единицами для сохранения постоянства. В случае нашего аукциона, например, мы можем создать два репозитория: один для Members и один для Items (обратите внимание, что нам не нужен репозиторий для Bids, потому что они будут сохранены как часть агрегата Items). В листинге 3-2 показано, как могут быть определены эти репозитории.

Листинг 3-2: C# классы репозиториев для доменных классов Member и Item
public class MembersRepository
{
	public void AddMember(Member member)
	{
		/* Реализуй меня */
	}
	public Member FetchByLoginName(string loginName)
	{
		/* Реализуй меня */
		return null;
	}

	public void SubmitChanges()
	{
		/* Реализуй меня */
	}
}
public class ItemsRepository
{
	public void AddItem(Item item)
	{
		/* Реализуй меня */
	}
	public Item FetchByID(int itemID)
	{
		/* Реализуй меня */
		return null;
	}
	public IList<Item> ListItems(int pageSize, int pageIndex)
	{
		/* Реализуй меня */
		return null;
	}
	public void SubmitChanges()
	{
		/* Реализуй меня */
	}
}

Обратите внимание, что репозитории касаются только загрузки и сохранения данных: они не содержат доменной логики вообще. Мы можем завершить классы репозитериев путем добавления выражений для каждого метода, который выполняет операции по сохранению и получению для соответствующего механизма сохранения. В главе 7 мы начнем строить более сложные и реальные MVC приложения, и мы покажем вам, как использовать Entity Framework для реализации ваших репозиториев.