Рекомендуемый подход

Введение в проблемно-ориентированное проектирование

Дэвид Лэриби (David Laribee)

В этой статье рассматриваются следующие вопросы.

  • Моделирование на общеупотребительном языке (Ubiquitous Language)
  • Ограниченные контексты и составные корни
  • Использование принципа персональной ответственности
  • Репозитории и базы данных
В этой статье используются следующие технологии.
Visual Studio

Содержание

Модель Платона
Используем речь
Контекст
Надо знать, что главное в нашем деле
Система персональной ответственности
У сущностей есть индивидуальность и жизнь
Объекты-значения описывают вещи
Сводные корни объединяют элементы
Службы доменов моделируют основные операции
Репозитории хранят и распространяют сводные корни
О базах данных
Как начать работу в DDD

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

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

Если вам понравятся представленные здесь идеи, я настоятельно рекомендую расширить свои познания, прочитав книгу Domain-Driven Design: Tackling Complexity in the Heart of Software (на английском языке), написанную Эриком Эвансом (Eric Evans). Это не просто первое и основное введение в DDD, это сокровищница информации, открытая для нас одним из лучших разработчиков программного обеспечения. Схемы и основные принципы DDD, о которых я буду говорить в этой статье, ведут свое происхождение от концепций, описанных в этой книге.

Разделение контекстов по нуждам архитектуры

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

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

Кого-то это может потрясти до глубины души, но администраторы баз данных и разработчики могут ладить друг с другом. Ограниченные контексты позволяют взглянуть на эту обетованную землю. Если вас интересуют архитектурно ограниченные контексты, я настоятельно рекомендую читать блог Грега Янга (Greg Young). Он хорошо знаком с этим подходом, ясно излагает его принципы и вообще немало об этом пишет.

Модель Платона

Раз мы только начинаем разговор, не помешает определить, что я имею в виду под моделью. Отвечая на этот вопрос, мы совершим короткое метафизическое путешествие. Для этого нет проводника лучше Платона.

Платон, самый известный ученик Сократа, полагал, что концепции, люди, места и вещи, которые мы постигаем и воспринимаем через органы чувств — не более чем тени истины. Он назвал праобразы вещей «эйдос», или «форма».

Объясняя, что такое формы, Платон использовал известную аллегорию с пещерой. Она описывает людей, находящихся в глубокой темной пещере. Эти люди в пещере удерживаются оковами так, что могут видеть только стену перед собой, на которую падает свет из щели в потолке. Если мимо щели проходит животное, на стену падает тень, которую видят узники пещеры. Для них эти тени — реальные вещи. Если мимо проходит лев, они показывают на тень льва и кричат «Бегите!» Но на самом деле это лишь тень настоящей формы, самого льва.

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

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

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

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

В DDD эта идея называется «разработкой на основе модели». Понимание вами модели проявляется в коде. Человек, занятый проблемно-ориентированным проектированием, не будет заниматься томами документации или объемными инструментами составления схем. Вместо этого он попытается пропитать сам код своим пониманием предметной области.

Код, «схватывающий» модель, — центральная идея в DDD. Создавая программу, которая решает конкретную текущую проблему и ограничена этой проблемой, вы в итоге получаете программу, готовую воспринять новые идеи и озарения. Мне нравится, как это описывает Эрик Эванс: сжатие знания в модели. Узнав что-нибудь важное об области, вы сразу поймете, куда двигаться.

Используем речь

Рассмотрим некоторые подходы, которые предоставляет DDD для достижения этой цели. Значительная часть вашей работы разработчика — общение с теми, кто не пишет код, чтобы понять, что вы должны сделать. Если вы работаете в организации, где используется какой-то процесс, требования обычно выглядят как история пользователя, задача или случай использования. Часто ли бывают полны требования или спецификации, которые к вам попадают?

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

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

Кто-нибудь здесь понимает, что это значит? Я, например, не понимал, когда увидел это в написанном виде с расставленными приоритетами. Как вообще можно понять все, что должно войти в состав сопровождаемого программного обеспечении, из такого общего описания?

Хорошие истории пользователей — это приглашения к беседе с их автором, пользователем. Это значит, что, начиная работать над функцией одобрения/отклонения полисов, лучше всего иметь доступ к страховщику. Для тех, кто не в курсе: страховщики (андеррайтеры) — это такие специалисты (по крайней мере, хорошие страховщики), которые определяют, безопасно ли страховать данной страховой компании определенный тип рисков.

