Как стать автором
Обновить

Комментарии 33

PersonModel->Person

А может пойти дальше и сделать что-то типа PersonStored чисто модель для EF или другой системы ORM? Тогда не нужно будет создавать костыли в виде Value Conversions и т.д., а сделать многое декларативно — на атрибутах.
Конечно, можно. Но не нужно.
Во первых, атрибуты — это такая себе штука в DDD. Чем меньше домен замусорен атрибутами (тем более ориентированными на инфраструктурный слой), тем лучше. Но в некоторых случаях без них не обойтись — нет идеальных ЯП, библиотек и ORM.
Во вторых — две мощнейшие ORM в .Net Core, EF Core и NHibernate, многое полезное для DDD умеют «из коробки». Хотя NHibernate в плане удобства для DDD явно выигрывает у EF Core, но EF Core активно развивается. И в этом плане я не считаю Value Conversions костылем. В обычном EF все гораздо хуже.
В третьих — нужно еще оценить затраты времени на создание такого proxy-слоя между доменом и ORM. Опять же, EF Core хорошо шагнул вперед и постоянно развивается, а NHibernate в этом плане давно все умеет без хитрых костылей
Чем меньше домен замусорен атрибутами

А причем тут домен? Не нужно вводить инфраструктуру в домен. EF или Nhibernate, а может и простая сериализация в файл не должна влиять на предметную область. Просто доменные VO переводим в объекты инфраструктуры (могут быть классы или байты) и отдаем в соответствующий слой. Все четко и понятно без нарушения границ.
Вот неплохая статья на эту тему, хотя тут частично про EF vs NHibernate

Having the domain model separated from the persistence model

ИМХО, но DDD очень сложен в 100% имплементации на любом из языков программирования. И мы, как разработчики, все равно будем ориентироваться на выбранную ORM или сериализацию, хотя бы с целью оптимизации времени на разработку. DDD сам по себе добавляет весьма заметный оверхед на разработку
Хочу дополнить по Owned types. К сожалению они ещё далеки от идеала. Они не могут быть Null, и их всегда нужно создавать (можно со значениями свойств по умолчанию, но это уже нарушение DDD, так как VO в некорректном состоянии).
Кроме того, EntryState у Owned Type в трекере изменений должен совпадать с его владельцем. К примеру если Entity имеет state modified, то и все его Owned Type тоже должны быть modified и наоборот. Это создает некоторые неудобства и костыли.
Да про null я уже упомянул.
А EntryState у Owned Type — не думаю, что это может быть проблемой, поскольку VO в принципе не могут быть изменены.
Если мы хотим изменить VO/Owned Type, то мы должны создать новый VO. На примере Person и PersonalName это будет примерно так:
var firstName = new Name(model.FirstName);
var lastName = new Name(model.LastName);
person.PersonalName = new PersonalName(firstName, lastName);

Никаких проблем или костылей я тут не вижу
К примеру мы достали из базы данных сущность Person, у него указан какой-то адрес (VO). Мы создали новый инстанс адреса и присвоили его к Person. У Person EntryState == modified, а у адреса он становится Added, потому что он новый. И в подобных случаях необходимо вручную указывать что на самом-то деле адрес не новый, а обновленный. Либо предусмотреть возможность полного обновления VO на основе другого VO через какой-нибудь метод)
Вы меня конечно простите, но использование DDD в реальных проектах не работает. Все что я видел, это примеры уровня HelloWorld, которые разбиваются при попытке создать реальную систему. В паре компаний где я работал, были попытки использовать DDD что выливалось в тонны однообразного кода и одинаковых методов.

1) Приведите мне банальный сценарий, как вы будете реализовывать смену фамилии? Допустим у нас в CRM девушка замуж вышла? А еще бы надо прикрутить смену возраста например, так как пользователь мог внести неправильно данные.

