Около 10 лет назад Эрик Эванс предложил термин «проектирование на уровне доменов» (domain-driven design — DDD) для обозначения нового подхода к разработке программного обеспечения. Метод DDD появился в то время, когда ведущей моделью было проектирование на основе SQL Server. В основном архитекторы строили системы, начиная с оптимизированной (реляционной) модели данных. Все остальное — в частности, бизнес-логика — организовывалось поверх сущностей, идентифицированных в модели данных.

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

Как обычно, мир разделился на два непримиримых лагеря: сторонников модели «программный код в первую очередь» и тех, кто ставит на первое место базу данных. У противников есть серьезные доводы, но члены обеих групп неправы, когда слепо игнорируют друг друга.

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

Entity Framework никогда не отличалась четкостью подхода к домену и сохраняемости. Разработчикам никогда не составляло труда следовать любому методу проектирования и сочетать уровень домена с инфраструктурой. В результате платформу Entity Framework можно гибко приспособить для DDD-проектирования (через модель «программный код в первую очередь») и для классического проектирования на основе SQL Server (модель «база данных в первую очередь»). В этой статье мы рассмотрим пять аспектов модели сохраняемости Entity Framework: моделирование с акцентом на программный код и базу данных, сложные типы, типы перечисления (enum), типы массивов и одну таблицу на иерархию.

Моделирование с акцентом на программный код и базу данных

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

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

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

Совсем иное дело — модель Entity Framework на основе базы данных. Вы берете существующую базу данных SQL Server и выводите из нее модель классов. Благодаря инструментарию Entity Framework это можно сделать также с помощью мастера.

Какой подход лучше и в каких случаях предпочтителен тот или иной метод? Как всегда, ответ на эти вопросы зависит от различных факторов.

Если система проектируется на основе существующей базы данных, и отсутствует возможность внести изменения на уровне сохраняемости, то предпочтительно начинать с базы данных. Но главное при работе с Entity Framework — мыслить в терминах объектов и методов, а не строк и хранимых процедур. Модель можно вывести из существующей базы данных, то есть уровень сохраняемости инвариантен, но старайтесь дополнить объекты бизнес-логикой таким образом, чтобы они выглядели как сущности реального мира. Подход, при котором база данных стоит на первом месте — не просто способ использовать объекты вместо строк данных. Если в конечном итоге вы работаете с классами исключительно со строго типизированными свойствами, то прочитать исходный текст будет наверняка проще, чем при использовании простого ADO.NET, но вы не извлечете никакой выгоды из изменения парадигмы программирования.

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

Сложные типы

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

Если выбрать подход на основе кода, то цель — как можно более точно моделировать домен. Поэтому сущность Customer («Клиент») имеет адрес, и адрес состоит из улицы, почтового индекса, города и страны. Более того, адрес представляет собой не просто одну-две строки. Адрес может быть связан с конкретными малыми элементами логики, такими как форматирование компонентов или вычисление долготы и широты. Методы класса — идеальное место для размещения этих фрагментов логики.

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

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

Типы перечисления

Начиная с Entity Framework 5, можно определить типы перечисления и использовать их, чтобы задать свойства в модели. В подходе DDD типы перечисления играют важную роль, так как с их помощью можно ясно указать действительные значения для данного свойства. Иногда при моделировании обнаруживается, что какое-то свойство определенной сущности имеет целочисленное значение, и, следовательно, ему назначается тип Int32. Но очень редко бывает, что допустимые значения занимают весь диапазон целых чисел. Конечно, можно организовать фильтрацию на уровне проверки, но не будет ли это излишним усложнением? Если удастся удачно моделировать свойство, то дополнительной проверки не потребуется. Это оптимальный подход, но одновременно и наглядный пример того, как теория DDD противоречит практике Entity Framework.

Платформа Entity Framework поддерживает тип перечисления начиная с версии 5, но необходимо компилировать приложение для. NET 4.5. Ситуация немного улучшилась с появлением Entity Framework 6, поскольку в рамках подхода на основе программного кода можно без проблем использовать в модели свойства с типом перечисления:

public class PlayerGame
{
public Backhand Backhand { get; set; }
public Hand Handed { get; set; }
public Surface PreferredSurfaces { get; private set; }
public Shot PreferredShots { get; private set; }
}

В данном случае тип — сложный, и все свойства выводятся как простые перечисления. NET. В SQL Server сохраняются целые числа, скрытые за перечислениями. При чтении из хранилища целые числа преобразуются в элементы перечисления.