Начиная обсуждать функцию со страховщиком (или с экспертом в другой предметной области, к которой относится ваш проект), внимательно следите за используемыми им терминами. Такие эксперты предметной области используют термины, принятые в их компании или во всей отрасли. В DDD такой набор терминов называется общеупотребительным языком (Ubiquitous Language). Вы, как разработчик, должны понимать этот язык и не только использовать его в разговоре с экспертами, но и отразить эту терминологию в своем коде. Если в беседе часто встречаются термины «код класса», «нормировка» или «риск», в коде было бы уместно использовать такие же названия классов.

Это основная схема DDD. На первый взгляд общеупотребительный язык кажется чем-то очевидным, и многие из вас наверняка уже используют его интуитивно. Но использование языка предметной области в коде сознательно, в качестве постоянного правила очень важно для разработчика. Это уменьшает разрыв между языком предметной области и техническим жаргоном. «Как» подчиняется «что» и разработчик оказывается ближе к цели своей работы: пользе для бизнеса.

Контекст

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

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

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

Страховые полисы для пособий по нетрудоспособности следует рассматривать, принимая во внимание следующие элементы.

  • Расценки и продажи
  • Общий процесс работы с полисом (продление, прекращение)
  • Оценка выплат за аудит
  • Ежеквартальные оценки
  • Установка и поддержание ставок
  • Выплата комиссий агентам и брокерам
  • Получение выплат от клиентов
  • Общая бухгалтерия
  • Определение допустимых рисков (андеррайтинг)

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