2) Другой сценарий. Нам нужно отдать список клиентов отфильтровав по одному из полей. Где будет находиться эта логика? Должен ли репозиторий знать о специфичных вариантах использования? Судя по тому, что у вас репозиторий возвращает лист, вы предполагаете, что все методы манипулирующие данными будут в нем. Хорошо. А что тогда мы будем делать, когда нам потребуется для разных подсистем давать доступ только к определенным методам? Будем создавать несколько типов репозиториев для одного домена?

3) В приведенном выше коде контроллера имеет место быть каша. Мы почему-то должны знать в нем, что существует Name, Age у Person. В нормальном мире такое обычно выносят в отдельный сервис, но тут DDD так что куда это девать я не знаю.

4) Я абсолютно не понимаю, почему люди с завидным упорством продолжают тыкать в .Net Core UnitOfWork и репозитории, когда DbContext и DbSet предоставляют всю необходимую логику и требуемый уровень абстракции. Свои велосипеды приводят лишь к повторению механических действий по бесконечному созданию репозиториев. Конечно, можно настроить кодогенератор, но в любом мы получаем кучу одинаковых на 90% классов.

Поэтому выскажу мое личное мнение. По-моему, не стоит предлагать красивые архитектуры, если вы на 100% не уверены, что они работают в реальном мире.

Если DDD не работает в ваших проектах — это не означает, что DDD не работает совсем.
У меня был не один проект с DDD, все работает отлично и продолжает развиваться. DDD — штука сложная и понимать это должна вся команда, а не один тимлид. Возможно, по этому у вас DDD и не работает.

  1. Допустим, что имя не должно именяться, только фамилия
    Person.cs
    public class Person {
        
        ...
        
        public PersonalName PersonalName { get; private set; }
    
        public void UpdateLastName(Name lastName) {
            if (lastName == null) {
                throw new ArgumentNullException(nameof(lastName));
            }
    
            this.PersonalName = new PersonalName(
                this.PersonalName.FirstName, 
                lastName);
        }
    }
    возраст — вообще элементарно:
    person.Age = new Age(newAge);
  2. Логика фильтрации — в репозитории. В статье есть пример — IPersons.GetOlderThan(Age age).
    Домен — это ядро всего приложения, все его возможности доступны всем слоям, которые с ним работают. Назовите хоть одну причину, почему подсистема А может иметь доступ к IPersons.GetOlderThan, а подсистема Б — не может. Это ведь одно приложение. Если уж совсем нужно — это можно сделать на уровне сборок, но никакого смысла я в этом не вижу.
  3. Где каша? Почему мы не должны знать про Name и Age у Person? Это же публичный доменный объект.
    Если вы имели в виду преобразование Person -> PersonModel, то тут я согласен — обычно это делает отдельный PersonModelBuilder (сервис уровня Presentation layer). В статье я это опустил, потому что статья не про правильное программирование с DDD в целом, а конкретно про Value Objects.
  4. Я это упоминал в материале. Мы не должны раскрывать детали реализации Persistence. Использование DbContext и DbSet очевидно нарушает это правило. Если проект не использует DDD — конечно же никакие репозитории и UoW не нужны.

Мое мнение — красивые архитектуры очень даже работают, если прилагать для этого некоторые усилия, а не формошлепить. DDD вносит очень заметный оверхед в разработку, но оно того стоит на средних и больших проектах. Для сайтов-визиток это явно не нужно.
Абсолютно соглашусь с вашим комментарием. На мой взгляд статья очень поверхностная и ушла на полшага вперед от пресловутого примера Blogs/Posts, а может и даже вредная. Вся эта мишура рассыпается, как только нужно сделать безнес транзакцию между несколькими доменами. Например, навестить админа, что кто-то сменил фамилию. Или добавить троттлинг на чтение пользователей.
Добавлю от себя еще 5 копеек. Если проект не очень сложный, но есть желание поиграть в ДДД, то в первую очередь нужно создавать доменные DbContext. Далее следует сменить, так сказать, mindset и вместо вызовов методов нужно посылать сообщения между подсистемами. Для этого советую воспользоваться библиотекой MediatR для начинающих или GreenPipes, если охота больше хардкора. Для более сложных проектов нужно переходить к message broker'ам. Тут поможет MassTransit или NServiceBus.
Еще интересует, зачем вы смешали валидацию и VO? Посмотрите насколько убог код вашего контроллера. 3 одинаковых if. А что если я добавлю еще одно правило? Мне надо не забыть изменить еще код контроллера? Для этих целей советую изучить библиотеку FluentValidation.
Если хотите узнать, про настоящее DDD советую доклады Jimmy Bogard'a. А также must see вот это видео, где подробнее раскрыты тезисы из моего комментария.
На мой скромный взгляд, DDD это вообще не про EF и не про UoW в EF зло или нет (зло). Это все слишком низкоуровневые вещи. DDD — это про правильное разделение приложения на Bounded Context, а также разработка протоколов передачи сообщений между этими самыми Bounded Context.
Спасибо за видео, оно действительно весьма полезно, но я его уже смотрел, практически сразу после его появления. Тоже рекомендую всем к просмотру.

