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

  • Tutorial
Эванс написал хорошую книжку с хорошими идеями. Но этим идеям не хватает методологической основы. Опытным разработчикам и архитекторам на интуитивном уровне понятно, что надо быть как можно ближе к предметной области заказчика, что с заказчиком надо разговаривать. Но не понятно как оценить проект на соответствие 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<CompanyState>
{
   public static class Specs
   {
       public static Spec<Supplier> ByInnAndKpp(string inn, string kpp)
           => new Spec<Supplier>(x => x.Inn == inn && x.Kpp == kpp);

       public static Spec<Supplier> ByInn(string inn)
           => new Spec<Supplier>(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<out TKey> : IHasId
        where TKey: IEquatable<TKey>
    {
        new TKey Id { get; }
    }
    
    public static bool IsNew<TKey>(this IHasId<TKey> obj)
        where TKey : IEquatable<TKey>
    {
        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<CompanyData> query, string inn, string kpp)
        => query
            .Where(x => x.Company, Supplier.Specs.ByInnAndKpp(inn, kpp))
            .FirstOrDefault();

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

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

var priceInfos = DbContext
    .CompanyData
    .ByInn("инн")
    .ToList();

Альтернативный вариант — использовать SelectMany.

var priceInfos = DbContext
    .Company
    // имеется в виду другой extension-метод  с подходящей сигнатурой
    .ByInnAndKpp("инн", "кпп")
    .SelectMany(x => x.Company)
    .ToList();

Вопрос эквивалентности вариантов с Select и SelectMany с точки зрения IQueryProvider я еще до конца не изучил. Буду благодарен любой информации на эту тему в комментариях.

Связанные коллекции


public virtual ICollection<Document> Documents { get; protected set; }

Желательно использовать только в блоке Select для преобразования в SQL-запрос, потому что код вида company.Documents.Where(…).ToList() не построит запрос к БД, а сначала поднимет в оперативную память все связанные сущности, а потому применит Where к выборке в памяти. Таким образом, наличие коллекций в модели может крайней негативно отразиться на производительности приложения. При этом рефакторинг будет произвести сложно, потому что придется передавать необходимые IQueryable из вне. Чтобы контролировать качество запросов нужно поглядывать в miniProfiler.

Сервисы (Service)


В анемичной модели вообще вся логика хранится в сервисах. Я предпочитаю добавлять сервисы только по необходимости, если логика неуместна в коде агрегата или описывает взаимодействие между агрегатами. Лучший вариант, когда домен содержит точные названия для сервиса — «касса», «склад», «кол-центр». В этом случае постфикс «Service» можно опустить. Набор методов в каждом классе соответствует набору use case'ов, сгруппированных по элементам пользовательского интерфейса. Работает хорошо, если интерфейс разработан в стиле Task Based UI.

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

Read-методы возвращают DTO для сериализации и отправки на клиент. Благодаря Queryable Extensions в AutoMapper и Mapster можно использовать маппинги для трансляции в выражения для Select, что позволяет не тащить всю сущность из БД целиком.

Менеджеры (Manager)


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

TPT для union-type


Иногда одна сщуность может быть связана с одной из нескольких других. Для создания непротиворечивой системы хранения можно использовать TPT, а для control flow — pattern matching. Этот подход подробно описан в отдельной статье.

Queryable Extensions для проекций в DTO


Использование DataMapper позволяет снизить количество boilerplate-кода, а использование Queryable Extensions — строить запросы на получение DTO без необходимости писать Select вручную. Таким образом можно повторно использовать выражения для маппинга в оперативной памяти и построения деревьев выражений для IQueryProvider. AutoMapper довольно прожорлив по памяти и не быстр, поэтому со временем заменил его на Mapster.

CQRS для отдельных подсистем


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

В отсутствии команд реализации IQuery возвращают одинаковые результаты на одинаковых входных данных. Поэтому тела таких методов можно агрессивно кешировать. Таким образом, после замены реализаций инфраструктурный код (контроллеры) останется без изменений, а модифицировать придется только тело метода IQuery. Подход позволяет оптимизировать приложение точечно по небольшим кусочкам, а не все целиком.
Подход ограничено-применим для очень-очень нагруженных ресурсов из-за накладных расходов на IOC-контейнер и memory traffic для per request lifestyle. Однако, все IQuery можно сделать singleton'ами, если не инжектировать зависимости от БД в конструктор, а вместо этого использовать конструкцию using.

Работа с унаследованным кодом


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

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

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Как вы работаете с предметной областью
Поделиться публикацией
Похожие публикации
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 18
  • +1
    var priceInfos = DbContext
        .CompanyData
        .ByInnAndKpp("инн", "кпп")
        .ToList();


    Оно же может вызвать NRE из-за того, что ByInnAndKpp может вернуть null, нехорошо.
    • 0
      Спасибо за комментарий. Ошибся при редактировании кода. ToList там вообще не скомпилируется, потому что ByInnAndKpp выполняет FirstOrDefault. Добавил пример метода ByInn и отредактировал статью.
    • 0
      Ваш подход к структурированию кода понятен :)
      Непонятно только только, почему при использовании DDD нельзя описывать бизнес логику в сервисах
      Ведь DDD — это лишь методология взаимодействия разработчика с заказчиком, а не предписания к архитектуре проекта.:)
      Еще интересно было бы узнать как выглядит схема базы данных и как она развивается по ходу проекта
      У вас для каждой сущности своя таблица или вы храните все данные в виде набора событий?
      • 0
        Промахнулся и ответил вам ниже отдельным комментарием.
        • +2
          нельзя описывать бизнес логику в сервисах

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


          а не предписания к архитектуре проекта.:)

          Идея в том, что бы ваши архитектурные решений руководствовались именно общением с бизнесом и пониманием предметной области. Есть отличный доклад на эту тему: Udi Dahan — If (domain logic) then CQRS, or Saga?. Там он затрагивает вопрос о том, как не заданные бизнесу вопросы могут случайно повлиять на архитектурные решения.


          У вас для каждой сущности своя таблица или вы храните все данные в виде набора событий?

          event sourcing это совершенно отдельная интересная тема. У меня например — и так и так. Есть 2-3 сущности которые хранятся как стрим событий, и все остальные — просто как таблички в базе (потому что там от сложность от event sourcing не перекрывает профит)

        • 0
          Непонятно только только, почему при использовании DDD нельзя описывать бизнес логику в сервисах

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

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

          Более подробно этот вопрос раскрывает Дино Эспозито. Ближе к концу доклада он приводит следующий пример. Допустим, в поддержку приходит баг. Пользователь объясняет, что он совершил некоторое действие, но ему не были начислены бонусные балы (или типа того). Если в коде программист видит «репозитории» и изменение каких-то свойств ему потребуется приложить дополнительные усилия, чтобы интерпретировать слова пользователя и сопоставить с кодом. А если он видит в коде нечто вроде:
          if(user.IsVip)
          {
             user.AddBonus(100);
          }
          

          Он может сразу уточнить статус пользователя. Может быть, про бонусные баллы он услышал от друга, а про VIP-статус позабыл. Может быть у нас баг и пользователю не был присвоен VIP-статус, хотя должен был. В любом случае, диалог получится куда более предметным.
          • +1
            Спасибо за статью.
            Всегда интересно посмотреть как это делают другие разработчики.

            Мы решили полностью следовать советам книги Patterns, Principles, and Practices of Domain-Driven Design. Тут подробно описано, с примерами, что зачем и почему, куда что класть.
            Все то что Эванс говорил, но разжевано.

            У меня так же есть несколько вопросов по примерам:

            Обоснуйте пожалуйста, почему метод Accept принадлежит сущности Company. Ведь аккредитация выдается Ведомством. А то получается что компания сама себя куда хочешь может аккредетировать… Ну или как минимум в этот метод должен передаваться Policy ведомства или экземпляр объекта Аккредитация, с данными кто выдал и почему.

            Почему метод DangerouslyChangeInnAndKpp вообще содержит в себе эту приставку? Если предметной области это нормальное явление — это бессмысленно. Может стоило тогда ввести другую сущность?

            Я не знаком с .Net вообще, но вы упоминаете «луковую» и «чистую» архитектуры, а потом в домене используете public class Specs. Разве это не кусок вашего фреймворка только что просочился в домен? Т.о. вы нарушаете направление связей.
            • 0
              Спасибо за вопросы.
              Обоснуйте пожалуйста, почему метод Accept принадлежит сущности Company. Ведь аккредитация выдается Ведомством. А то получается что компания сама себя куда хочешь может аккредетировать… Ну или как минимум в этот метод должен передаваться Policy ведомства или экземпляр объекта Аккредитация, с данными кто выдал и почему.

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

              Почему метод DangerouslyChangeInnAndKpp вообще содержит в себе эту приставку? Если предметной области это нормальное явление — это бессмысленно. Может стоило тогда ввести другую сущность?

              Смена ИНН и КПП — это перерегистрация. Т.е. в реальном мире нельзя просто так взять и сменить. Однако, предполагалось, что в подаваемых сведениях могут быть ошибки и к системе были предъявлены требования дать возможность эти ошибки исправлять.
              Название DangerouslyChangeInnAndKpp навеяно React'ом. Видимо, зря я удалил аннотации, в которых эта логика объяснялась.

              Я не знаком с .Net вообще, но вы упоминаете «луковую» и «чистую» архитектуры, а потом в домене используете public class Specs. Разве это не кусок вашего фреймворка только что просочился в домен? Т.о. вы нарушаете направление связей.

              В .NET на уровне платформы встроены «деревья выражений» (Expression Trees). Например:
              Expression<Func<Company, bool>> acceptedSpec = 
                   x => x.State = CompanyState.Accepted;
              

              Expression<Func<Company, bool>> — довольно многословная конструкция. Spec<T> — более читаемая и сразу специализирована для сигнатуры T -> bool. Плюс добавлена перегрузка операторов && и ||. Спецификация по определению представляет правила бизнес-логики, т.е. является частью домена.

              Необязательно создавать спецификации внутри класса сущности, можно положить их рядом. Мне просто удобно использовать такой синтаксис: Company.Specs.Accepted. Все бизнес-правила фильтрации агрегата Company под рукой и intellisense поможет их найти. Можно использовать и другие правила, например отдельный статический класс. Тогда будет так: CompanySpecs.Accepted.
            • 0
              Я не специалист в проектировании программ. Зачем смешивать логику хранения, бизнес логику и логику отображения в одном и том же классе? Какой в этом практический смысл? Есть ли у такого подхода преимущества в сравнении с 3-х уровневым или onion подходом, где все разделено на слои?
              • 0
                Можете раскрыть свой вопрос более подробно?
              • 0
                Как вы считаете, в сущности можно внедрять зависимости через конструктор если они помогают достичь большей выразительности при создании методов? Например Product::price() может обращаться внутри к стратегии (PricingStrategy), которая определит цену товара исходя из скидочных кампаний, акций производителя и т.п.?
                • 0
                  На одном из проектов есть такая задача. У нас отдельно Product.BasePrice и есть CartCalculator. Внутри калькулятора — еще другие зависимости, которые расчитывают цену в зависимости от лицензии, роялти, накопительной скидки и других бизнес-правил.
                  • 0
                    А чтобы узнать в указанном проекте конечную цену товара нужно было вызвать метод Product::price() или CartCalculator::price(product) или что-то в этом роде? CartCalculator используется внутри Product или снаружи?
                    • 0
                      cartCalculator.Calculate(cart), кэп!:) Калькулятор считает ценую корзины, потому что скидки могут применять как позиции заказа, так и заказу в целом. Бывает «3 по цене 2», «купи на 3000 рублей и получи в подарок шнурки для галошь» и просто накопительные скидки для постоянных клиентов.
                      • 0
                        Большое спасибо за ответ!)
                  • 0

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


                    Если уж хочется через Product::price() получать рассчитанную по стратегии цену, то получайте её как Product::price(PricingStrategy pricingStrategy), в худшем случае через последовательность Product::setPricingStrategy(PricingStrategy pricingStrategy); Product::price() с броском исключения или применением дефолтной стратегии.

                  • +1

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


                       [Display(Name = "ИНН")]
                       [Required]
                       [DisplayFormat(ConvertEmptyStringToNull = true)]
                       [Inn]
                       public string Inn { get; protected set; }

                    прямо кричит об этом. Если я правильно его понимаю (аннотации, используемые для генерирования кода и подобных вещей, не влияющие непосредственно на прямых клиентов объекта, создающих и дергающих его методы), то подобные вещи выношу куда-то в слой инфраструктуры, часто в текстовые (yaml, xml) его конфиги. Да, часто добавление одного поля, сквозного для всей системы от UI до БД, приводит к необходимости по паре строчек добавлять чуть ли не в два десятка файлов, зато при изменении в инфраструктуре "зацепляется" часто только один.


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


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

                    • +1
                      спасибо за интересные статьи

                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                      Самое читаемое