Comments 45
Обработчик команды может возвращать значение. Если не согласны спорьте с Грегом Янгом
Можно ссылку на то, где Янг это подтверждает?
При редактировании объекта, post запрос содержит только изменяемые поля {id:someid, param:newval}, как решаете проблему проверка прав доступа?
В простейшем случае так, хотя лучше использовать АОП:
void Handle(PostUpdateCommand command)
{
if(!CheckAccess(command)) throw new SecurityException("Don't try to hack me!");
//...
}
Так или иначе, при обработке ICommandHandler, объект надо будет загрузить целиком из БД. Это будет IQuery внутри ICommandHandler? Или напрямую через EF запрос делать будете?
По-умолчанию загружаем напрямую весь агрегат из ORM с помощью TypeConverter (ссылка на код в статье) для AutoMapper. ORM абстрагирована за ILinqProvider, поэтому завязки на конкретную реализацию нет. Может использоваться Query для получения каких-то объектов. Если система много пишет, то ORM не используется. Dapper пишет напрямую.
Все руководители разработки, применяющие DDD, с которыми я обсуждал тему, отметили «дороговизну» этой методологии, в первую очередь из-за отсутствия в книге Эванса ответов на практические вопросы «как мне сделать FooBar, не нарушая принципов DDD?».
Странно у вас как DDD используется. DDD это ведь не набор правил, это набор рекомендаций к дизайну приложений, в конечном счёте сводящийся к тому, что бизнес превыше всего. Если у вас есть бизнес-необходимость, сделайте отдельную read-модель для отчёта.
DDD предполагает делегирование части задач по аналитике разработчикам.
Не предполагает. Предполагается понимание аналитиков разработчиками и наоборот, но работу аналитиков делают по прежнему аналитики.
В неустоявшихся бизнесах (особенно стартапах) часто нет никакого понимания предметной модели. Все может меняться ежедневно. В этих условиях бесполезно требовать от участников бизнес-процесса использовать единую терминологию.
В стартапах или нет, модель всегда эволюционирует вместе с её пониманием разработчиками. DDD не предполагает единовременное формирование всех знаний о предметной области, DDD лишь про фокус на модели.
Event Sourcing требует CQRS
Строго говоря, это неверно. Я вполне могу представить себе как клиенты делают запросы напрямую в агрегат, который собирается из эвентов. Это убивает некоторые возможности, но это возможности CQRS, а не ES.
В случае DDD, чтобы знать где искать ту или иную бизнес-логику необходимо понимать предметную область. В случае CQRS программист всегда знает, что запись происходит в обработчиках команд, а для доступа к данным используются Query.
Почему вы их противопоставляете? CQRS отлично работает на DDD бэкграунде, потому что конкурировать там нечему. Это всё равно что говорить, что CQRS лучше Agile.
Если у вас есть бизнес-необходимость, сделайте отдельную read-модель для отчёта.
У отчета может быть не read модель?
Разве при формировании отчета мы намереваемся изменить состояние системы?
По-вашему, если операция продолжительная, то сразу сага? То, что построение отчета занимает много времени, выполняется асинхронно, с возможностю указания параметров… — это уже детали реализации read-model, имхо.
У read-model не должно быть деталей реализации, read-model — это проекция состояния системы на определённый клиентский запрос, она должна быть в оптимальной для этого запроса форме и в идеале, доставаться за один запрос в БД. Когда у вас есть какая-то логика в получении read-модели, возможно, у вас не полноценный CQRS.
Если в вашем приложении отчёт является полноценной сущностью со своим жизненным циклом (пусть и коротким), то он будет корнем агрегата или находится в составе агрегата.
Я решил, что под отчетом подразумевается «информация оформленная в удобном для пользователя виде» — статистика, графики и т.п. Если же «отчет» — это термин предметной области, то спору нет.
read-model — это проекция состояния системы на определённый клиентский запрос, она должна быть в оптимальной для этого запроса форме и в идеале, доставаться за один запрос в БД. Когда у вас есть какая-то логика в получении read-модели, возможно, у вас не полноценный CQRS
Соглашусь, что read-model открывает возможности для оптимизации операций чтения. Но не могли бы вы пояснить (или поделиться ссылкой) — почему наличие read-логики делает CQRS неполноценным?
Я решил, что под отчетом подразумевается «информация оформленная в удобном для пользователя виде» — статистика, графики и т.п. Если же «отчет» — это термин предметной области, то спору нет.
Так это Hydro спросил, может ли отчёт быть больше, чем просто read-модель. Да, может, но я говорю о ситуации, когда отчёт необходим как статистика, графики и т.п. И да, там достаточно только read-модели.
Но не могли бы вы пояснить (или поделиться ссылкой) — почему наличие read-логики делает CQRS неполноценным?
Автор тезисом указал, что это не реализуется с использованием DDD. Я же в этом сценарии не вижу в DDD никакой помехи: если отчёт — это бизнес-понятие, то оно должно быть отражено в виде сущности в том или ином контексте, а в терминах CQRS стать частью write-модели. Если отчёт — это лишь ещё одна проекция состояния системы и собственного жизненного цикла не имеет, то вы можете на основе имеющейся в системе информации подготовить read-модель, именуемую «отчёт» и сохранить её в максимально удобной для получения форме. Именно подготовить, а не строить на лету какие-то SQL запросы с JOIN'ами и кверить все агрегаты в системе.
Про это писал Vaughn Vernon в книге Implementing Domain Driven Design, часть его труда посвящена как раз использованию CQRS вместе с DDD.
В общем-то, можно добавить, что CQS (разделение чтения и изменения данных) — это крайне полезный паттерн при любой архитектуре.
DDD. Выводы
ОЧЕНЬ дорого
Работает хорошо в устоявшихся бизнес-процессах
Плохо масштабируется
Сложно реализовать в высоконагруженных приложениях
Плохо работает в стартапах
Не подходит для построения отчетов
Требует особого внимания с ORM
Слова Entity лучше избегать, потому что его все понимают по-своему
Программиста нанимать не пробовали?
Мой 6-ти летний опыт с DDD дает ровно обратные выводы.
public static class ValidationExtensions
{
public static string Check<T>(T obj, Func<T, bool> func, string message)
=> func(obj) ? null : message;
public static string[] Check<T>(this Func<T, string>[] funcs, T data)
=> funcs
.Select(x => x.Invoke(data))
.Where(x => x != null)
.ToArray();
public static ValidationResult GetResult<T>(this Func<T, string>[] funcs, T data)
{
var checkResult = funcs.Check(data);
return checkResult.Any() ? new ValidationResult(checkResult.Join(",")) : ValidationResult.Success;
}
public static string Join(this IEnumerable<string> strings, string delimiter)
=> strings.Aggregate((c, n) => $"{c}{delimiter}{n}");
}
public interface IValidator<in T>
{
ValidationResult Validate(T obj);
}
Вообще, валидацию лучше выносить в отдельные классы, потому что есть разница хотите вы сделать 1 запись в БД или импортировать 500.000. Желательно иметь возможность использовать код валидации и сохранения как на массовых операциях, так и единичных. За исключением случаев, когда нужно вставлять действительно большие массивы данных очень быстро это возможно за счет map/reduce и управления batch size и лайфтамом контекста БД через explicit scope
Если нравится АОП и производительность позволяет, можно еще так: http://simpleinjector.readthedocs.io/en/latest/aop.html#decoration
Вот допустим у нас есть проекты и их менеджеры. Есть требование что обращаться по-любому поводу к «чужому» проекту нельзя, даже имя узнать.
Агрегат Проект решал эти вопросы доступа сам, зная кто его менеджер и вся эта логика (она была сложнее и касалась не только проектов) была на стороне Command, в агрегатах.
На стороне Query всю эту логику приходилось дублировать чтобы люди запросами типа GetProjectBudgets(id) не узнали лишнего.
Думал над вынесением логики доступа к ресурсам в отдельный слой, ещё над разделением потоково на Command и Query. Не успел, сменил свой рабочий проект.
Что бы вы посоветовали сделать в этом случае в рамках CQRS и DDD?
Я делаю так: есть два вида секьюрити: безопасность данных и безопасность операций.
Секьюрити операций сейчас пропустим.
Безопасность данных — это ограничение доступа к "чужим" данным, как на чтение, так и на запись. Реализуется она в виде фильтров данных, либо на стороне базы (row-level security или самопальный велосипед), либо генерацией и применением SQL фильтра (where
) ко всем (в том числе update
) запросам из сервера приложения. Если используется LINQ, можно такой фильтр генерировать в терминах доменной модели.
Основная проблема — это создать архитектуру данных, которая позволит сохранить баланс между избыточностью (денормализацией данных) и производительностью таких секьюрити-фильтров.
Отвечая более конкретно на ваш вопрос — если запись и чтение происходят в два источника, то логику придется дублировать, другой вопрос, что можно дублирование минимизировать, используя похожую структуру данных в read model и master data, интерфейсы и полиморфизм и т.п. Ну и конечно, надо отделить вычисление прав пользователя от преобразования набора прав в конкретный фильтр для сущности.
Декораторы и AOP помогают когда можно полностью запретить или полностью разрешить вызов command/query.
А для запросов по площади типа GetAllProjects() от конкретного юзера надо городить в самом теле запроса логику фильтрации.
В read-model у меня была тонна .Where(UserBelongsToProject) или что-то вроде .Where(EntityNotDeleted) и если я в каком-то запросе пропустил эти фильтры — это потенциальная брешь в безопасности данных.
А если взять какой-то сложный запрос на финансовый отчёт по всем доступным проектам, тут вообще кошмар, т.к. приходится джойнить таблицы и не забывать что для каждой такой таблицы надо тоже применять фильтры.
Получается что мне нужен кто-то на стороне Query, кто решает вопрос доступа к данным, а значит там надо городить отдельную доменную модель, что убивает смысл CQRS.
Проблема, повторюсь, в том, что это логика защиты данных, и решать ее надо на уровне данных (хотя сами вычисления прав доступа станут частью бизнес-логики).
CQRS создан для оптимизации производительности (ну и плюс некоторые задачи на него хорошо ложаться), он не декларирует никак доступ к данным (ну за исключением разделения read/write data model), и ждать от него изящества в работе с данными не стоит, разве что можно добавлять фильтрацию по правам при восстановлении агрегата из набора событий.
Изящного решения этой проблемы нет даже в DDD, есть только более-менее удачные для конкретного проекта.
Удачное решение, как по мне, — это дополнительная модель данных, автоматически включающая секьюрити.
Думал над вынесением логики доступа к ресурсам в отдельный слой, ещё над разделением потоково на Command и Query. Не успел, сменил свой рабочий проект.
Все верно думали. Просто нужно иметь абстракцию, которую вы сможете переиспользовать и там и там. Не всегда это просто. Чтобы была конкретика нужно код смотреть.
Один из вариантов: выносить такую логику в АОП, например так. Погуглите Cross-cutting concern. Вот здесь кое-что есть по-русски на эту тему.
Один из вариантов: выносить такую логику в АОП, например так. Погуглите Cross-cutting concern. Вот здесь кое-что есть по-русски на эту тему.
Я так и сделал. все что требовало доступа уровня проекта имело маркерный интерфейс IProjectRequest, на который был настроен декоратор, отказывающий пользователю, если это не его проект. Но приходилось это делать и на командах, и на запросах.
Таким образом у меня бизнес логика «протекала» из агрегатов и размазывалась по декораторам. А сам агрегат уже не был самодостаточной сущностью и мог правильно существовать только с внешними декораторами.
Та же проблема и с выделением логики во внешний слой. Логика утекает вообще не пойми куда, а агрегат Проект должен надеяться что вопрос доступа к конкретному проекту был решён за него.
Я обычно при выборе архитектурных решений иду не от паттернов-шматтернов, а от Jira (или что там используется): смотрю где ботлнеки, начинаю прикидывать на что уходит время.
То, что у вас логика расползлась — это, конечно, не очень. С другой стороны, вы считали несете ли вы убытки от этого в монетарном выражении? Не теряли ли вы больше времени на другие задачи?
У нас аутсорсинговая компания, поэтому CQRS нам сейчас хорошо подошел. Когда я работал в трейдинге (продуктовая разработка), мы использовали более DDD-шный подход.
Самый распространенный в гугл-группе CQRS, вопрос по словам Грега Янга: «Босс просит меня построить годовой отчет. Когда я поднимаю в оперативную память все корни агрегации у меня начинает все тормозить. Что мне делать?». На этот вопрос есть очевидный ответ: «нужно написать SQL-запрос». Однако, написание ручного SQL-запроса – это однозначно против правил DDD.
В своем проекте решал проблему так, все спецификации реализуют паттерн Посетитель, в итоге для каждого типа репозитория вызывается свой вариант проверки, для in-memory это простая лямбда, для SQL Server создается фрагмент для вставки в WHERE часть запроса. Поднимать в память данные из базы для фильтрации — недопустимо.
Соответственно при создании новых вариантов отчета добавляются новые спецификации, или используется комбинация готовых.
На уровне абстракций
public interface ISpecification<T>
where T : IEntity
{
bool IsSatisfied(T t);
}
public interface IRepository<T>
where T: IEntity
{
// ....
IEnumerable<T> Get(ISpecification<T> specification);
// ....
}
Немного ниже где известно про типы репозиториев:
public interface ISqlServerSpecification
{
SqlServerSpecification GetWhereFragment();
}
Пример спецификации:
public class AgeLessThenSpecification:
ISpecification<Human>,
ISqlServerSpecification
{
private readonly short _maxAge;
public AgeLessThenSpecification(ushort maxAge)
{
_maxAge = maxAge;
}
public bool IsSatisfied(Human h)
{
return h.Age <= _maxAge;
}
public SqlServerSpecification GetWhereFragment()
{
return new SqlServerSpecification
{
Where = "WHERE [Age] <= @Age",
Parameters = new SqlParameter[] { new SqlParameter("Age", _maxAge) }
};
}
}
На уровне репозитория (SQL) выполняется проверка реализует ли спецификация расширение для SQL, если да, то будет использоваться запрос.
private bool TryGetWhere(ISpecification<T> specification, out SqlServerSpecification condition)
{
var sqlSpecification = (specification as ISqlServerSpecification);
if (sqlSpecification == null)
{
condition = SqlServerSpecification.EmptySpecification;
return false;
}
condition = sqlSpecification.GetWhereFragment();
return true;
}
- добавить книгу
- если автора еще нет, создать автора
- изменить статистику общую (сколько книг, авторов и т.п.)
- изменить статистику по автору
Т.е. до того как отработает UnitOfWork или его аналог, нужно создать набор этим изменений. И вот как их правильно собирать, ни у кого не встречал.
По правильности имеется в виду следующее, иметь обощенный инструмент, который позволяет:
- формировать набор операций для транзакции
- собрать ключи объектов которые будут затронуты операцией
- блокировка ключей на время транзакции
- после создания списка операций выкинуть те, которые не приводят к изменению состояния
- создание виртуальной модели будущего состояния для вычисляемых полей (по примеру, количество книг автора, и общее количество книг)
- автоматическое определение слабосвязанных объектов, затрагиваемых изменением и их обновление
- выполнение транзакции и откат по всем репозиториям в случае ошибок (например, откат в кэше оперативы и в базе данных)
Был ли такой опыт и как решали этот вопрос у себя, или использовали что-то готовое?
В итоге был написан отдельный фреймворк, который сначала позволяет собрать план выполнения запросов в транзакции, затем оптимизирует этот план, и затем исполняет в одной транзакции.
Решали так: в основное хранилище пишем транзакционно «добавили товар» и выкидываем событие «товар добавлен». Событие ловит диспетчер. В диспетчере логика: добавили товар — выкинь еще события «обнови эластик», «обнови ленты», и т.д. В случае облома обработчика любого события пишем в лог.
События отправляются в RabbitMQ / Akka.net, обработчики уже за пределами транзакции БД. Если что-то обламалось по логам вызываем заново обработчики. Если все жестко сломалось Вызываем Supervisor, который может по корневому хранилищу (релаяционная БД) построить эластик и т.д. Пуш уведомления, СМС и все-такое в этом случае не рассылаем, потому что можно разослать все повторно.
«Жестко сломалось» было пару раз при сильном изменении корней агрегации.
По идее, вот это
- формировать набор операций для транзакции
- собрать ключи объектов которые будут затронуты операцией
- блокировка ключей на время транзакции
- после создания списка операций выкинуть те, которые не приводят к изменению состояния
функционал Unit Of Work.
Любая доп прослойка типа DTO или ICommandHandler — вот удорожание системы и разработки, а также доп лейтенси и уменьшение производительности в итоге.
Как мы попробовали DDD, CQRS и Event Sourcing и какие выводы сделали