1. По поводу сигналов — это очень на любителя. Лично я не испытываю проблем от вызова методов в проектах +- средней сложности. Тут никто никого не ограничивает в выборе инструмента. Будет нужно — рассмотрю необходимость сигналов, но статья — не про это.

2. FluentValidation — штука прикольная, но, я уже это объяснял — правила создания доменного объекта должны задаваться на уровне домена, а не на уровне Presentation. FluentValidation тут разве что поможет несколько упростить код контроллеров, но брать на себя ответственность за логику валидации не должно. К тому же я не задавался целью преподнести прям «production-ready» код, поскольку тема — VO + EF Core. Соответственно и контроллер — максимально упрощен для понимания.

3. Статья — не про DDD в целом, а про Value Objects, которые являются частью DDD, EF Core, которая часто используется как ORM, в том числе и в DDD проектах, и как объединить вместе VO и EF Core

Если вы не испытываете проблем с вызовом методов, как же вы решаете проблему с раздуванием количества зависимостей в сервисах?
Да, валидация — это часть домена, с одной стороны, а также Cross cutting concern с другой, поэтому нужно писать код, учитывая этот дуализм. Никто не мешает вам упаковать валидаторы в один проект с доменной логикой.

Если в сервисе становиться слишком много зависимостей — очевидно, что сервис стал слишком сложным и пришло время его рефакторить, т.к. скорее всего нарушаются KISS и Single Responsibility
Ага, интересно, как вы решите проблему, когда у вас есть простой CRUD PersonsService. Но вот новая задача, при удалении Person нужно оповестить админа. Получается CRU методы получают в нагрузку INotificationService депенденси, хотя не используют ее. Или вы предлагаете создать IPersonsDeleteService? Может я что-то не знаю, но по-моему In-Memory CQRS решает эту проблему на ура.
Суть моих претензий заключается в том, что вы рассказали об очень узком паттерне, при этом подали это все под соусом DDD, наделав кучу антипаттернов в примере. Упрощение для примера — не оправдание говнокоду! Вы даже с IDisposable перемудрили в коде UoW.
Я уже несколько раз отмечал, что писал про EF Core + ValueObjects, а не про паттерны DDD.
«Атрибуты не подходят, так как домен не должен ничего знать про нюансы маппинга.» — а разве ваши валидации — это нюансы маппинга? Чем Range и MaxLength не подошли?
«нюансы маппинга» — не совсем корректно написал, скорее «нюансы ограничений при создании объектов».
Presentation layer не должен брать на себя ответственность устанавливать правила валидации. Его задача — провалидировать данные, т.е. спросить домен, нравится ли ему такое имя/возраст или нет, и если нет — сообщить об ошибке. Потому что Name/Age — это доменные объекты и только домену решать, подходит ли ему Age 25 или Name «Alex»
Почему бы не отдать всё на откуп домену?

public class Person
{
public int Id { get; set; }

[MaxLength(100), ErrorMessage = «Недопустимое имя»)]
public string Name { get; private set или как кому нравится }

