Комментарии 108
Автору спасибо за статью. Собираю по крупицам информацию. Не задумывался, почему может быть плохо использовать query внутри command. Тут был поднят вопрос без ответа на него: что делать с автосгенерированными БД ID? Я полагаю, тут истинно верного решения нет?
Единственное, что мне до сей поры очень плохо ясно, это куда во всей этой схеме пихать валидацию?
Посмотрите еще раз абзац с декораторами. Там пример, как работать с валидацией. Middleware тоже подойдет со следующими ограничениями:
- некоторые сущности могут быть вычитана по два раза
- транзакционностью придется управлять за границей CQRS-стека
Тут был поднят вопрос без ответа на него: что делать с автосгенерированными БД ID? Я полагаю, тут истинно верного решения нет?
Если у вас синхронный CQRS без шины — возвращайте Id из команды / хендлера и не мучайтесь. Если с шиной, то у вас либо должен быть механизм доставки доменного события «заказ создан» до пользователя, например web sockets. Либо отказ от автогенерируемых Id — создаем Guid'ы на клиенте.
Так я смогу в будущем переключить выполнение команд с основного потока в очередь.Сомневаюсь. Придется переделать UI, частино придется переделать валидацию, и, скорее всего, сами обработчики (чтобы ничего не возвращали). Прозрачно «переключить» точно не получится.
Единственное, что мне до сей поры очень плохо ясно, это куда во всей этой схеме пихать валидацию?
В одном из видео от Uncle Bob он как раз отвечает на этот вопрос. Все предельно просто. Вы делаете валидацию там, где она вам на данный момент нужна. Весь фокус в том, чтобы определиться что, почему и как вы собираетесь валидировать. Это "определение" делает Ваше высказывание
А что, если мне нужно проверить одни и те же данные в разных местах?
недействительным. Может быть вы и проверяете одни и те же данные, но контекст уже будет другой.
Например:
- Приходит запрос (request) который мы трансформируем в query/command-объект. Для инициализации самого объекта валидируем наличие и типы параметров запроса.
- далее скомпанованный объект доходит до обработчика (command-/query-handler). Для обработки валидируем те же самые параметры, но уже в другом контексте. Идет проверка на соответствие с требованиями бизнес-логики.
ну и тд и тп
Единственное, что мне до сей поры очень плохо ясно, это куда во всей этой схеме пихать валидацию?
Валидацию нужно пихать туда, где вы будете обрабатывать ошибки и отдавать клиенту, то есть в контроллер.
Валидировать команды в command heandlers или middleware не правильно, так как вы не сможете корректно отреагировать на обнаруженные ошибки.
Да, вы можете сделать валидирующий middleware и бросать ValidationExcepton
, но это неверно с точки зрения семантики, так как невалидные данные не являются исключительной ситуацией.
Рекомендую посмотреть готовые библиотеки реализующие CQRS, Middleware, Payload.
1. Грубая проверка пользовательского запроса: форма, обязательные параметры, их типы, глобальные статические правила
2. Локальные (без обращения к внешним сервисам) бизнес-правила
3. Глобальные бизнес-правила
Соответственно и размещать их нужно на нужных слоях и часто приходится выбирать или выворачивать кишки наружу, или бросать исключения.
В целом, я согласен, что уровней валидации несколько, но вся валидация все равно должна выполняться в контроллере, а на слой бизнес логики должны приходить уже валидные данные.
А вот если на слой бизнес логики придут невалидные данные, то это уже будет исключением.
Во первых, мы должны на уровне интерфейса ограничить возможность выполнить платеж на сумму превышающую остаток по счету.
Во вторых, да. Нам нужно проверить остаток по счету прежде чем мы отправим запрос дальше по стеку. Это не нужно делать прямым обращением к репозиторию. Это можно сделать через валидаторы и спецификации.
В ином же случае нам придется валидацию размазывать по коду и бросать исключения в сервисах и моделях, а исключение это само по себе плохо и использовать их для передачи данных в верх по стеку крайне не желательны.
Однако, это не отменяет необходимости валидировать данные на доменном слое. Причем валидирование как бизнес правил, так и самих данных. Об этом говорил Вернон. Я не приверженец двойной валидации, но это правильней. И тут каждый выбирает свою истину. Кто-то размазывать валидацию, кто-то концентрирует валидацию в контроллерах, а кто-то использует двойную валидацию.
Исключение само по себе не плохо. Но можно обходиться и без исключений, если их недостатки перевешивают в конкретном случае преимущества. Например, как в Go или в старом добром C.
Я в целом считаю неправильным концентрировать валидацию в контроллере, поскольку зачастую полноценная валидация требует множество знаний о внутренних процессах сервисов о сущности, то есть связана с нарушением инкапсуляции, с одной стороны, а, с другой, снимает ответственность за консистентность состояния с сущностей или сервисов и перекладывает её на контроллер, у которого и так достаточно ответственностей обычно.
Если говорить о валидации в контексте CQRS, то на уровне контроллера достаточно, по-моему, провалидировать данные запроса на предмет возможности создания команды, а потом передать в шину или хэндлер и ждать не выплюнут ли (исключения, коды возврата или ещё что-то — детали реализации) они её с ошибкой. Контроллер не должно интересовать на каком уровне или слое команда не смогла выполниться и почему.
Это тоже самое, что ValidatorMiddleware. И middleware на мой взгляд предпочтительней декораторов.
Но сути это не меняет. Нужно бросить исключение чтоб прекратить выполнение и поднять сообщение(я) об ошибке наверх в контроллер. Я не в восторге от такого подхода.
Окей. Вы подмешиваете данных в возвращаемое значение. Тоже метод решения задачи. С таким же успехом можно использовать аргумент out (возвращаемся к разделу ICommandHandler должен всегда возвращать void?).
Можно придумать еще с 10 костылей, но зачем?
Есть такой подход в ddd, как ContextValidation. Думаю, его лучше юзать
https://martinfowler.com/bliki/ContextualValidation.html
https://lostechies.com/jimmybogard/2009/02/15/validation-in-a-ddd-world/
Главный посыл таков — "Instead of answering the question, “is this object valid”, try and answer the question, “Can this operation be performed?”.
Например, мы можем сохранить некоторый заказ без адреса в базу, но выполнить операцию отправки нет.
Допустимо ли строить архитектуру таким образом?
Потому что строить чистый CQRS изначально, с отдельным представлением данных для CUD и R — довольно дорого, особенно в условиях, когда даже нет целостного представления о том какой должна быть модель (а это означает постоянный рефакторинг. Много рефакторинга).
И второй вопрос — что скажете на счёт MediatR в качестве инфраструктуры для CQRS. Там правда всего один интерфейс, но проблема деления на команды и запросы решается нэймингом.
строить чистый CQRS изначально
тут у меня два вопроса — что в вашем понимании "чистый CQRS" и почему введение двух интерфейсов вместо одного для разделения операций чтения и записи вдруг стало дорого?
CQRS то очень простой паттерн, никаких там комманд басов и прочей ерунды. Просто с точки зрения клиентского кода операции чисто на запись и операции чисто на чтение должны принадлежать разным интерфейсам. Это не значит что вы прям обязаны делать разные модели данных, просто за счет этого разделения у вас появляется такая возможность.
У Грега Янга по поводу всех этих безумств с шинами команд и query bus есть отдельная статья: CQRS, Task Based UIs, Event Sourcing agh!
Я спросил, на счёт того — допустимо ли использование бизнесовых сущностей и контекста БД не только для модификации, но и для чтения на начальных этапах проектирования системы, так как домен там постоянно изменяется по мере уточнения требований. А так же спросил на счёт возможности использования готовой библиотеки в качестве инфраструктурной основы.
допустимо ли использование бизнесовых сущностей
допустимо, но тогда это не CQRS так как доступ к данным организован через один и тот же объект для двух разных типов операций. (причем частенько это загрязняет сущности специфическими для UI вещами вроде геттеров, но это не так страшно в большинстве случаев)
С другой стороны — CQRS это не цель, это инструмент. Если вам так удобно — то все хорошо. Более того, если у вас скажем большая часть приложения это CRUD и есть только маленький кусочек где все не так просто — возможно разделение нужно только там.
Допустимо, но тогда это не CQRS так как доступ к данным организован через один и тот же объект для двух разных типов операций.
Мы довольно долго так думали, пока не нашли
QueryableExtension'ы
. Кейсы, где нужно возвращать сущности как-то после этого кончились.А если на начальном этапе разработки в качестве хранилища взять тот же EF Core, и оперировать его контекстом, как в командах, так и в запросах. А потом, по мере роста нагрузок и усложнения логики — проводить постепенный рефакторинг, устраняя узкие места (с использованием того же Dapper или вообще ADO).
Я в абзаце «Возвращать из QueryHandler сущности или DTO» примерно это и предлагаю. Что вас смутило?:)
уже «заплатить по счетам»
практика показывает что никто потом не платит.
Тут важный момент — если на момент разработки у вас нет профита от разделения чтения и записи — то может быть профита и дальше не будет? Ну то есть, если все работает и так — для чего вы дальнейшие действия будете делать?
схема может ощутимо так измениться в процессе рефакторинга
Если делать рефакторинг чаще, можно уменьшать объем изменений. Актуализация же DTO в целом не настолько дорогая штука. Скучно — да, но не долго.
BTW у автомаппера также есть возможность не писать явный маппинг
И второй вопрос — что скажете на счёт MediatR в качестве инфраструктуры для CQRS. Там правда всего один интерфейс, но проблема деления на команды и запросы решается нэймингом.
MediatR реализует идею separation of concerns, только вместо декораторов использует Behaviors. Даже используется «фишка» с пустым дженерик-интерфейсом, чтобы указать возвращаемый тип:
public class Ping : IRequest<string> { }
В данном контексте я не вижу разницы: использовать SimpleInjector с декораторами или MediatR. Есть смысл проверить производительность и выбрать, что работает быстрее. SimpleInjector компилирует деревья выражений, а MediatR использует механизм фабрик.
Mediatr очень хорошо подойдет. Сделали на нем уже два проекта.
Посмотрите мои ссылки выше и ниже, там статьи от создателя медиатра с хорошей теорией и практикой.
IRequest
без сервисного слоя?Есть несколько сервисов типа LdapServise/AuthService, но всякие бизнес и crud операции напрямую в обработчике.
Вот кстати еще вопрос — а кошерно ли использовать CommandHandler'ы внутри CommandHandler'ов?
Джимми Боггард пишет, что нет и лучше юзать композицию/реализовывать несколько commandHandler'ов в одном классе
По поводу только одного интерфейса- достаточно завести свой собственный интерфейс и унаследовать от IRequest
Что-то вроде
interface IQuery: IRequestКовариацию не забудьте только, мне с телефона неудобно редактировать
Субъективно кажется, что имея
IRequestHandler<SomeCommand>
и IRequestHandler<SomeQuery>
проще запутаться и случайно добавить мутацию в IRequestHandler<SomeQuery>
, чем в случае двух интерфейсов: IRequestHandler<SomeQuery>
, а в IQueryHandler<SomeQuery>
. Это минус.Библиотека выглядит приятно. Если у вас нет проблем с соблюдением нейминага и код-ревью не в web-морде, а в IDE (чтобы можно было по файлам пробежаться, посмотреть, проверить наличие мутаций), решение вполне имеет право на жизнь.
У медиатра ваш прием с ResultHandler'ом не пройдет, поскольку вы меняете тип возвращаемого значения.
Но всякие валидации, транзакции и прочие шаблоны очень здорово залетают в pipeline behavior
Спасибо за статью!
У меня нубский вопрос — а как быть с загрузкой данных во write части? Городить ещё репозиториев для CommandHandler'ов?
Сейчас я в обработчиках использую EF контекст напрямую и при тестировании подменяю хранение на InMemoryStorage (у нас ef core) и вроде всё хорошо, но как-то не по себе.
В общем-то, пример можно посмотреть здесь у Jimmy Bogard'a
Сейчас я в обработчиках использую EF контекст напрямую и при тестировании подменяю хранение на InMemoryStorage (у нас ef core) и вроде всё хорошо, но как-то не по себе.
А что вас не устраивает в таком подходе? DbContext уже реализует репозиторий, зачем городить абстракцию поверх. Можно отвязаться от EF и инжектировать
IQueryable<T>
через фабрику: dbContext.Set<T>()
, но тогда EF не будет кешировать обращения по Id (по-умолчанию кешируются только вызовы метода Find
). Это тоже можно исправить, но работа с контекстом — это вообще тема отдельной статьи.На практике абстракция от ORM создает больше проблем. Если можно говорить о какой-то заменяемости между
EF
и NHibernate
, то остальные, даже если поддерживают LINQ требуют значительного переписывания кода для замены. Так что первая ложь — ORM абстрагирует вас от БД и вы сможете через 5 лет разработки переехать с Оракла на Постргес. Вторая — IQueryable
абстрагирует вас от деталей реализации.Сейчас посчитал — реально пять лет писал на MSSQL со старым EF, переехал на постгрес за выходные. Потом была схожая история с переездом с MSSQL на MySQL и на AuroraDb (последнее не особо валидно, поскольку аврора это форк innodb). Возможно, дело в каких-то фундаментальных различиях оракла с постгресом?
сначала хотелось бы поблагодарить за одну из немногих более или менее вменяемых статей по CQRS и DDD. на удивление очень много пишут явный бред. спасибо!
далее к вашему примеру
Но как быть с Id, генерируемыми БД? Например, мы отправили команду «оформить заказ». Номеру заказа соответствует его Id из БД. Id нельзя получить, пока запрос INSERT не выполнен.
а никак. потому как тут явно кто-то забыл один из главных принципов DDD — Persistence Ignorance. Ваша система, разрабатываемая по принципам DDD, по умолчанию "забила" на БД. Идентификатор Entity является неотъемлимым признаком сущности. Нет идентификатора, нет Entity. Независимо от существования или отсутствия БД.
Как только возникает необходимость или зависимость от автогенерированных ID — фэйл, рукалицо и фтопку.
я не совсем понимаю, почему Вы автогенерируемые Id вместе с GUID/UUID "в кучу скидываете". как раз GUID и есть отличная замена для БД-идентификаторов.
попробуйте ответить самому себе на вопрос: На каком основании Вы в своей системе создаете зависимость от имплементации БД-идентификаторов? кто гарантирует, что со следующим обновлением БД-движка или его заменой останутся те же самые идентификаторы?
это, конечно, действительно толко в ситуации, если вы не пишите свой БД-движок
кто гарантирует, что со следующим обновлением БД-движка или его заменой останутся те же самые идентификаторы?
Приведите реальный пример обновления СУБД, чтобы слетели первичные ключи?
Guid'ы совсем не бесплатные. Если есть возможность использовать автоинкремент в БД зачем усложнять себе жизнь?
Приведите реальный пример обновления СУБД
конечно, это самый жесткий аргумент :) зачем нам теория если она не подтверждается реальностью сиюминутно и постоянно. https://bugs.mysql.com/bug.php?id=199
бесплатного в этом мире ничего нет ;) но, вот как раз опыт и показывает, что использование автоинкремента из БД есть зло и усложнение жизни. Вы же сами приводите отличный пример, когда ваш бизнес зависит от реализации БД.
А вот насчет Эванса — согласен. Вот только там ничего насчет БД нет. "Технические средства" могут быть генератор GUID для предотвращения коллизий, например. А домен по умолчанию не БД. Ну, если вы, конечно не создаете новый phpMyAdmin
ну а чем технически некий "генератор GUID" (насколько я понимаю, их GUIDы не с неба ангелы передают) и некий "генератор автоинкремента" отличаются? оба зависят от реализации, оба имеют свои странности, оба могут быть забагованы
насчет реализации и багов — не спорю, согласен. попросили реальный пример — получите (см ссылку).
а вот отличаются они тем, зачем и для кого они. автоинкремент — идентификатор строки в таблице. а GUID как идентификатор "вообще". то есть, одно есть решение проблемы БД на уровне БД. догадайтесь с полпинка, с какой проблемой сталкиваются в первую очередь при переезде с MySQL на PostgreSQL?
То есть отличаются они как раз тем на чем держится вся идея DDD. Контекстом и целесообразностью — то бишь доменом.
все, безусловно, в праве делать что и как они хотят, но тогда не удивляйтесь, если дойдете до ситуации: заказ есть, но его нет, потому что мы не сохранили в БД то, что было или вообще то не было.
соглашусь со следующей реализацией, хотя и полный бред:
для инициализации сущности (Entity) Вы делаете запрос в БД на доступный автоинкремент. Получаете сий идентификатор. И только после этого "сохраняете" Entity.
Если же у вас есть момент, когда Entity "ждет" (те он уже есть как некий объект) свой идентификатор — ищите прокол в Вашей архитектуре. Такая ситуация по DDD не имеет места быть
Автоинкремент это способ генерации уникального числового id. Integer такой же идентификатор «вообще», как и GUID.
Если Вы по поводу приведённого мною примера, то должен вас огорчить. Да, частично вы правы: пока сущности существуют ничего страшного не произойдет. Но
- Проблема была в завязке идентификации сущностей посредством автоинкремента из БД. На что я сказал, что при обновлении БД-движка может измениться подход самого движка к автоинкременту.
- По данной ссылке как раз видно, что я имел в виду. С данным "багом" БД использовала id заново. Надеюсь, Вам не надо разъяснять в чём тут проблема с точки зрения DDD?
- ID совершенно не то же самое что есть GUID. Дело в том, в каком контексте эти идентификаторы являются таковыми. Исходя из приведенного мною выше примера, должно стать понятным, что ID используется БД и только ей. Разрабов БД ну ни в коем случае не интересуют проблемы использования их технических решений не по назначению. Надеюсь, что и это не надо разъяснять
2. У генератора случайных GUID при неправильной реализации может быть такой же баг, если seed при запуске устанавливается в одно и то же число. И нет, у сущностей, хранящихся в этой таблице, не было одних и тех же id.
3. Автоинкрементный целочисленный первичный ключ используется БД точно так же, как и любой другой неинкрементный. Счетчик инкремента это отдельная сущность, в некоторых СУБД она отдельно от таблицы (SEQUENCE), а в MySQL связана с ней и снаружи недоступна.
К 1. Нет. В этом "баге" речь о том, что после затирания записи из таблицы значение id используется БД-движком при сохранении новой строки (reuse). А это есть совсем другой подход к автоинкременту как идентификатору. Что на уровне БД вполне допустимо.
К 2. Тут речь не о том, что баги возможны, а о том что вообще в каком контексте будет багом и какие последствия от них будут. См 1
К 3. И опять, речь абсолютно о другом.
2. Так точно тот же контекст и те же баги. В одном случае вы запрашиваете генератор, он возвращает 128 бит, в другом вы тоже запрашиваете генератор, он возвращает 32 бита.
3. Так нет принципиальной разницы. И то и другое это первичный ключ, и то и другое надо генерировать. Либо приведите конкретные примеры, показывающие разницу.
Битые ссылки это настолько же неправильно, как и использование указателей после освобождения памяти. Никого же не удивляет, что эту память могут отдать другим данным при следующем вызове new.
Битые ссылки вполне могут быть частью бизнес-модели. Например, требование безопасности или потребление вычресурсов — в базе не должно быть данных старше года, обращаться за ними нужно в "бумажный" архив или к сисадминам, чтобы из бэкапа достали. Но ссылка, тип и идентификатор сущности, нужны, чтобы они могли точечно достать, а не искать по косвенно идентифицирующим атрибутам типа договор такого клиента за такое-то число. В рамках такой бизнес-модели битая ссылка не считается ошибочной, а вот чтобы переиспользование идентификаторов в одном и том же контексте не считалось ошибкой мне сложно представить сходу.
Сущность не исчезает, а перемещается, поэтому нельзя сказать, что сущности нет. Если она перемещается за пределы БД, то нужно хранить хотя бы значение автоинкремента последней внешней записи.
Об этом же случае пишут в комментариях к багу. Там тоже данные перемещаются в архивную таблицу. То есть проблема не в самом сбросе счетчика, а в том, что он должен быть общим для двух хранилищ. Просто в MySQL такого нет. Допустим, в одной таблице есть записи 1,2, а запись 3 перемещена в архивную таблицу. Счетчик в первой вполне может сбрасываться на 4.
В рамках контекста сущности нет, она уничтожается, другие сервисы могут хранит значение идентификатора для своих целей, но в рамках данного контекста сущности больше нет, её у него забрали, он больше за неё не отвечает.
Проблема именно в сбросе счётчика. Он отдаёт одинаковые значения при повторном обращении.
Ну так пусть отдает. Главное чтобы прежние значения нигде не использовались. Какие проблемы будут, если мы, скажем, откатом транзакций увеличим счетчик до 10, а потом сбросим на 4? Но я согласен, что сохранять счетчик решение более простое и логичное.
Контексту ничего неизвестно о какой-то общей модели, но если в требованиях говорится, что идентификатор сущности должен быть уникальным в рамках контекста и этот идентификатор входит в публичный интерфейс, то он не должен повторяться, если явно не сказано обратного. Если сказано, что он должен монотонно увеличиваться на 1, начиная с 1, то тоже недопустимо создание двух сущностей с одним идентификатором, даже если одновременно они не существуют.
Вы о чем? В этой статье речь о CQRS и DDD. Судя по Вашим комментариям, Вы ну ни разу не поняли, что такое Entity.
Хозяин, конечно, барин, но с таким подходом могу Вам и Вашим проектам только принести свои искренние соболезнования и пожелать граблей с черенком помягче. Потому как наступать Вы на них будете не раз.
Я про ваш пример с багом. Если он не относится к статье, зачем вы его привели? Если у вас была причина его привести, почему я не могу его обсуждать?
Судя по Вашим комментариям, Вы ну ни разу не поняли, что такое Entity.
Вас конечно же не затруднит привести конкретные цитаты с указанием противоречий?
Я говорю про Entity в логическом смысле, как про объект, имеющий первичный ключ и состояние. И о том, что способ генерации первичного ключа никак на нее не влияет.
Доказательств, почему "ID совершенно не то же самое что есть GUID", я так понимаю не будет.
ладно, еще раз пережуём
В статье шла речь о том, что, используя БД-генеририванный ID, сталкиваются с трудностями (ID генерируется только после сохранения записи в БД). На что я сказал, что, придерживаясь принципов DDD (persistence ignorance) такие проблемы не возникают. К тому же БД-идентификаторы не предусмотрены для использования вне контекста самой БД. Автор не согласился и запросил пример, когда обновление БД-движка может отразиться на бизнес-логике системы. Мой пример "бага" с повторным использованием ID как раз и показывает такой фэйл.
Вас конечно же не затруднит привести конкретные цитаты с указанием противоречий?
Если Вы про Ваши высказывания.
Ну и пусть используется. Той сущности уже нет, и того id уже нет
Повторное использование ID подразумевает, что у вас может появиться Entity с тем же самым идентификатором. А этого по определению не может быть.
Так как не похоже, что Вы понимаете о каком контексте идет речь в DDD, попробую объяснить на примере.
Родился Вася. При рождении он был зарегистрирован в системе в которой использовались ID генерированные БД. Васю зарегистрировали под номером 5. Деревня была маленькая. Вася пожил какое-то время и умер. Чтобы не заморачиваться его учетную запись стерли. С глаз долой, из сердца вон.
Через некоторое время родилась Маша. Её так же зарегистрировали. Угадайте какой номер получила она. Да — 5.
Маша подросла и пошла получать паспорт. Но выдали ей только похоронку.
Да, я понимаю, что Вы сейчас найдете достаточно технических решений, чтобы предотвратить подобную ситуацию, но суть не в том, чтобы собирать костыли.
Разница между ID и GUID видна хотя бы уже в самом "наименовании". GUID — globaly.
Ещё раз, используя ID из БД Вы работаете против принципов DDD в любом случае.
ладно, допустим DDD не про все эти контексты, единый язык и тд, а про технические вопросы…
ну так у вас persistence ignorance и при этом вы рассказываете истории про хранение данных о маше и васе в реляционной бд… одна история интереснее другой.
это получается на каком-нибудь редисе и при отсутствии в языке/окружении генератора уникальных айди у вас "DDD" не взлетит?
К тому же БД-идентификаторы не предусмотрены для использования вне контекста самой БД.
Вот это утверждение и вызывает вопросы. Почему вы так решили? Int такой же первичный ключ, как и GUID.
Мой пример "бага" с повторным использованием ID как раз и показывает такой фэйл.
Не показывает. После этого обновления в существующей бизнес-логике не поменяется ничего.
Повторное использование ID подразумевает, что у вас может появиться Entity с тем же самым идентификатором. А этого по определению не может быть.
Так а как определить, что это тот же самый идентификатор, если сущность исчезла и идентификатор исчез и следов уже нигде нет. Вас же не смущает, что оператор new может вернуть адрес, который ранее использовался для ссылки на объект, но был освобожден.
Маша подросла и пошла получать паспорт. Но выдали ей только похоронку.
С чего это, если о Васе никаких записей не осталось?
Да, я понимаю, что Вы сейчас найдете достаточно технических решений, чтобы предотвратить подобную ситуацию
Вы понимаете неправильно. Ее не надо предотвращать, она просто не случится в правильно построенной системе без битых ссылок. И неважно, будет в ней автоинкремент или нет.
В баге с MySQL проблема именно в битых ссылках. Ее можно решить маленькой вероятностью повторения ссылки, общим инкрементом на обе таблицы, отключением сброса в виде сохранения значения инкремента, или как-то еще. Общий/сохраняемый инкремент, кстати, дает 100% вероятность неповторения, в отличие от GUID.
Разница между ID и GUID видна хотя бы уже в самом "наименовании". GUID — globaly.
И все, слово в названии это единственный ваш аргумент? Я возьму и скопирую себе в БД какой-нибудь GUID. Все, он уже не глобальный. Или автоинкремент 128-битный сделаю. Все, он сможет адресовать больше объектов, чем GUID, в котором пропуски.
А повторение высказывания "еще раз" аргументом не является.
ещё раз — удачи
Ее не надо предотвращать, она просто не случится в правильно построенной системе без битых ссылок.
Кроме системы есть внешний мир, о котором система ничего не знает. И там вполне могут оставаться битые ссылки. И при обращении к системе за ними она должна отдавать ошибку типа NotFound минимум, а не отдавать другую сущность.
Если данные ушли во внешний мир, то мы не можем удалять этот идентификатор из системы. Тогда и битых ссылок не будет. А если удаляем, то считаем, что все внешние ссылки стали неправильные.
Ещё раз, используя ID из БД Вы работаете против принципов DDD в любом случае.
В какой-то мере это справедливо только в случае, если мы не можем получить значение счётчика перед или при создании сущности. По сути это справедливо именно для автоинкремент полей в СУБД, в которых единственный способ получить значение идентификатора для невставленной записи без конфликтов, это вставить её. Хотя можно и обойти костылями типа предварительной записи фейковой сущности, либо созданием псевдопоследовательности на базе таблицы с единственным автоинкрментным полем. Но для приложения это не будет нарушением persistence ignorance, просто у нас будет сервис или метод в сервисе генерации нового идентификатора для новой сущности. Будет внутри него база, генератор UUID, генератор рандомных чисел, функция хэширования или иного вычисления, или даже пользовательский ввод — деталь реализации, скрытая за абстракцией.
о, а это, кажется новая и интересная мысль! хотя…
https://habrahabr.ru/post/347908/#comment_10645562
соглашусь со следующей реализацией, хотя и полный бред
Если же у вас есть момент, когда Entity "ждет" (те он уже есть как некий объект) свой идентификатор — ищите прокол в Вашей архитектуре. Такая ситуация по DDD не имеет места быть
Считаю, что Entity ждать может, например в конструкторе, не могут ждать клиенты Entity, уже получив ссылку на неё.
согласен безоговорочно.
но вот только что описанная Вами ситуация не совсем подходит как пример. в Вашем примере нет момента "ожидания". Entity "начинает существовать" с того момента как получает ИД. То есть вполне возможно породить Entity без содержимого, но не без ID.
В ситуации жесткой подвязки к БД "пустой" Entity создать получится только в том случае, если БД позволяет сохранять пустые записи.
Можно разделять собственно создание объекта, как элемента ЯП, и начало жизни сущности в этом объекте. Грубо, после entityDraft = new Entity(data)
, это просто технический объект, у которого есть какие-то "неопределенные" данные и поведение, не имеющие значения в предметной области, а вот после entityDraft.save(); entity = entityDraft
, где в числе прочего присваивается и ид, это уже объект, представляющий сущность бизнес-модели. Да, несколько запачкивает код, да, может понадобиться разделение одной бизнес-транзакции на несколько транзакций БД, но принципов DDD такая точка зрения не нарушает.
С точки зрения DDD — Entity или есть или его нет. Целиком и полностью. Никаких танцев с бубном не подразумевается.
Грубо говоря Ваше "тех-решение" — надругательство над DDD схожее с изнасилованием.
а вот после entityDraft.save(); entity = entityDraft, где в числе прочего присваивается и ид, это уже объект, представляющий сущность бизнес-модели
В DDD оно не то что не нужно, ему просто нельзя быть. Все остальное — битая архитектура.
Если Вам в проекте следующим принципам DDD нужны подобные костыли, то Ваш велосипед не рабочий.
Весь прикол DDD в том, что техническая сторона реализации сводится к перечню используемых или наиболее подходящих шаблонов. И вот от этого и надо танцевать. Бубен тут совершенно не нужен, если понять что к чему.
надругательство над DDD схожее с изнасилованием.
но DDD плевать насколько у вас модель изолирована, чиста и все такое. Оно как бы подразумевается что нам по хорошему нужен persistence ignorance, или cqrs, или еще чего. Это все не важно и не требуется что бы говорить что "я юзаю DDD". Достаточно просто что бы драйвером для дизайн решений была предметная область. То есть это и выделение контекстов и все что является сложным.
Весь прикол DDD в том, что техническая сторона реализации сводится к перечню используемых или наиболее подходящих шаблонов.
Не стоит так все упрощать.
С точки зрения DDD — Entity или есть или его нет. Целиком и полностью.
Ну так она или есть, или нет. Просто момент "есть" начинается не с new Entity(), а с entity.save(), которую вполне можно обернуть в, например, фабричный метод, чтобы не усложнять создание сущности клиенту.
В DDD оно не то что не нужно, ему просто нельзя быть.
DDD не регламентирует технические способы создания сущностей.
Весь прикол DDD в том, что техническая сторона реализации сводится к перечню используемых или наиболее подходящих шаблонов.
Фабричный метод, отдельный класс фабрики или какой-нибудь билдер для создания сущностей вполне ложится на DDD, а будет там вызываться генератор UUID, "чистая" последовательность PostgreSQL или автоинкремент MySQL, путём вставки записи — деталь реализации.
Вообще DDD не требует применения ООП в общем случае, пускай и декларируется, что ООП лучше из других популярных парадигм подходит для реализации модели.
ООП лучше из других популярных парадигм подходит для реализации модели.
я не видел подобных утверждений. Мне кажется тут больше все в типы упирается.
"Парадигмы моделирования и средства программирования" в главе 3 части 1 "DDD" Эванса (Вильямс, 2011).
Можно трактовать и по другому, но для себя сделал вывод, что автор DDD рекомендует в основном ООП использовать. Ну или Пролог :)
F# мультипарадигменный язык, с объектами вполне нормально работает. Думаю, что хорошо реализованная функциональная составляющая позволит не использовать объекты в некоторых случаях, в которых на языках типа Java или C# никуда.
То, что в C#/Java прежде всего :)
То, что в C#/Java прежде всего :)
То есть то, что в C++. Но это уже дискуссия не на ту тему. Это все не очень относится к DDD.
Не очень, да. В целом DDD мало зависит от языка, пока язык позволяет реализовывать элементы DDD достаточно выразительно, без ситуации "за деревьями не видно леса".
ООП, как я понимаю, выбрана как дефолтная парадигма с одной стороны, благодаря хорошей связи многих концепций ООП и DDD (может и не случайной :) ), а, с другой, просто по практическим соображениям, типа много программистов, знающих ООП и ту же диаграмму классов худо-бедно могут понять и не программисты.
Можно разделять собственно создание объекта, как элемента ЯП, и начало жизни сущности в этом объекте.
Но зачем?
а вот после entityDraft.save()
то есть мы все же не делаем persistence ignorance? Опять же, могу представить такое только в силу ограничения выбранных инструментов, например всякие эктив рекорды и прочее.
Но в целом я согласен с тем что все это к DDD никакого отношения не имеет и никак не конфликтует.
Но зачем?
А он и так обычно разделён. По крайней мере в языках, где я глубоко вникал в основные механизмы создание объектов. Конструктор уже получает готовый указатель this/self/..., объект уже существует в терминах ЯП, просто на него ещё нет внешних ссылок, а его свойства в лучшем случае проинициализированы статическими дефолтными значениями. А в том же JS конструктор без вяских выкрутасов может вернуть совсем другой объект, не тот что пришёл к нему в this.
то есть мы все же не делаем persistence ignorance?
На практике save()
будет спрятана за каким-то интерфейсом типа EntityId::createIn(Entity entity)
, а он, возможно, за фабрикой типа Entity::create()
с кодом типа entity = new self; EntityId::createIn(entity); return entity;
Да, основное назначение, с одной стороны преодолеть ограничения выбранных или потенциальных инструментов, а, с другой, всё же изолироваться от них.
С guid'ами проще, например, мержить две таблицы в одну либо данные с двух разных бд. Был реальный кейс объединения двух клиентских баз
У меня есть мысль, что id это адрес в логической оперативной памяти. Соответственно, создание идентификатора это аналог операции new. Можно использовать большое пространство и выбирать случайную ячейку (GUID), можно выделять память последовательно (autoincrement).
Где-то в UserRepository делаем метод UserId generateNextId() и уже в реализации решаем что конкретно будем использовать, guid, serial, autoincrement и т. д. Можно cделать сигнатуру конструктора User типа (?UserId id, ?UserRepository repo) и в нём код типа this.id = id ?: repo. generateNextId()
Я надеюсь, что вы это не серьёзно имеете в виду. У вас Entity зависит от Repository — не по фэншую.
очень печально. а по вашему конструктор составная часть чего? то, что Вы не "храните" параметр конструктора в сущности еще не отменяет её зависимость от таковых. посмотрите ещё раз, что подразумевается под зависимостями.
Какой смысл делать это в конструкторе сущности? Это ж усложняет и тесты и вообще… Да и вы же сами прописали на уровне интерфейса что вы уже передаете готовый ID. это может быть как UUID так и следующий элемент секвенса и это уже можно спрятать в фабрику.
UserId передается сверху, потому сущность от инфраструктуры не зависит. Как был получен этот UserId — дело третье.
Интересует ваше мнение на следующий вопрос. В Clean architecture Роберта Мартин одним из основных элементов является класс Interactor, идею которого он позаимствовал у Якобсона. Согласно Мартину Interactor — это класс, содержащий верхнеуровневую логику приложения и реализующий конкретный пользовательский сценарий.
На первый взгляд от хэндлеров мы можем получить те же преимущества, что и от interactor: разделение на vertical slices и приблизиться к «кричащей архитектуре», когда просматривая команды и хэндлеры будет явно понятен способ взаимодействия с приложением.
Есть ли на ваш взгляд принципиальная разница между Interactor и Handler? И как мы должны реализовать Handler, чтобы получить все преимущества идеи, заключенной в Interactor?
A core element of CQRS is rethinking the design of the user interface to enable us to capture our users’ intent such that making a customer preferred is a different unit of work for the user than indicating that the customer has moved or that they’ve gotten married.
Т.е. команды должны быть, что называется, fine-grained чтобы обеспечить масштабируемость и не потерять из-за ошибок параллелизма.
UseCase Якобсона как прародитель Interactor'a Мартина описывает как актор(человек или система) достигает цели с использованием нашего ПО. А это может включать в себя выполнение нескольких операций, что как раз может вызвать ошибки при паралелльном выполнении.
CQRS. Факты и заблуждения