Support us

Знакомство с предметно-ориентированным проектированием (DDD). Часть 2

Оставить комментарий
Знакомство с предметно-ориентированным проектированием (DDD). Часть 2

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

читать дальше

МОДЕЛИРОВАНИЕ ПРЕДМЕТНОЙ ОБЛАСТИ

При работе с ограниченными контекстами мы стараемся создавать максимально выразительные модели: такие, которые дают представление именно о назначении системы, а не о конкретной реализации. Если этого удается достичь, то концепции предметной области естественным образом оказываются «на поверхности». Такие модели получаются гибкими и удобными для рефакторинга.

Паттерны предметно-ориентированного проектирования — это, в сущности, варианты паттернов, описанных «Бандой четырех» (Эрихом Гаммой, Ричардом Хелмом, Ральфом Джонсоном, Джоном Влиссидсом), Фаулером и другими, применяемые именно в сфере моделирования предметных областей.

Наиболее распространенные паттерны описаны ниже.

Работа со структурой

Сущности (Entity)

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

Объекты значений (Value object)

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

В данном примере, если адрес клиента изменяется, то инстанцируется новый объект значения Address, присваиваемый клиенту.


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

Количество ассоциаций

Чем больше количество ассоциаций между классами, тем более сложна вся структура. Добавляя квалификаторы, стремитесь обходиться минимальным количеством ассоциаций.

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

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

Служебные классы (Service)

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

Агрегаты (Aggregate)

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

При работе с агрегатами следует придерживаться следующих простых правил:

• корень имеет глобальную идентичность, а остальные объекты — локальную идентичность;

• корень проверяет и гарантирует, что удовлетворяются все инварианты;

• сущности, находящиеся вне агрегата, содержат ссылки только на корень;

• при удалении агрегата удаляется вся содержавшаяся в нем информация;

• в случае изменения объекта все инварианты по-прежнему должны удовлетворяться.

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

Работа с жизненными циклами

Фабрики (Factory)

Фабрики управляют началом жизненного цикла некоторых агрегатов. Здесь мы видим применение паттерна «Фабрика» (Factory), или «Построитель» (Builder), описанного «Бандой четырех». Необходимо обеспечить соблюдение правил агрегата, в частности удовлетворение всех инвариантов внутри него. Рационально используйте фабрики. Не забывайте, что фабрики бывают очень полезны, но без них вполне можно обойтись.

Репозитории (Repository)

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

Работа с поведениями

Спецификация (Specification)

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

class Project {
	public boolean isOverdue() { ' }
	public boolean isUnderbudget() { ' }
}

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

public interface ProjectSpecification {
	public boolean isSatisfiedBy(Project p);
}
public class ProjectIsOverdueSpecification implements
	ProjectSpecification {
	public boolean isSatisfiedBy(Project p) { ' }
}

Таким образом, клиентский код становится и более гибким, и более удобочитаемым.

If (projectIsOverdueSpecification.isSatisfiedBy(theCurrentProject) { ' }

Стратегия (Strategy)

Паттерн «Стратегия», также именуемый «Политика» (Policy), используется для того, чтобы делать алгоритмы взаимозаменяемыми. В таком паттерне изменяемая часть «выносится за скобки».

Рассмотрим следующий пример, в котором успешность проекта определяется на основании двух показателей: (1) проект успешен, если заканчивается вовремя; (2) проект успешен, если укладывается в определенный для него бюджет.

public class Project {
	boolean is SuccessfulByTime();
	boolean is SuccessfulByBudget();
}

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

interface ProjectSuccessPolicy {
	Boolean isSuccessful(Project p);
}
class SuccessByTime implements ProjectSuccessPolicy { ' }
class SuccessByBudget implements ProjectSuccessPolicy { ' }

Выполнив рефакторинг исходного класса Project для выполнения такой политики, мы инкапсулируем критерии успешности реализации политики, а не сам класс Project.

class Project {
	   boolean isSuccessful(ProjectSuccessPolicy policy) {
	return policy.isSuccessful(this);
  }
}

Композитный паттерн (Composite)

Здесь мы имеем дело с непосредственным применением данного паттерна, описанного «Бандой четырех» внутри моделируемой предметной области. Важно помнить, что в такой ситуации клиентский код должен работать лишь с абстрактным типом, представляющим композитный элемент. Рассмотрим следующий класс:

public class Project {
	private List<Milestone> milestones;
	private List<Task> tasks;
	private List<Subproject> subprojects;
}

Subproject — это проект, в состав которого входят элементы Milestone и Task (задача). Milestone — это задача, для которой определена дата исполнения, но не указана длительность. При применении композитного паттерна мы можем добавить новый тип — «Активность» (Activity) с иными реализациями.

interface Activity {
  public Date due();
}
public class Subproject implements Activity {
  private List<Activity> activities;
  public Date due() { ' }
}
public class Milestone implements Activity {
  public Date due() { ' }
}
public class Task implements Activity {
  public Date due() { ... }
  public int duration() { ' }
}

Теперь модель проекта значительно упрощается.

public class Project {
  private List<Activity> activities;
}

UML-представление данной модели показано ниже.

АРХИТЕКТУРА ПРИЛОЖЕНИЯ

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

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

Пользовательский интерфейс Отвечает за конструирование пользовательского интерфейса и управление взаимодействиями внутри предметной области. Как правило, для реализации этого уровня применяется паттерн «модель-вид-контроллер» (MVC)
Приложение Тонкий слой, обеспечивающий взаимодействие вида с предметной областью. Внимание: этот слой легко превращается в «свалку» вытесненных поведений предметной области и в своеобразный «магнит» для кода в стиле «сценариев транзакций»
Предметная область Исключительно богатая поведениями и выразительная модель предметной области. Не забывайте, что в состав этой области входят и репозитории, и фабрики. Но механизм объектно-реляционного отображения, которому могут делегироваться задачи репозиториев, является частью инфраструктуры, то есть располагается ниже этого слоя.
Инфраструктура Работает с решениями, специфичными для данной технологии, и в большей степени фокусируется на реализации, а не на назначении. Обратите внимание: на этом уровне могут создаваться экземпляры объектов предметной области, но, как правило, основная роль отводится репозиторию, который взаимодействует с данным уровнем для получения ссылок на конкретные объекты.

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

НОВЫЕ ПАТТЕРНЫ

Большой ком грязи

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

В подобных случаях нужно очертить границу вокруг всего этого беспорядка и не пытаться осуществлять в этом контексте никакого изощренного моделирования. Не допускайте просачивания этого беспорядка в другие контексты. Данный паттерн создан Брайаном Футом и Джозефом Йодером и подробно описан по адресу http://www.laputan.org/mud/mud.html

События предметной области

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

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

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

ИСТОЧНИК

Место солидарности беларусского ИТ-комьюнити

Далучайся!

Хотите сообщить важную новость? Пишите в Telegram-бот

Главные события и полезные ссылки в нашем Telegram-канале

Обсуждение
Комментируйте без ограничений

Релоцировались? Теперь вы можете комментировать без верификации аккаунта.

Комментариев пока нет.