[Range(5, 100, ErrorMessage = «Недопустимый возраст»)]
public int Age { get; private set или как кому нравится }
}
  1. Name и Age (как VO) — это доменные объекты и могут быть (и наверняка будут) переиспользованы в других Entity. Для всех будете атрибуты писать и потом везде исправлять, если вдруг требования поменяются?
  2. Value Objects — не обязательно должны быть только частью Entity. В домене они могут использоваться и сами по себе.
  3. Атрибуты никак не помешают мне сделать так:
    person.Age = -25;
    person.Name = "%_!&?"

    И успешно сохранить это в БД. А все потому, что это атрибуты валидации и должны использоваться в DTO (PersonModel в нашем случае).
    System.ComponentModel.DataAnnotations Namespace
    The System.ComponentModel.DataAnnotations namespace provides attribute classes that are used to define metadata for ASP.NET MVC and ASP.NET data controls.
    Т.е. это Presentation layer, а Entity — это доменный объект, а не DTO.
    VO же полностью исключают возможность передачи некорректных данных в домен.

Ну и, ваш Person, если убрать private set — это обычная анемичная модель, со всеми ее преимуществами и недостатками
«Атрибуты никак не помешают мне сделать так… person.Age = -25;… И успешно сохранить это в БД» — пятница принимается в качестве оправдания. Попробуйте. Просто попробуйте.
Перед тем, как написать коммент — я, естественно, попробовал. Все прекрасно сохранилось, потому что эти атрибуты — немножко не для предотвращения таких операций
То же самое спокойно реализуется таким образом:
public class Person
{
    public Person(string name, int age)
    {
        SetName(name);
        SetAge(age);
    }

    public int Id { get; private set; }
    public string Name { get; private set; }
    public int Age { get; private set; }

    public bool SetName(string newName)
    {
        // validation rules here
    }

    public bool SetAge(int newAge)
    {
        // validation rules here
    }
}


При работе с ORM можно добавить конструктор «protected Person()», поменять private на protected, добавить virtual, и т.д. Это уже выбор между прагматичностью и «идеальной» архитектурой.

И насчет ValueObjects, я не знаю, откуда взято то, что они immutable. Идем к первоисточникам (Domain Driven Design by Eric Evans):

An object that represents a descriptive aspect of the domain with no conceptual identity is called a VALUE OBJECT. VALUE OBJECTS are instantiated to represent elements of the design that we care about only for what they are, not who or which they are.


И хороший пример, который показывает, что все зависит от контекста приложения:

Is “Address” a VALUE OBJECT? Who's Asking?
  • In software for a mail-order company, an address is needed to confirm the credit card, and to address the parcel. But if a roommate also orders from the same company, it is not important to realize they are in the same location. Address is a VALUE OBJECT.
  • In software for the postal service, intended to organize delivery routes, the country could be formed into a hierarchy of regions, cities, postal zones, and blocks, terminating in individual addresses. These address objects would derive their zip code from their parent in the hierarchy, and if the postal service decided to reassign postal zones, all the addresses within would go along for the ride. Here, Address is an ENTITY.
  • In software for an electric utility company, an address corresponds to a destination for the company's lines and service. If roommates each called to order electrical service, the company would need to realize it. Address is an ENTITY. Alternatively, the model could associate utility service with a “dwelling,” an ENTITY with an attribute of address. Then Address would be a VALUE OBJECT.

Для каждого свойства каждой Entity будете создавать методы-сеттеры и копи-пастить правила валидации? Зачем? Чтоб написать кучу одинакового кода и потом искать во всему проекту, если Name со 100 увеличится до 150 символов? В Entity валидация данных — явно лишняя. Валидация логики — да, но не данных.
И другое — если среди кода я вижу Name, то я точно знаю, что это Name, а не PhoneNumber или EmailAddress, хотя формально все они — строки. А Age — это именно возраст, а не рост в сантиметрах.
В этом методе программист элементарно может перепутать местами параметры:
void SomeMethod(string name, string phoneNumber);

А в этом перепутать не получится при всем желании:
void SomeMethod(Name name, PhoneNumber phoneNumber);

