Domain Driven Design на практике


Download 333.79 Kb.
bet1/2
Sana23.04.2023
Hajmi333.79 Kb.
#1384601
  1   2
Bog'liq
Domain Driven Design на практике

Domain Driven Design на практике


Эванс написал хорошую книжку с хорошими идеями. Но этим идеям не хватает методологической основы. Опытным разработчикам и архитекторам на интуитивном уровне понятно, что надо быть как можно ближе к предметной области заказчика, что с заказчиком надо разговаривать. Но не понятно как оценить проект на соответствие Ubiquitous Language и реального языка заказчика? Как понять, что домен разделен на Bounded Context правильно? Как вообще определить используется DDD в проекте или нет?



Последний пункт особенно актуален. На одном из своих выступлений Грег Янг попросил поднять руки тех, кто практиукует DDD. А потом попросил опустить тех, кто создает классы с набором публичных геттеров и сеттеров, располагает логику в «сервисах» и «хелперах» и называет это DDD. По залу прошел смешок:)

Как же правильно структурировать бизнес-логику в DDD-стиле? Где хранить «поведение»: в сервисах, сущностях, extension-методах или везде по чуть-чуть? В статье я расскажу о том, как проектирую предметную область и какими правилами пользуюсь.

Все люди лгут



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

Сначала аналитика, потом проектирование и лишь затем — разработка



Н ачинать нужно не со структуры БД или набора классов, а с бизнес-процессов. Мы используем BPMN и UML Activity в сочетнии с контрольными примерами. Диаграммы хорошо читаются даже теми, кто не знаком со стандартами. Контрольные примеры в табличной форме помогают лучше обозначить пограничные кейсы и устранить противоречия.

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

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

Выделяем контексты



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

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

Архитектура




Порты и адаптеры, луковая архитектураclean architecture — все эти подходы базируются на идее использовать домен в качестве ядра приложения. Эванс вскользь затрагивает этот вопрос, когда говорит о «домене» и «инфраструктуре». Бизнес-логика не оперирует понятиями «транзакция», «база данных», «контроллер», «lazy load» и т.д. n-layer — архитектура не позволяет разнести эти понятия. Запрос будет приходить в контроллер, передаваться в «бизнес-логику», а «бизнес-логика» будет взаимодействовать с DAL. А DAL это сплошные «транзакции», «таблицы», «блокировки» и т.д. Clean Architecture позволяет инвертиртировать зависимости и отделить мух от котлет. Конечно совсем абстрагироваться от деталей реализации не получится. RDBMS, ORM, сетевое взаимодействие все-равно наложат свои ограничения. Но в случае использования Clean Architecture это можно контролировать. В n-layer придерживаться «единого языка» гораздо сложнее из-за того что на самом нижнем слое лежит структура хранения.

Clean Architecture хорошо работает в паре с Bounded Context. Разные контексты могут представлять собой разные подсистемы. Простые контексты лучше реализовывать с помощью простого CRUD. Для контекстов с асимметричной нагрузкой хорошо подойдет CQRS. Для подсистем, требующих Audit Log'а есть смысл использовать Event Sourcing. Для нагруженных на чтение и запись подсистемах с ограничениями по пропускной способности и задержкам есть смысл рассмотреть event driven — подход. На первый взгляд это может показаться неудобным. Например я работал с CRUD-подсистемой и мне пришла задача из CQRS-подсистемы. Придется некоторе время смотреть на все эти Command и Query как на новые ворота. Альтернатива — проектировать систему в едином стиле — недальновидна. Архитектура — это набор инструментов, а каждый инструмент подходит для решения конкретной задачи.

Структура проекта



.NET-проекты я структурирую следущим образом:

/App
/ProjectName.Web.Public


/ProjectName.Web.Admin
/ProjectName.Web.SomeOtherStuff
/Domain
/ProjectName.Domain.Core
/ProjectName.Domain.BoundedContext1
/ProjectName.Domain.BoundedContext1.Services
/ProjectName.Domain.BoundedContext2
/ProjectName.Domain.BoundedContext2.Command
/ProjectName.Domain.BoundedContext2.Query
/ProjectName.Domain.BoundedContext3
/Data
/ProjectName.Data
/Libs
/Problem1Resolver
/Problem2Resolver

