Поделиться через


Доступ к данным

Кодирование при использовании DDD: советы разработчикам, сосредоточенным на операциях с данными

Джули Лерман

Исходный код можно скачать по ссылке.

Джули ЛерманВ этом году революционной книге Эрика Эванса (Eric Evans) по проектированию программного обеспечения «Domain-Driven Design: Tackling Complexity in the Heart of Software» (Addison-Wesley Professional, 2003, amzn.to/ffL1k) исполняется десять лет. Эванс вложил в эту книгу свой многолетний опыт помощи крупным предприятиям в процессах создания ПО. Он долго размышлял о том, как инкапсулировать шаблоны взаимодействия с клиентом, анализа решаемых бизнес-задач, подбора членов групп и проектирования архитектуры ПО, необходимые для того, чтобы проекты этих предприятий привели к успеху. В центре внимания этих шаблонов лежит предметная область бизнеса (business’s domain), и вместе они составляют концепцию проектирования, управляемого предметной областью, — Domain-Driven Design (DDD). С помощью DDD вы моделируете нужную вам предметную область. Шаблоны формируются на основе абстракции ваших знаний о предметной области. Даже сегодня перечитывая вступление Мартина Фаулера (Martin Fowler) и предисловие Эванса, по-прежнему получаешь отличное представление о сути DDD.

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

Почему я заинтересовалась DDD?

Мое знакомство с DDD началось с короткого видеоролика — интервью на InfoQ.com с Джимми Нилссоном (Jimmy Nilsson), уважаемым архитектором в сообществе .NET (и не только в нем), который говорил на тему LINQ to SQL и Entity Framework (bit.ly/11DdZue). В конце интервью Нилссона попросили назвать его любимую техническую книгу. Он ответил: «Моя любимая книга по компьютерным технологиям — это книга Эрика Эванса «Domain-Driven Design». Она кажется мне весьма поэтичной. В ней не только отличное содержание, но ее можно много раз перечитывать, и она читается как стихи». Стихи! Я тогда писала свою первую техническую книгу «Programming Entity Framework» (O’Reilly Media, 2009) и была заинтригована таким описанием. Поэтому я отправилась за книгой Эванса и немного почитала ее, чтобы понять, что она собой представляет. Эванс отлично владеет словом. И это в сочетании с его глубоким знанием разработки ПО делает чтение его книги чистым удовольствием. В то же время я была удивлена тем, что я читала. Книга была не только увлекательно написана, она еще и заинтриговала меня. Он рассказывал о выстраивании отношений с клиентами и о том, как научиться по-настоящему понимать их бизнес и бизнес-задачи (естественно, в той части, которая касается разрабатываемого ПО), а не просто усиленно строчить код. Это всегда было немаловажно для меня за четверть века разработки ПО. Но я хотела большего.

Я ходила на цыпочках вокруг DDD еще несколько лет, а потом начала изучать его глубже — после того, как познакомилась с Эвансом на одной из конференций, а потом посещала его четырехдневный тренинг с полным погружением. Хотя я далеко не эксперт в области DDD, я сочла, что шаблон Bounded Context был как раз тем, что можно было бы немедленно использовать в работе, чтобы придать своему процессу создания ПО более организованную и управляемую структуру. Об этом вы можете прочитать в моей статье в этой рубрике за январь 2013 г. «Shrink EF Models with DDD Bounded Contexts» (msdn.microsoft.com/magazine/jj883952).

С тех пор я продолжала постепенное изучение этой области. DDD интересовало и вдохновляло меня, но приходилось бороться с моими подходами, тесно увязанными с манипуляциями над данными, и вникать в некоторые технические шаблоны, которые сделали бы их успешными. Весьма вероятно, что многие разработчики проходят через то же, что и я, поэтому я намерена поделиться некоторыми уроками, которым я научилась благодаря помощи, заинтересованности и великодушию Эванса, а также ряда других специалистов и преподавателей по DDD, в том числе Пола Рейнера (Paul Rayner), Вона Вернона (Vaughn Vernon), Грега Янга (Greg Young), Сесара де ла Торре (Cesar de la Torre) и Ива Рейнхаута (Yves Reynhout).