Системы, в которых не удается разделить и изолировать ограниченные контексты, часто приобретают архитектурный стиль, который имеет красноречивое название «Большой ком грязи». В 1999 г. Брайан Фут (Brian Foot) и Йозеф Йодер (Joseph Yoder) определили архитектурный стиль (или, что может быть вернее, антиархитектурный стиль) в классической статье с таким же названием («Большой ком грязи (Big Ball of Mud»).

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

fig01.gif

Рис. 1 От ограниченного контекста — к карте контекста

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

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

Некоторые зоны в моделях группируются теснее, чем другие. Модули — способ организовать эти группы в определенном контексте, они служат как мини-границы, где можно остановиться и подумать о связях с другими модулями. Это еще один метод организации, уводящий нас от «маленьких комьев грязи». Технически модули создавать просто: в Microsoft .NET Framework это просто пространства имен. Но вот искусство идентифицировать модули требует провести чуть больше времени с кодом. Иногда получается так, что нечто выделяется из модели как мини-модель; тогда есть смысл подумать, не выделить ли отдельное пространство имен.

Разделение моделей на связанные модули дает хороший эффект в IDE. Дело в том, что для явного включения модулей нужно использовать несколько операторов «using», в результате чего получается гораздо более чистая работа в IntelliSense. Они также позволяют искать связи между концептуально большими частями системы с помощью статических инструментов анализа, например NDepend.

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

Слои, предохраняющие абстракцию

Слой, предохраняющий абстракцию (ACL) — это еще одна схема DDD, в которой предлагается создавать фильтры, не дающие концепциям из другой предметной области просочиться в вашу модель. Они поддерживают чистоту модели.

В сущности, репозитории — разновидность ACL. Они оставляют снаружи вашей модели SQL или конструкции объектно-реляционного сопоставления (ORM).

ACL — отличный способ для введения того, что Майкл Физерс (Michael Feathers) называет в своей книге Эффективная работа с устаревшим кодом слово «шов». Шов — это зона, где можно начать убирать устаревший код и вносить изменения. Выделение швов, как и изоляция основной предметной области, могут быть очень выгодны при использовании методов DDD для рефакторинга и усиления наиболее значимых частей кода.

Надо знать, что главное в нашем деле

Большинство фирм, занятых разработкой, состоят из нескольких опытных менеджеров и разработчиков высокого уровня, которые могут выделить и описать проблему и создать элегантное, легко сопровождаемое объектно-ориентированное решение. Чтобы предоставить клиенту максимальную отдачу на каждый вложенный доллар, нужно точно понимать основную предметную область приложения. Основная предметная область — это ограниченный контекст, самый ценный результат применения DDD.

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

Какая бы это ни была отрасль, у работодателя или клиента есть что-то, выделяющее его на рынке; и обычно для этого и нужно специальное программное обеспечение. А именно этим программным обеспечением обычно и моделируется основная предметная область.

Можно измерять инвестиции с другой точки зрения: как потратить интеллектуальные ресурсы для достижения технического совершенства. Слишком много ведущих разработчиков относится к разряду людей, увлекающихся новыми технологиями. Этого стоит, конечно, ожидать в некоторой степени — отрасль развивается с непрекращающимся ускорением и производителям приходится постоянно выпускать новые технологии, чтобы удовлетворить требованиям своих клиентов и оставаться конкурентоспособными. Задача разработчиков в том, как мне кажется, чтобы освоить фундаментальные принципы и схемы, обеспечивающие работу ядра системы. Конечно, велико искушение упаковать все это в новую инфраструктуру или платформу, но нужно помнить, что фирмы производят эти продукты главным образом для того, чтобы мы верили — они работают, не покладая рук.

Система персональной ответственности

Я упоминал о том, что DDD обеспечивает язык шаблонов для структурирования многообразных предметных областей. Путем реализации этих шаблонов получается определенный уровень соответствия SRP (система персональной ответственности), и это определенно полезно.

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

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

У сущностей есть индивидуальность и жизнь

Сущность — это «штука» в вашей системе. О них часто удобно думать существительными: люди, места и, ну, штуки.

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

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

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

Я вот не расстраиваюсь оттого, что нужно использовать другие классы, не являющиеся сущностями, и попытался бы избежать вынесения центрального поведения из сущности. Нужно все время помнить, что сущности — это по своей природе единицы с поведением. Часто это поведение реализуется как конечный автомат — при вызове команды на сущности она отвечает за изменение своего внутреннего состояния — но иногда нужно получать дополнительные данные или производить побочные эффекты во внешнем мире. Я люблю получать такие эффекты, создавая зависимость от метода команды:

public class Policy {
  public void Renew(IAuditNotifier notifier) {
    // do a bunch of internal state-related things,
    // some validation, etc.
    ...
    // now notify the audit system that there's
    // a new policy period that needs auditing
    notifier.ScheduleAuditFor(this);
  }
}

Преимущество этого подхода в том, что для создания сущности не нужен контейнер инверсии управления (IOC). Еще один вполне допустимый подход, на мой взгляд, — использовать службу обнаружения для разрешения IAuditNotifier внутри метода. Благодаря этому подходу интерфейс получается чище, но предыдущая стратегия лично для меня более информативна в отношении зависимостей на высоком уровне.

Объекты-значения описывают штуки

Объекты-значения — это дескрипторы или свойства, важные в той предметной области, которую вы моделируете. У них, в отличие от сущностей, нет обозначения; они просто описывают штуки, у которых обозначение есть. Изменяете ли вы сущность «тридцать пять рублей» или увеличиваете баланс счета?

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

Объекты-значения не изменяются. После создания изменить их невозможно. Почему важно, чтобы они были неизменяемы? С объектами-значениями мы хотим получить функции, свободные от побочных эффектов (еще одна концепция, позаимствованная DDD). Добавляя 20 рублей к 20 рублям, вы изменяете 20 рублей? Нет, вы создаете новый денежный дескриптор 40 рублей. В C# можно использовать ключевое слово read-only, которым помечаются общедоступные поля для того, чтобы обеспечить их неизменность и функции, свободные от побочных эффектов, как показано на рис. 2.

Рис. 2 Использование Read-Only для обеспечения неизменности

public class Money {
  public readonly Currency Currency;
  public readonly decimal Amount;

  public Money(decimal amount, Currency currency) {
    Amount = amount;
    Currency = currency;
  }

  public Money AddFunds(Money fundsToAdd) {
    // because the money we're adding might
    // be in a different currency, we'll service 
    // locate a money exchange Domain Service.
    var exchange = ServiceLocator.Find<IMoneyExchange>();
    var normalizedMoney = exchange.CurrentValueFor(fundsToAdd,         this.Currency);
    var newAmount = this.Amount + normalizedMoney.Amount;
    return new Money(newAmount, this.Currency);
  }
}

public enum Currency {
  USD,
  GBP,
  EUR,
  JPY
}

Сводные корни объединяют сущности

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

Главное правило, которое следует помнить: сводные корни — единственный вид сущностей, на который может ссылаться программа. Это позволяет избежать «Большого кома грязи», потому что теперь у вас есть ограничение, не дающее создавать тесно связанные системы, где все сопряжено со всем.

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

Мне нравится, что сводные корни сами все сделали за меня. Рассмотрим такой код-потребитель, который обращается к сводному корню Policy:

Policy.CurrentPeriod().Renew() 

Я пытаюсь продлить страховой полис — вспомните диаграмму классов основной области управления страховыми полисами. Обратили внимание, как я прослеживаю путь к поведению, которое я хочу вызвать?

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

Разве много точек в вызове — это удобно? IntelliSense — одна из лучших и самых полезных функций Visual Studio и других современных IDE, но при вызове нужной функции через цепочку объектов в систему вводится излишняя связанность. В предыдущем примере, скажем, возникла зависимость от классов Policy и Period.

Более подробно это описано в прекрасной статье Брэда Эпплтона (Brad Appleton) на его сайте, в которой рассказывается о реализации, теории, инструментарии и подводных камнях, связанных с законом Деметры.

Выражение «смерть от тысячи порезов» — хорошее описание кошмара, которым может стать поддержание слишком связанной системы. Создавая повсюду ненужные ссылки, вы создаете также жесткую модель, в которой изменения в одном месте ведут к каскаду изменений по всему употребляющему его коду. На мой скромный взгляд, того же самого можно добиться гораздо более выразительным фрагментом кода:

Policy.Renew()

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

По моим ощущениям, при модульном тестировании сводных корней с помощью такой техники, как разработка на основе поведения (BDD), тесты склоняются в сторону парадигмы черного ящика и тестирования состояния. Сводные корни и сущности часто становятся конечными автоматами, и их поведение этому соответствует. В итоге остается проверка состояния, добавление и вычитание. В примере продления на рис. 3 описано довольно много поведения; нетрудно представить себе, как выразить это в тесте в стиле BDD.

Рис. 3 Тестирование сводных корней

public class 
  When_renewing_an_active_policy_that_needs_renewal {

  Policy ThePolicy;
  DateTime OriginalEndingOn;

  [SetUp]
  public void Context() {
    ThePolicy = new Policy(new DateTime(1/1/2009));
    var somePayroll = new CompanyPayroll();
    ThePolicy.Covers(somePayroll);
    ThePolicy.Write();
    OriginalEndingOn = ThePolicy.EndingOn;
  }

  [Test]
  public void Should_create_a_new_period() { 
    ThePolicy.EndingOn.ShouldEqual(OriginalEndingOn.AddYears(1));
  }
}

Службы предметных областей моделируют основные операции

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

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

Службу предметной области можно использовать в случае продления. Это другой стиль. Вместо того, чтобы использовать метод для встраивания IAuditNotifier прямо в метод метода Renew элемента Policy, можно извлечь службу предметной области для обработки за нас разрешения зависимости; естественнее сопоставить службу предметной области с контейнером IOC, чем с сущностью. Эта стратегия мне кажется более осмысленной, если зависимостей много, но я просто показывал ее как альтернативу.

Вот небольшой пример службы предметной области.

public class PolicyRenewalProcesor {
  private readonly IAuditNotifier _notifier;

  public PolicyRenewalProcessor(IAuditNotifier notifier) {
    _notifier = notifier;
  }
  public void Renew(Policy policy) {
    policy.Renew();
    _notifier.ScheduleAuditFor(policy);
  }
}

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

Службы приложений, с другой стороны, — прекрасный способ ввести многослойную архитектуру. Их можно использовать для сопоставления данных в модели предметной области с формой, которая нужна клиентскому приложению. Например, нужно показать табличные данные в DataGrid, но при этом хочется поддерживать в модели детализированный график объекта.

Службы приложений также весьма полезны для интеграции нескольких моделей, например при переходе от аудита полисов к основному рабочему процессу с полисами. Подобным образом я использую их, чтобы вносить зависимости от инфраструктуры. Представьте себе обычную ситуацию, в которой вы представить свою модель предметной области с помощью Windows Communication Foundation (WCF). Я бы использовал для этого службу приложения, снабженную атрибутами WCF, но не позволил бы WCF пробраться в мою чистую модель области.

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

Рис. 4. Простая служба приложения

public IPolicyService {
  void Renew(PolicyRenewalDTO renewal);
  void Terminate(PolicyTerminationDTO termination);
  void Write(QuoteDTO quote);
}

public PolicyService : Service {
  private readonly ILogger _logger;
  public PolicyService(ILogger logger, IPolicyRepository policies) {
    _logger = logger;
    _policies = policies;
  }

  public void Renew(PolicyRenewalDTO renewal) {
    var policy = _policies.Find(renewal.PolicyID);
    policy.Renew();
    var logMessage = string.Format(
      "Policy {0} was successfully renewed by {1}.", 
      Policy.Number, renewal.RequestedBy);
    _logger.Log(logMessage);
  }
}

Репозитории хранят и распространяют сводные корни

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

Репозитории — хороший кандидат на роль суперкласса или того, что Мартин Фаулер (Martin Fowler) называет схемой Layer Supertype (супертип слоя). Использовать обобщенные типы для получения базового интерфейса репозитория из предыдущего примера нетрудно.

public interface IRepository<T>
  where T : IEntity
{
  Find<T>(int id);
  Find<T>(Query<T> query);
  Save(T entity);
  Delete(T entity);
}

Репозитории не дают концепциям баз данных или сохранения данных, таким как выражения SQL или сохраненные процедуры, проникнуть в модель и отвлечь от текущей задачи: описания предметной области. Такое разделение кода модели от инфраструктуры — хороший признак. Более подробно — в боковой панели «Слои, предохраняющие абстракцию».

Вы уже заметили, что я не говорю о том, как сводные корни, подчиненные им сущности и присоединенные объекты-значения сохраняются на диск. Это сделано намеренно. Сохранение данных, нужных для выполнения поведения в модели, — задача, независимая от самой модели. Сохранение данных — это инфраструктура.

Обзор этих технологий выходит далеко за рамки введения в DDD. Достаточно сказать, что есть несколько подходящих и вполне законченных вариантов для сохранения данных модели, от инфраструктур объектно-реляцонного сопоставления (ORM) до документно-ориентированных баз данных и средств сопоставления данных «сделай сам», которые применимы для простых сценариев.

Материалы по DDD

Официальный сайт DDD

Дан Норт (Dan North) о том, как структурировать истории пользователей

Архитектурный стиль «Большой ком грязи»

Блог Грега Янга (Greg Young) на CodeBetter

Статья Роберта С. Мартина (Robert C. Martin) о принципе персональной ответственности

Брэд Эпплтон (Brad Appleton) о законе Деметры

Мартин Фаулер (Martin Fowler) описывает схему супертипа слоя

Роберт С. Мартин о принципах S.O.L.I.D. Principles (Принципы)

О базах данных

К этому моменту вы наверняка начали думать: «Дейв, все это здорово. Но где мне хранить сущности?» Да, с этим тоже нужно что-то делать, но то, как или где сохранять модели, не имеет прямого отношения в обзору DDD.

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

Моделирование данных как основная техника оставляет, однако, желать лучшего. Если вы хотите понять поведение, присущее той же предметной области, то методами, основанными только на данных, например схемами элемент-отношение (ERDs), моделями элемент-отношение или диаграммами классов, сделать этого не удастся. Нужно видеть части приложения в движении, видеть, как типы работают вместе и приходят к результату.

Когда я создаю модель, я часто пользуюсь схемами последовательностей на доске в качестве средства общения. Этот подход охватывает почти всю суть общения о проектировании поведения или проблемы без формальностей Unified Modeling Language (UML) или архитектуры на основе моделей. Я не люблю излишних формальностей, особенно если собираюсь стереть схемы, нарисованные на доске. В своих схемах я не стремлюсь на 100 процентов следовать UML, а предпочитаю простые квадратики, стрелочки и полосы, которые можно нарисовать быстро.

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

Начало работы с DDD

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

С чего начать? Коротко говоря: сделайте домашнее задание. Узнайте о таких вещах, как принципы S.O.L.I.D. и прочтите книгу Эрика Эванса. Затраты времени окупятся сторицей. InfoQ выпустило уменьшенную версию книги по DDD, описывающую несколько основных принципов. Если у вас ограничено время или деньги или вы просто ищете что-то новое, советую начать с нее. Разобравшись с основами, переходите к группе DDD на Yahoo! , чтобы узнать, с чем сейчас разбираются коллеги-разработчики, и поучаствуйте в беседе.

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

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

Дэйв Лэриби (Dave Laribee) обучает группу разработчиков продуктов в VersionOne. Он регулярно выступает на местных и национальных мероприятиях для разработчиков и удостоился звания MVP Microsoft в области архитектуры в 2007 и 2008. Он пишет в сети блогов CodeBetter по адресу thebeelog.com.