И насчет ValueObjects, я не знаю, откуда взято то, что они immutable.
Да, их не обязательно делать immutable. На приктике даже само название — Value Object — говорит о том, что это некое значение. Новое значение — новый Value Object.

Еще дополню, что мы можем передать, скажем, номер телефона, в доменный сервис и совершать с ним какие либо манипуляции без привлечения Entity. Если это string — как сервис может быть уверен, что значение корректно, а не какая то ерунда по типу «qwerty$%^»? С VO PhoneNumber такого в принципе не может случится
Копипаст — это зло, тут я с вами соглашусь. Есть много методов не копипастить — выносить код можно разными способами, один из которых — то, что вы описали.

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

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

repeatThis("swift", andDoItThisManyTimes: 3)


И сложно согласиться с аргументом по поводу Value Object — я привел цитату Eric Evans из первоисточника по DDD. Что в этой цитате наводит вас на сделанные выводы о поведении Value Object, кроме названия?
Я не говорю, что DDD нельзя использовать. Однако реальные бизнес приложения с DDD требуют гигантсктого оверхеда. И то, что вы написали выше лишь подтверждает это.

1) Представьте, что в вашем домене 40 свойств. Вы не можете напрямую их изменять из ваших сервисов, а значит у вас будет еше 40 методов на изменение этих свойст. Получается, чтобы просто поменять 5 свойств из модели, мы должны сделать 5 вызовов.

2) Касаемо логики фильтрации. Представьте, что у вас два десятка полей, по которым можно фильтровать. Скорее всего, вы захотите использовать какой-нибудь экспрешион, а не писать 20! методов. Сразу возникает вопрос, где будет происходить его формирование в случае DDD.

3) Права доступа. Пример. Админ может менять паспортные данные, клиент — нет. Желательно, чтобы этих методов команда разработки клиентов просто не видела. Сейчас я могу сделать AbcAdminService.dll и AbcClientService.dll. Они будут использовать одинаковую логику доступа к данным через AbcDataService и одинаковые модели, но доступа к чужим методам не будет. В больших проектах это крайне полезно, ибо десятки похожих методов просто путают разработчиков и усложняют разработку.

4) Да, я имел ввиду, что на уровне контроллера вы должны знать только Presentation модель. Да, вы правильно отметили, что нужен сервис AbcModelBuilder. Вопрос, почему мы тогда не можем использовать хотя бы AbcDataService, инкапсулирующего логику создания expression для запросов и прочие однотипные операции? Я веду к тому, что сервисная модель гораздо лучше вливается в реальные проекты.

5) Мы не должны раскрывать детали реализации Persistence. Да EntityFramework и скрывает от вас реализацию Persistence. Вы можете использовать кучу конкретных провайдеров, хоть в память, а хоть и свой для сохранения в файл напишите. Когда вы используете DbContext, вы в общем случае понятия не имеете, что там у вас используется.

6) throw new ArgumentNullException(nameof(persons));
Никогда. Нет, не так. НИКОГДА так не делайте для бизнес логики. Просто померяйте скорость обработки исключений. Вместо 20к запросов в секунду у вас будет 200.
DDD отчасти диктует более логичную структуру приложения, где бизнес логика находится на по возможности на самых низких слоях. То есть определенный код будет в любом случае присутствовать, вопрос только — где именно.

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

2. Для этого есть Queries, как говорится не репозиторием единым…

3. В таких случаях можно делать определенные методы internal и доступными только сервисам, а методы сервисов, которые проверяют права доступа — доступными извне. Опять же, код тот же, просто немного по-другому структурирован.

5. Я не сильный фанат EntityFramework, но DbContext уже реализует паттерн UnitOfWork и большинство ORM поддерживает TransactionScope. Одна из идей репозитория в DDD, что он сохраняет Aggregation Root, то есть UnitOfWork мало нужен. Для случаев, когда нужно сохранить несколько Aggregation Root, есть Transaction Scope и UnitOfWork тоже мало нужен.