Моделируя предметную область, забудьте о постоянстве

Моделирование домена заключается в том, что все внимание концентрируется на задачах конкретного бизнеса. Проектируя типы и их свойства и поведение, я испытываю сильное искушение поразмышлять о том, как их взаимосвязи будут отражены в базе данных и как моя любимая инфраструктура объектно-реляционного сопоставления (object relational mapping, ORM) — Entity Framework — будет обрабатывать создаваемые мной иерархии свойств, отношений и наследования. Однако, если только вы не создаете ПО для компании, чей бизнес сосредоточен на хранении данных и их выборке (вроде Dropbox), постоянство данных играет лишь вспомогательную роль в вашем приложении. Здесь есть аналогия с вызовом API какого-то источника метеоданных для отображения пользователю текущей температуры. Или с отправкой данных из вашего приложения внешнему сервису, скажем, для регистрации на Meetup.com. Конечно, ваши данные могут быть более сложными, но при подходе с применением DDD к контекстам связывания, фокусировке на поведениях и соблюдении правил DDD в создании типов проблема постоянства может быть куда проще, чем системы, которые вы, возможно, разрабатываете сегодня.

Если вы изучили свою ORM и знаете, например, как конфигурировать сопоставления базы данных с помощью Entity Framework Fluent API, то должны уметь заставить работать концепцию постоянства так, как вам нужно. В худшем случае вам может понадобиться внести какие-то изменения в свои классы. А в экстремальном случае, имея дело с унаследованной базой данных, вы могли бы даже добавить модель постоянства (persistence model), рассчитанную на сопоставление базы данных, после чего воспользоваться, скажем, AutoMapper для разрешения сущностей между вашими моделями предметной области и постоянства.

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

Закрытые аксессоры set и открытые методы

Другое эмпирическое правило — делать аксессоры set свойств закрытыми. Вместо того чтобы разрешать вызывающему коду произвольно задавать различные свойства, вы должны контролировать взаимодействие с DDD-объектами и связанными с ними данными, используя методы, которые модифицируют свойства. И, нет, я не имею в виду методы вроде SetFirstName и SetLastName. Например, вместо создания нового экземпляра типа Customer с последующей установкой каждого из его свойств вы могли бы указывать некие правила, которые должны учитываться при создании нового клиента. Эти правила можно встроить в конструктор Customer, использовать метод шаблона Factory или даже предусмотреть метод Create в типе Customer. На рис. 1 показан тип Customer, определенный согласно шаблона DDD агрегатного корня (т. е. как «родитель» графа объектов, также называемый в DDD корневой сущностью). Свойства Customer имеют закрытые аксессоры set, чтобы напрямую работать с этими свойствами могли только члены класса Customer. Этот класс предоставляет конструктор для управления тем, как создается его экземпляр, и скрывает конструктор без параметров (требуется для Entity Framework) как internal.

Рис. 1. Свойства и методы типа, который действует как агрегатный корень

public class Customer : Contact
{
  public Customer(string firstName,string lastName, string email)
 { ... }
  internal Customer(){ ... }
  public void CopyBillingAddressToShippingAddress(){ ... } 
  public void CreateNewShippingAddress(
    string street, string city, string zip) { ... }
  public void CreateBillingInformation(
    string street, string city, string zip,
    string creditcardNumber, string bankName){ ... } 
  public void SetCustomerContactDetails(
    string email, string phone, string companyName){ ... }
  public string SalesPersonId { get; private set; }
  public CustomerStatus Status{get; private set;}
  public Address ShippingAddress { get; private set; }
  public Address BillingAddress { get; private set; }
  public CustomerCreditCard CreditCard { get; private set; }
}