Что делать, если по какой-то причине невозможно применить. NET 4.5 или подход на основе программного кода? В этом случае можно прибегнуть к перечислениям. С помощью этого же приема можно работать с массивами.

Типы массива

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

public IList FaultsTeam1
{
get { return BuildFaultsTable(InternalFaultsTeam1); }
set
{
InternalFaultsTeam1 = String.Join(«,", value);
}
}

Свойство FaultsTeam1 — массив типа Fault, в котором каждый элемент массива соответствует игроку команды. Недостаточно просто предоставить свойство get/set. Это приемлемо в модели, но вряд ли может быть реализовано в Entity Framework. Чтобы обойти проблему, можно объявить дополнительное свойство — в данном случае оно именуется InternalFaultsTeam1 — строкового типа. Это простое свойство, успешно обрабатываемое в Entity Framework.

public String InternalFaultsTeam1 { get; set; }

Пара методов получения/задания в FaultsTeam1 обеспечивает чтение и запись свойства InternalFaultsTeam1 каждый раз, когда используется официальный API-интерфейс для нарушений. Тип Fault может содержать информацию об игроке и подробности о нарушении. Ради простоты в образцовом исходном тексте предполагается, что о Fault известно лишь, что это целое число, представляющее количество нарушений для игрока. Игроки идентифицируются индексом, поэтому первый элемент списка — первый игрок команды, и т.д. Сериализованный список нарушений представляет собой строку с разделителями в виде запятых. При сопоставлении модели с механизмом сохраняемости Entity Framework вы сопоставляете InternalFaultsTeam1, но Entity Framework получает указание игнорировать FaultsTeam1. Сопоставление можно выполнить как через текущий API, так и через заметки к данным, как показано в следующем исходном тексте:

[NotMapped]
public IList FaultsTeam1
{
get { return BuildFaultsTable(InternalFaultsTeam1); }
set
{
InternalFaultsTeam1 = String.Join(»,«, value);
}
}

Таким образом создается столбец с именем InternalFaultsTeam1. Он содержит сериализованную строку, построенную методом присваивания. Но при считывании сериализованная строка загружается в массив для обработки остальными компонентами программного кода.

Это тот же прием, который можно задействовать в. NET 4.0 при использовании перечисляемых типов с версией Entity Framework, в которой нет собственной поддержки типов перечисления. Вы определяете внутреннее целочисленное свойство для хранилища и предоставляете несопоставленное свойство перечисления, метод получения/задания которого выполняет преобразование в/из внутреннего буфера.

Одна таблица на иерархию

Главное преимущество подхода»программный код в первую очередь«заключается в возможности построить иерархию связанных классов и, за исключением массивов и в некоторых случаях перечислений, Entity Framework может сохранить ее в таблице базы данных. Для добавления таких типовых элементов, как ключи, ограничения и связи, имеются соглашения и явные правила, задаваемые через атрибуты или текущий API. На странице документации Microsoft по Entity Framework (EF) можно найти видеоматериалы и другие основные документы по сопоставлению классов структурам базы данных.

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

public class YourContext: DbContext
{
public WaterpoloContext(): base(»your_Database«)
{
}
public DbSet Matches { get; set; }
public DbSet Users { get; set; }
}

Если сущности связаны через наследование, то можно применить несколько подходов. Подход по умолчанию, одна таблица на иерархию (Table-per-Hierarchy, TPH), предусматривает, что все классы в иерархии сопоставляются одной таблице. Иначе можно иметь отдельные таблицы, по одной для типа. Этот подход известен как»одна таблица на тип«(Table-per-Type, TPT) и для функционирования он требует текущего кода:

modelBuilder.Entity().ToTable(»Players«);
modelBuilder.Entity().ToTable(»Rankings«);
modelBuilder.Entity().ToTable(»Stats«);

Еще один подход строится на основе одной физической таблицы на конкретный (не абстрактный) тип в иерархии Table-Per-Concrete Type Inheritance, TPC). Этот подход требует следующего явного кода текущего API, в котором выполняется импорт унаследованных свойств из базового класса с последующим созданием таблицы для производного типа.

m.MapInheritedProperties();
m.ToTable(»Rankings");
});

Если использовать способ проектирования на основе программного кода, то код сопоставления находится в переопределяемом методе OnModelCreating, заданном в пользовательском классе контекста.

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{. .. }

TPH, TPT и TPC — три режима сопоставления, поддерживаемых Entity Framework специально для ситуаций, в которых в модели домена используется наследование между классами. TPH — подход по умолчанию, и он выбран не случайно. Это подход, связанный с минимальной стоимостью на стороне запроса благодаря уменьшению числа операций объединения.

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