6. Это зло на продакшене, но такие ошибки должны всплывать раньше и на проде такой код теоретически даже не будет вызываться. Это просто способ не думать о том, откуда пришел NullPointer :)
1. Если свойство доступно для изменения, то это будет просто SomeProperty { get; set; } и доступно всем. Никаких методов для этого не нужно. Отдельный метод нужен только, если на изменение свойства завязана какая нибуть логика. Скорее всего это будет доменный сервис или метод в Entity (в простых случаях) и сеттер становится private или internal.
2. Ничего не мешает передать фильтры в Repository и конкретный запрос с фильтрами формировать уже там. С IQueryable это очень просто.
3. Логика изменения паспортных данных выносится в доменный сервис, которым пользуются только админы. Ограничить можно разными сборками (доменные сервисы не обязательно должны быть в одной сборке с Entity), но если это один VS Solution — программисты клиентской части все равно будут видеть этот сервис.
Code Review, кстати, никто не отменял.
4. Если я вас правильно понял, то AbcDataService — это PersonRepository. Репозиторий — это же обычный сервис, только находится в Persistence и отвечает за сохранение и выборку данных, за что и получил отдельное название.
5. Entity Framework — это уже раскрытие реализации. В проекте в дополнение может использоваться легковесный Dapper для быстрой выборки, могут быть чистые SQL запросы через ADO.Net, загрузка из csv файлов, что угодно — и все это в одном проекте. Persistence — это черный ящик, и что там внутри — остальные слои знать не должны.
6. Подразумевается, что параметр не может быть null. Если это произошло в домене — однозначно что то пошло не так и бизнес-процесс далее не может выполнятся, что исключение в том числе и делает. Опять же, если это произошло в домене — это нештатная ситуация, требующая разбора полетов. Presentation layer (как и другие слои) должен проверить все данные перед тем, как передавать их в доменные сервисы. Не проверил и пропустил null — это баг. А сам по себе if (...) в 100 раз не замедлит выполнение
Представьте, что в вашем домене 40 свойств. Вы не можете напрямую их изменять из ваших сервисов, а значит у вас будет еше 40 методов на изменение этих свойст. Получается, чтобы просто поменять 5 свойств из модели, мы должны сделать 5 вызовов.

А где тут написано, что нужен отдельный метод на изменение каждого свойства?


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

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


Права доступа. Пример. Админ может менять паспортные данные, клиент — нет. Желательно, чтобы этих методов команда разработки клиентов просто не видела.

Кажется, в DDD было что-то про фасады и контексты...


Да EntityFramework и скрывает от вас реализацию Persistence. Вы можете использовать кучу конкретных провайдеров, хоть в память, а хоть и свой для сохранения в файл напишите.

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


Чем более ограниченный интерфейс для слоя хранения используется — тем проще этот самый слой хранения писать.


Никогда. Нет, не так. НИКОГДА так не делайте для бизнес логики. Просто померяйте скорость обработки исключений. Вместо 20к запросов в секунду у вас будет 200.

Предполагается, что в рабочей программе эта проверка никогда не сработает. Исключение, которое никогда не возникает, ну никак не повлияет на производительность.

Я так понимаю что Value Objects нельзя использовать в качестве Primary Key.
Даже в своём примере вы используете Guid, вместо PersonId.
Guid это ValueObject из стандартной библиотеке. Да, можно его создать с дефолтным значением (0000000...) но это все равно будет валидное состояние. Как для int 0 это валидное состояние в контексте целых чисел. Да int это тоже VO. Возможно в вашем контексте это не валидное (например количество атомов у клетки) тогда вы создаете VO специфичный для вашего Контекста которое не может быть меньше единицы например.
Вот только хочется-то использовать произвольный Value Object, а не ограниченный список «ValueObject из стандартной библиотеки»

First-class поддежрка Value Objects планируется в EF Core 8.0: Plan for Entity Framework Core 8. Можно поддержать идею на GitHub

Чтобы работал Linq в Where для ValueObject необходимо реализовать implicit cast для этих объектов.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Изменить настройки темы

Истории