Тип Customer контролирует и защищает остальные сущности в агрегате (некоторые адреса и тип кредитной карты), предоставляя специфические методы (наподобие CopyBillingAddressToShippingAddress), с помощью которых вы будете создавать эти объекты и манипулировать ими. Агрегатный корень должен гарантировать, что правила, определяющие каждую сущность в агрегате, применяются с использованием логики предметной области и поведения, реализованных в этих методах. Еще важнее, что агрегатный корень отвечает за инвариантную логику и согласованность в рамках всего агрегата. Об этих инвариантах я расскажу подробнее в следующей статье, а пока советую почитать публикацию Джимми Богарда (Jimmy Bogard) «Strengthening Your Domain: Aggregate Construction» в блоге по ссылке bit.ly/ewNZ52, где дается прекрасное объяснение инвариантов в агрегатах.

В конечном счете Customer предоставляет поведения (методы), а не свойства: CopyBillingAddressToShippingAddress, CreateNewShippingAddress, CreateBillingInformation и SetCustomerContactDetails.

Обратите внимание на то, что тип Contact, от которого наследует Customer, находится в другой сборке, называемой «Common», поскольку он может понадобиться другим классам. Мне нужно скрыть свойства Contact, но они не могут быть закрытыми, а иначе Customer не получал бы к ним доступа. Вместо этого они определены как защищенные (protected):

public class Contact: Identity
{
  public string CompanyName { get; protected set; }
  public string EmailAddress { get; protected set; }
  public string Phone { get; protected set; }
}

Небольшое отступление по поводу идентификаций. Customer и Contact могут выглядеть подобно значимым объектам DDD, потому что у них нет значения ключа. Однако в моем решении значение ключа предоставляется классом Identity, от которого наследует Contact. И ни один из этих типов не является неизменяемым, поэтому их в любом случае нельзя рассматривать как значимые объекты (value objects).

Так как Customer наследует от Contact, он получит доступ к этим защищенным свойствам и сможет устанавливать их, как в следующем методе SetCustomerContactDetails:

public void SetCustomerContactDetails(string email, string phone, string companyName)

{
  EmailAddress = email;
  Phone = phone;
  CompanyName = companyName;
}

Иногда достаточно CRUD

Не все в вашем приложении нужно обязательно создавать, используя DDD. Подход с DDD помогает справляться со сложными поведениями. Если вам требуется лишь произвольное редактирование или запросы, тогда будет вполне достаточно простого класса (или набора классов), определенного так, как это обычно делается при использовании EF Code First (с помощью свойств и связей). Затем вы включаете в него методы вставки, обновления и удаления (через хранилище или просто DbContext). Чтобы осуществить нечто вроде создания заказа и его позиций, вы могли бы задействовать DDD для использования специфических бизнес-правил и поведений. Например, относится ли данный клиент к категории Gold Star? Для этого нужно получить некоторую более подробную информацию о клиенте и определить ответ на этот вопрос. Если ответ положительный, применяйте 10%-ную скидку к каждой позиции, добавляемой в заказ. Предоставил ли пользователь информацию о своей кредитной карте? Если да, может понадобиться обращение к некоему сервису верификации, чтобы убедиться в том, что она действительна.

Ключевую роль в DDD играет включение логики предметной области в виде методов в классы сущностей предметной области, и это позволяет использовать преимущества ООП вместо реализации «транзакционных скриптов» в бизнес-объектах, не поддерживающих состояние подобно типичному демонстрационному классу Code First, который выглядит именно так.

Но иногда вы занимаетесь лишь стандартными операциями, например создаете запись контакта (имя, адрес и т. д.), а затем сохраняете ее. Это просто операции создания, чтения, обновления и удаления (create, read, update, delete — CRUD). Вам не нужно создавать агрегаты, корни и соответствующие поведения.

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

Общие данные могут стать проклятием в сложных системах

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