Проекты из папки Libs не зависят от домена. Они решают только свою локальную задачу, например формирование отчетов, парсинг csv, механизмы кеширования и т.д. Структура домена соответствует BoundedContext'ам. Проекты из папки Domain не зависят от Data. В Data находятся DbContext, миграции, конфигурации, относящиеся к DAL. Data зависит от сущностей Domain для построения миграций. Проекты из папки App используют IOC-контейнер для внедерния зависимостей. Таким образом получается добиться максимальной изоляции кода домена от инфраструктуры.

Моделируем сущности



Под сущностью будем понимать объект предметной области, обладающий уникальным идентификатором. Для примера возьмем класс, описывающий российскую компанию, в контексте получения аккредитации в неком ведомстве.

[DisplayName("Юридическое лицо (компания)")]


public class Company
: LongIdBase
, IHasState
{
public static class Specs
{
public static Spec ByInnAndKpp(string inn, string kpp)
=> new Spec(x => x.Inn == inn && x.Kpp == kpp);

public static Spec ByInn(string inn)


=> new Spec(x => x.Inn == inn);
}
// Для EF
protected Company ()
{
}

public Company (string inn, string kpp)


{
DangerouslyChangeInnAndKpp(inn, kpp);
}

public void DangerouslyChangeInnAndKpp(string inn, string kpp)


{
Inn = inn.NullIfEmpty() ?? throw new ArgumentNullException(nameof(inn));
Kpp = kpp.NullIfEmpty() ?? throw new ArgumentNullException(nameof(kpp));
this.ValidateProperties();
}

[Display(Name = "ИНН")]


[Required]
[DisplayFormat(ConvertEmptyStringToNull = true)]
[Inn]
public string Inn { get; protected set; }

[Display(Name = "КПП")]


[DisplayFormat(ConvertEmptyStringToNull = true)]
[Kpp]
public string Kpp { get; protected set; }

[Display(Name = "Статус организации")]


public CompanyState State { get; protected set; }

[DisplayFormat(ConvertEmptyStringToNull = true)]


public string Comment { get; protected set; }

[Display(Name = "Дата изменения статуса")]


public DateTime? StateChangeDate { get; protected set; }

public void Accept()


{
StateChangeDate = DateTime.UtcNow;
State = AccreditationState.Accredited;
}

public void Decline(string comment)


{
StateChangeDate = DateTime.UtcNow;
State = AccreditationState.Declined;
Comment = comment.NullIfEmpty()
?? throw new ArgumentNullException(nameof(comment));
}

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

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

Выбор уникального идентификатора



К счастью, Эванс дает четкие рекомендации на этот счет: сначала ищем идентификатор в предметной области: ИНН, КПП, паспортные данные и т.д. Если нашли — используем его. Не нашли — полагаемся на GUID или генерируемые базой данных Id. Иногда целесообразно использовать в качестве Id идентификатор, отличный от доменного, даже если последний существует. Например, если сущность должна быть версинируемой и система должна хранить все предыдущие версии или если идентификатор из предметной модели — сложный композит и не дружит с persistance.

Настоящие конструкторы



Для материализации объектов ORM чаще всего используют reflection. EF сможет дотянуться до protected-конструктора, а программисты – нет. Им придется создать корректное юр. лицо, идентифицируемое по ИНН и КПП. Конструктор снабжен гардами. Создать не корректный объект просто не получится. Extension-метод ValidateProperties вызывает валидацию по DataAnnotation — атрибутам, а NullIfEmpty не дает передать пустые строки.

public static class Extensions


{
public static void ValidateProperties(this object obj)
{
var context = new ValidationContext(obj);
Validator.ValidateObject(obj, context, true);
}

public static string NullIfEmpty(this string str)


=> string.IsNullOrEmpty(str) ? null : str;
}

Для валидации ИНН специально написан атрибут следующего вида:

public class InnAttribute : RegularExpressionAttribute


{
public InnAttribute() : base(@"^(\d{10}|\d{12})$")
{
ErrorMessage = "ИНН должен быть последовательностью из 10/12 цифр.";
}

public InnAttribute(CivilLawSubject civilLawSubject)


: base(civilLawSubject == CivilLawSubject.Individual
? @"^\d{12}$"
: @"^\d{10}$")
{
ErrorMessage = civilLawSubject == CivilLawSubject.Individual
? "ИНН физического лица должен быть последовательностью из 12 цифр."
: "ИНН юридического лица должен быть последовательностью из 10 цифр.";
}
}

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

Инкапсуляция и валидация



Свойства ИНН и КПП объявлены с protected — сеттером. EF опять сможет до них дотянуться, а программисту придется воспользоваться функцией DangerouslyChangeInnAndKpp. Название функции явно намекает, что смена ИНН и КПП – ситуация не штатная. В функцию передается два параметра, что означает, что если менять ИНН и КПП, то только вместе. ИНН+КПП можно было бы даже сделать композитным ключом. Но для совместимости я оставил long Id. Наконец, при вызове этой функции сработают валидаторы и если ИНН и КПП не корректны, будет выброшен ValidationException.

Можно еще больше усилить систему типов. Однако в описанном по ссылке подходе есть существенный недостаток: отсутствие поддержки со стороны стандартной инфраструктуры ASP.NET. Поддержку можно дописать, но такой инфраструктурный код чего-то стоит и его нужно сопровождать.

Свойства для чтения, специализированные методы для изменения

По бизнес-процессу организацию можно «принять» или «отклонить», причем в случае отклонения необходимо оставить комментарий. Если бы все свойства были публичными, то узнать об этом можно было только из документации. В данном случае правила смены статусов видны из сигнатур методов. В статье я привел только фрагмент класса юр.лица. На самом деле там гораздо больше полей и понимание что с чем связано очень помогает, особенно при подключении новых членов команды. Если свойство можно бесконтрольно менять в отрыве от других без явных бизнес-операций setter можно тоже сделать публичным. Однако такое свойство должно насторожить: если нет явных операций, связанных с данными, возможно эти данные не нужны?

Альтернативный вариант — использовать паттерн «состояние» и вынести поведение в отдельные классы.



Спецификации

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

Extension-методы



Ad hoc полиморфизм для интерфейсов (чтобы не приходилось реализовывать методы в каждом наследнике) рано или поздно появится в C#. Пока приходится довольствоваться extension-методами.

public interface IHasId


{
object Id { get; }
}

public interface IHasId : IHasId


where TKey: IEquatable
{
new TKey Id { get; }
}
public static bool IsNew(this IHasId obj)
where TKey : IEquatable
{
return obj.Id == null || obj.Id.Equals(default(TKey));
}

Extension-методы подходят для использования в LINQ для большей выразительности. Однако, методы ByInnAndKpp и ByInn нельзя использовать внутри других выражений. Их не сможет разобрать провайдер. Более подробно про использование extension-методов а-ля DSL рассказал Дино Эспозито на одном из DotNext.

public static class CompanyDataExtensions


{
public static CompanyData ByInnAndKpp(
this IQueryable query, string inn, string kpp)
=> query
.Where(x => x.Company, Supplier.Specs.ByInnAndKpp(inn, kpp))
.FirstOrDefault();

public static CompanyData ByInn(


this IQueryable query, string inn)
=> query
.Where(x => x.Company, Supplier.Specs.ByInn(inn));
}

Обратите внимание на необычный Where с двумя параметрами. EF Core стал поддерживать InvokeExpression. В прикладном коде используется так:


Download 333.79 Kb.

Do'stlaringiz bilan baham:
  1   2




Ma'lumotlar bazasi mualliflik huquqi bilan himoyalangan ©fayllar.org 2024
ma'muriyatiga murojaat qiling