Я учусь правильному пониманию того, где общие данные действительно нужны, а где — нет. Некоторые вещи просто не стоит пытаться делать, например сопоставлять что-то из разных контекстов с одной таблицей или даже одной базой данных. Самый распространенный пример — совместное использование Contact, который должен удовлетворять потребностям всех и каждого в разных системах. Как примирить систему управления версиями в случае типа Contact, который может потребоваться в многочисленных системах? Как быть, если в одной из систем потребуется модифицировать определение этого типа Contact? А если учитывать ORM, то как сопоставить Contact, используемый в разных системах, с одной таблицей или одной базой данных?

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

Мое активное неприятие этого вызвано тем, что 25 лет я уделяла основное внимание преимуществам повторного использования как кода, так и данных. Поэтому мне было очень нелегко принять идею о том, что дублирование данных — это вовсе не преступление. Конечно, не все данные укладываются в эту новую (для меня) парадигму. Как насчет небольших данных вроде имени персоны? Что такого особенного, если вы продублируете имя и фамилию какой-то персоны в нескольких таблицах или даже нескольких базах данных, выделенных разным подсистемах вашего программного решения? В долгосрочной перспективе, оставив в покое сложность работы с общими данными, вы намного упростите задачу построения своей системы. В любом случае вы должны всегда сводить к минимуму дублирование данных и атрибутов в разных связанных контекстах. Иногда вам нужны лишь идентификатор и статус клиента, чтобы рассчитать скидки в связанном контексте ценовой политики. А имя и фамилия клиента могут понадобиться только в связанном контексте управления контактами.

Но все равно остается уйма информации, которая должна быть общей между системами. Вы можете использовать то, что в DDD называют «антикоррупционным уровнем» («anti-corruption layer») (им может быть нечто простое наподобие сервиса или очереди сообщений), который, например, гарантирует: если кто-то создает новый контакт, вы либо распознаете, что такая персона уже где-то есть, либо заботитесь о том, чтобы эта персона вместе с ключом общей идентификации была создана в другой подсистеме.

Прежде чем расстаться на месяц

Пока я продолжаю свой путь в постижении технической стороны Domain-Driven Design, с трудом отказываясь от старых привычек, привыкая к новым идеям и все чаще издавая победный клич «ага!», советую воспользоваться моими рекомендациями, которые мы здесь обсуждали, — они по-настоящему помогли мне пролить свет на многие вещи. Иногда все упирается в перспективу, учет которой многое проясняет.

В следующей статье я поделюсь еще одной порцией рекомендаций, рождающихся у меня в те моменты, когда я издаю клич «ага!». Я расскажу о термине, о котором вы, возможно, уже слышали, — об анемичной модели предметной области (anemic domain model) наряду с родственным ей аналогом в DDD — богатой модели предметной области (rich domain model). Кроме того, мы обсудим односторонние отношения и то, чего ожидать, когда дело доходит до добавления поддержки постоянства данных при использовании Entity Framework. Я также затрону еще несколько вопросов, относящихся к DDD, которые вызвали у меня наибольшие затруднения в понимании.

Ну а пока, почему бы вам не присмотреться к своим классам и не подумать о том, как усилить контроль над ними, скрыв аксессоры set свойств и предоставив вместо них более описательные и явные методы. И помните: все методы вроде «SetLastName» запрещены. Это мухлеж!


Джули Лерман (Julie Lerman) — Microsoft MVP, преподаватель и консультант по .NET, живет в Вермонте. Часто выступает на конференциях по всему миру и в группах пользователей по тематике, связанной с доступом к данным и другими технологиями Microsoft .NET. Ведет блог thedatafarm.com/blog и является автором серии книг «Programming Entity Framework» (O’Reilly Media, 2010), в том числе «Code First Edition» (2011) и «DbContext Edition» (2012), также выпущенных издательством O’Reilly Media. Вы можете читать ее заметки в twitter.com/julielerman и смотреть ее видеокурсы на juliel.me/PS-Videos.

Выражаю благодарность за рецензирование статьи эксперту Microsoft Сесару де ла Торре (Cesar de la Torre).