Pull to refresh
356
1.1
Alex Efros @powerman

Systems Architect, Team Lead, Lead Go Developer

Send message

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

такую валидации можно провести а слое презентации ещё до трансформации входных данных в типы и передачи их в слой домена

Можно, но во-первых это не область ответственности слоя адаптеров/презентации, и во-вторых это не избавляет от необходимости выполнять эти же проверки ещё и в слое домена - ведь иначе модель не сможет гарантировать свои инварианты. А это уже дублирование кода и его нечаянные различия в будущем - слишком высокая цена за эту "фичу".

На самом деле слой адаптеров тоже валидирует. Просто он делает совершенно другие валидации: он проверяет что входящий DTO соответствует требованиям API (форматы значений, обязательные поля, etc.). Если по формату API полученный DTO корректный, то дальше данные передаются в модель на валидацию уже по бизнес-правилам. А чуть позже, при транзакции в БД, данные будут валидированы ещё раз, на соответствено констрейнам таблицы в базе. Все эти три слоя работают с разными данными и делают разные валидации. Очевидно, что UI в любом случае будет получать ошибки от всех трёх слоёв. Просто ошибка полученная от слоя презентации говорит о баге в самом UI-клиенте, ошибка от БД говорит о баге на бэке, а ошибки от слоя домена UI должен уметь удобно показывать юзеру.

валидируем входное DTO и привязываем ошибки к этим полям

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

Формат входного DTO обычно фиксирован и устарел (потому что это API и там важно сохранять совместимость), а модель могла сильно измениться со временем. Иногда меняются только названия и форматы полей но они всё ещё один-к-одному, но могут быть и другие варианты. Самый простой пример: на вход пришли два поля: FirstName и LastName, а при передаче в модель они были переданы как одно поле FullName (слой адаптера объединил их в одну строку через пробел). А бывает и наоборот, в DTO через API пришло одно поле FullName а при конвертации в модель адаптер это значение как-то разделил на FirstName и LastName.

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

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

Кроме того, помимо привязки ошибок к полям UI нередко должен и другие задачи решать - например выводить ошибки на языке юзера. Поддержке мультиязычности совершенно точно не место на бэке (причем во всех сервисах, потому что ошибки возвращают все).

Поэтому бэк может вернуть список ошибок, он должен вернуть код ошибки, он может приложить к каждой ошибке структуру с подробностями специфичными для конкретного кода ошибки - в общем предоставить UI всё необходимое, чтобы UI смог распознать ошибку, перевести на нужный язык подставив в шаблон детали, и вывести её наилучшим для UX способом. Но бэк не должен решать, что данную ошибку надо выводить рядом с каким-то полем в UI (хотя бы потому, что в UI поля FullName тоже может не быть, потому что UI запрашивает у юзера FirstName, MiddleName и LastName а на сервер передаёт их в одном поле DTO FullName).

С названием полей всё сложно. Когда на бэке чистая архитектура присланное UI поле может довольно сложным и неочевидным образом мапиться на модели домена. Соответственно, ошибка валидации обнаруженная в слоях приложения/домена будет в любом случае не про то поле, которое UI прислал в API, а про поля модели. При этом заниматься творческим переписыванием текста ошибок домена в слое адаптеров - очевидно дурная затея, поэтому обычно никто даже не пытается это делать. Так что, в конечном итоге, обычно именно UI приходится самому мапить ошибки бэка на поля ввода либо просто их выводить, чтобы юзер сам догадался какое поле(я) нужно исправить.

И с моей точки зрения, такому клиенту будет гораздо удобнее получать исключения разных типов, которые являются частью публичного интерфейса агрегата, чем один ValidationException

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

Поэтому группировку ошибок я бы использовал только в ситуации если есть очень жесткое требование со стороны UI и его никак нельзя обойти

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

Это слой который отвечает например за преобразование http запроса в CQRS команду или запрос, передачу его в слой application и преобразование ответа из слоя application в http ответ.

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

бросаем первое исключение, а внутрь него кладем коллекцию последующих

Я бы скорее использовал общее для всех ошибок валидации исключение (напр. как предложил выше @michael_v89"ValidationException с полем errors"), и использовал ровно ту же библиотеку для валидаций при необходимости проверки на наличие конкретной ошибки внутри поля errors. Но, в идеале, слой адаптеров не должен рыться этих ошибках, он должен тупо вернуть общий код ошибки в API для любых ошибок валидации (т.е. ему достаточно проверить что это именно ValidationException) и все ошибки как есть, может переложив первую или последнюю в отдельное поле (для простых клиентов).

Хотя нет, все равно клиенту придется итерировать по коллекции.

А кто тут является клиентом?

Если это клиент дёрнувший API, то он ошибки получает ответом из API как обычное значение, а не список исключений.

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

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

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

Другое дело, что хотелось бы избежать внесения изменений в код домена из-за изменений требований к UI. И как раз эта проблема решается одновременным возвратом всех ошибок валидации из домена (и на уровне сетевого API), т.к. позволяет дальше любые изменения требований к UI удовлетворять непосредственно в логике UI. Это требование не обязательно должно быть жёстким: если отдельные ошибки в слое домена сложно проверить и вернуть одновременно со всеми остальными то можно для них сделать исключение и возвращать их отдельно (юзабилити от этого если и пострадает то обычно незначительно, и в контексте DDD нам ясность доменной логики однозначно важнее юзабилити).

я имел ввиду слой UI бэкенд приложения

О чём речь? Можно пример?

И код этого клиента будет намного проще если агрегат будет возвращать одну ошибку, а не целый набор.

Для этого есть стандартный подход: в ответе API в поле "error" возвращается одна ошибка (обычно первая или последняя) для простых клиентов, а в поле "details" дополнительно прилагается полный список ошибок для более продвинутых клиентов.

Если мы будем делать проверки бизнес правил последовательно, то код в слое домена будет проще, но ошибку он будет возвращать одну.

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

Кроме этого, мне кажется, что то каким образом ошибки будут отображаться это скорее требование слоя UI.

Всё верно. Но UI нужно иметь, что отображать. Если UI имеет только первую ошибку, то отобразить все UI не сможет. А вот если UI получит все, то отобразить можно будет что и как угодно - одну, все, первые 3, сразу, по одной, etc. Так что если есть возможность вернуть с бэка сразу все - лучше так и сделать.

проверки легко можно продублировать в слое UI

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

мне кажется что лучше сделать в агрегате публичный метод который проверяет бизнес правило и возвращает boolean, который может дёрнуть UI, чем усложнять логику агрегата группировкой ошибок.

Прямо скажем, не самая лучшая идея:

  1. Это будет медленно в UI (надо дёрнуть кучу API перед отправкой основного запроса).

  2. Это будет громоздко на уровне API. Валидаций много - это будет раздувать API в целом в разы (условно, имея 5-10 валидаций на агрегат мы вместо в среднем 5 методов API на агрегат получим 10-15). Валидации могут часто меняться - это будет требовать много (причём вполне возможно несовместимых) изменений API.

  3. Полный список валидаций в UI и на бэке может различаться. Т.е. бэку всё-равно может потребоваться возвращать полный список ошибок, иначе UI всё ещё (после всех дополнительных проверок сделанных UI) может получить только одну ошибку из нескольких.

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

  4. Это будет тормозить бэк. Некоторые проверки могут потребовать получения самого агрегата и/или дополнительных сущностей из репозитория, в результате чего количество запросов в репозиторий удвоится.

  5. Это будет громоздко в слое домена. Будет толпа отдельных методов модели для отдельных проверок плюс основной метод изменяющий модель который должен будет то ли дёргать эти методы, то ли дублировать их, причём без гарантии что он дёрнет их все (часть вполне может остаться забытой после изменений либо остаться осознанно для совместимости API).

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

В бизнес-требованиях никаких событий нет, там четко сказано "сохранить это, отправить это".

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

Ну то есть нужные бизнес-требования реализовать нельзя.

Как раз бизнес-требования - можно. А вот искусственно добавленные разработчиками требования немедленной консистентности (в которой бизнес не нуждается и которые не требовал) - нет.

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

Ну, если речь конкретно про мьютексы, то приведите пример, когда в этом появляется необходимость. Мне сходу ничего в голову не приходит.

Что до сложностей, то чем искать способы реализовать что-то сложное лучше искать способы получить нужный результат избежав реализации чего-то сложного. Хороший пример в контексте DDD - идея дробления проекта на несколько Bounded Context, что позволяет сильно снизить сложность реализации каждого из них.

При отправке на ревью мьютекс это не замена транзакции, он сохраняется в течение 2 транзакций и сетевого вызова.

Хм. А что случится, если сервис в середине этого всего упадёт?

Иначе фоновая задача может что-то поменять между ними, и потом придется долго разбираться и корректировать данные вручную.

Может, конечно. Это должно быть штатной ситуацией, целостность при которой должна обеспечиваться как раз на уровне транзакций в БД. Если без мьютекса у нас нет целостности, то это значит только то, что при креше сервиса мы целостность окончательно потеряем, а такое приемлемо только в студенческих проектах, но не в реальном продакшне.

Зачем нам логика админки в коде пользовательской части?

Затем, что это - не логика админки! Это - просто штатная операция над товаром, которая требуется бизнес-логике. Кто имеет право её выполнять - в абсолютном большинстве случаев не важно для слоя домена.

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

Тут я не понял, как это должно выглядеть, и как возвращать список ошибок на фронтенд.

Примерно так:

  • Метод use case в слое приложения:

    • открыть транзакцию

    • вызвать метод доменного сервиса

    • закрыть транзакцию

  • Метод доменного сервиса в слое домена:

    • загрузить из репо агрегат

    • загрузить из репо вспомогательные сущности

    • вызвать метод агрегата, передав ему вспомогательные сущности параметрами

    • сохранить агрегат

  • Метод агрегата:

    • выполнить валидации, используя в т.ч. переданные параметрами дополнительные сущности

    • если валидации провалились, то вернуть группу соответствующих доменных ошибок

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

То есть пробрасываем техническую зависимость в сущность через аргументы метода? Она становится подозрительно похожа на сервис.

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

Это бизнес-требования к работе с товарами, значит это часть домена.

Это не так.

Если бизнес требует, чтобы конкретный статус товара выводился "красненьким", из этого не следует, что данному функционалу место в инвариантах модели в слое домена, а не в UI.

Для модели url картинки, сама картинка в виде набора байт, и путь к файлу с картинкой на файл-сервере - вполне могут быть идентичными способами представления картинки, и какой из них использовать в модели мы можем выбрать сами, а для бизнеса все эти 3 варианта - это просто "картинка".

Всё, что относится к особенностям представления данных обычно должно быть в UI, а не в слое домена. Если, конечно, представление данных не является основной задачей нашего приложения (ну т.е. мы не реализуем браузер или просмотрщик pdf).

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

Почему нельзя их открывать вручную когда нужно?

Потому что практика за последние 50 лет показала, что с такого рода задачами (аккуратное открытие/закрытие транзакций, выделение/освобождение памяти, блокировка/разблокировка мьютексов, etc.) люди справляются крайне плохо (лезет куча багов). Поэтому нужен какой-то универсальный/механический подход/паттерн, при котором не возникает необходимости думать и решать в каждом частном случае где нужно открывать/закрывать транзакцию и т.п. Так что на данный момент сформировалось три рабочих варианта:

  • транзакция тупо вокруг каждого use case целиком (обычно в DDD)

  • транзакция тупо вокруг каждого метода глобального репозитория (обычно в Transaction Script)

  • транзакция исключительно вокруг метода сохранения изменений накопленных в UnitOfWork (обычно в DDD)

Допустимые принятия решений человеком в первых двух случаях ограничены тем, чтобы часть транзакций помечать как read-only.

Там нет моего приложения. Мне интересно именно сравнить реализации для одинаковых бизнес-требований.

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

  • Обеспечение логики с 2 транзакциями "сохранить - отправить сетевой запрос - сохранить".

Первый use case выполняет только первую транзакцию плюс отправляет доменное событие. Обработчик этого события (второй use case) выполняет отправку сетевого запроса. Вторая транзакция выполняется либо в рамках второго use case, либо второй тоже отправляет событие и вторая транзакция выполняется в обработчике этого события (третьем use case).

  • Работа с 2 сущностями-агрегатами в одном действии, например установка значений и сохранение Product и Review в одной транзакции при отправке на ревью.

Обычно бизнес допускает в таких ситуациях eventual consistency. В этом случае первый use case добавляет/изменяет агрегат Review плюс отправляет доменное событие, а второй use case (обработчик этого события) отражает результат добавления/изменения ревью на агрегате Product.

  • Работа с мьютексами.

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

  • Разделение логики на группы, например чтобы логику для админки нельзя было вызвать из пользовательской части.

Это вопрос авторизации, и он не очень простой. Дело в том, что DDD очень не нравится идея замусоривать методы моделей логикой авторизации (потому что она нужна буквально во всех и это затрудняет понимание их основной бизнес-логики). Так что обычно (если возможно) авторизация делается где-то на предыдущих этапах: сетевой файрвол, edge proxy, слой приложения (use case). Сами модели остаются не защищены, так что мешать таким вызовам именно в слое домена - некому.

  • Валидация входных данных с загрузкой связанной информации - на входе name, description, category_id, надо подгрузить категорию и проверить статус, если она неактивна вернуть ошибку вместе с ошибками для других полей.

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

  • Валидация данных сущности, которая требует зависимости, например валидатор штрихкодов.

Ровно так же, как в предыдущем пункте, только вместо реализации репозитория (из слоя инфраструктуры, доступную в слое домена через инверсию зависимостей) используется такая же реализация клиента к внешнему API/библиотеке.

  • Использование зависимостей в логике - при выполнении действия отформатировать дату специальным форматтером, конвертировать markdown в html, при загрузке данных товара из CSV скачать изображение по URL и загрузить на файловый сервер с соответствующими записями в таблице file.

В общем случае всё это - не задачи для слоя домена. Бывают исключения, конечно (когда форматирование markdown в html это основной/ключевой функционал нашего проекта), но в большинстве проектов это задачи для сервисов в слое инфраструктуры (доступных в домене через интерфейсы/инверсию зависимостей) или даже вообще UI.

Лично мне нужен конкретный пример законченного приложения.

Поиск по гитхабу "ddd example" не помог?

В учебниках почему-то не показывают решение тех проблем, которые меня интересуют.

Если сможете озвучить кратко эти проблемы - возможно я смогу ответить.

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

Да, Ваша точка зрения вполне ясна. Но нет, не будет работать. Убедить я Вас в этом, очевидно, не смогу, так что остановимся на том, что наши мнения по этому вопросу расходятся.

Но зачем так делать, если можно не делать.

Из соображений производительности и консистентности. Иначе придётся либо открывать долгие транзакции "на весь use case" либо самостоятельно реализовывать механизм изоляции вместо встроенного в РСУБД (например, как делает UoW на ручном версионировании строк и оптимистических блокировках).

"У вас не покрыты вот эти методы, значит вы писали их без тестов. Сначала пишем тест вот так [пример кода], потом код метода вот так [пример кода]". Так же как в любом учебнике.

Угу. Только вот комментарий - не учебник. Учебников полно, но их в данном случае явно оказалось недостаточно, раз кто-то умудрился "работая по TDD" написать непокрытый тестами код. Так что тут не ещё один учебник нужен, а полноценное индивидуальное обучение.

С моей точки зрения логика в сервисах будет нормально работать с любыми требованиями, просто потому что

Будет. До какого-то предела. А потом случится то, что называется "проблема масштабирования". Big ball of mud тоже нормально пишется до какого-то предела, а потом почему-то ровно те же действия что и раньше по его развитию начинают требовать экспоненциально больше сил и времени. Но разработчик, у которого все Big ball of mud проекты укладывались в пару месяцев, вполне может искренне в это не верить, потому что не сталкивался лично.

Я говорю о том, что говорить "Если не работает, значит сделали неправильно, а как правильно не скажу" это неконструктивно.

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

Ну вот я и привел пример, как описывать бизнес-логику максимально близко к словам эксперта и не приносить в жертву все остальное.

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

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

Курсы повышения квалификации для английских дантистов. :) Там из-за неудачного стечения обстоятельств не было возможности применить стратегию DDD и в результате вся бизнес-логика проекта оказалась в одном микросервисе. А это и сами дантисты разных профессий/квалификаций, и их персональные планы развития, и требования гос.регулятора, и собственно обучающие материалы/курсы которые они могли проходить прямо у нас, и разные виды других обучающих активностей которые они могли делать не у нас но у нас учитывать, и поставщики обучающих курсов… посыпать всё это разными требованиями, которым надо соответствовать в определённые периоды времени (от гос.регулятора, из собственных планов пользователей, нашего проекта, …), добавить напоминалки/ачивки/… И это я ещё всех подробностей уже не помню, дело было 6 лет назад.

Маленькие изменения в требованиях приводят к маленьким изменениям в коде, большие к большим, это критерий правильной модели. С сервисами код не надо постоянно переписывать. Поддерживаемость в том числе и про это.

Всё так. Именно поэтому когда нет сложной бизнес-логики Transaction Script использовать и проще и дешевле. Просто Вы не сталкивались с ситуациями, когда из-за роста сложности бизнес-логики такой подход перестаёт работать. А я с таким столкнулся. Всего один раз, но мне хватило, чтобы понять, что да, такое бывает, и нет, своими обычными подходами я это не вытяну. Скорее всего пока Вы сами с такой ситуацией не столкнётесь Вы продолжите верить, что описанный в статье подход действительно лучше DDD справляется со сложной бизнес-логикой.

Ага, опять абстрактные пожелания, что код надо писать без ошибок)

Не-не-не. Это совсем другое. Это не про "делай хорошо - будет хорошо". Это про то, что мы гарантированно получаем только то, что у нас на первом месте в приоритетах.

Например, если в проекте практиковать TDD, то есть гарантия, что код будет практически полностью покрыт тестами. Качество тестов может быть паршивым, качество кода тоже, в нём могут быть упущенные тестами баги, он может делать вообще не то, что нужно бизнесу - но именно покрытие кода тестами будет гарантированно высоким.

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

Видите ли… Суть DDD как раз в том, чтобы никаких сложностей при реализации бизнес-логики не возникало в принципе. Вы должны писать в коде (точнее, в слое домена) практически буквально то, что произносит эксперт в предметной области. И если по мере развития проекта с этим начинаются сложности (код начинает сопротивляться дословному выражению требований эксперта), то дизайн нужно переделывать пока сложности не уйдут.

Эванс про это говорит вполне однозначно: вы не сделаете "правильный" дизайн ни с первого раза ни с любого другого. По мере углубления понимания предметной области или изменения требований "правильными" моделями окажутся не такие, как сейчас. Вот, к примеру, прекрасный доклад, где Эванс рассказывает большую часть времени о том, какой моделью лучше описывать перевозку груза из точки A в точку B: последовательностью моделей Leg (рёбер соединяющих две точки) или Stop (последовательностью точек). И как изменение понимания предметной области качает весы от одного варианта к другому.

Конечно, такого рода рефакторинг - дело недешёвое. (И это только одна из причин, почему использовать тактику DDD в простых проектах нет смысла.) Особенно, если делать его нужно каждый раз, когда выражать бизнес-логику в коде становится даже не сложно, а так, немного сложновато. Но именно такие приёмы DDD и позволяют гарантировать, что бизнес-логику в коде выражать всегда будет просто, и в большинстве случаев это будет почти дословно тому, как описывает требования эксперт.

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

На простых небольших примерах DDD никогда не будет выглядеть лучше или проще более прямолинейных подходов (вроде Transaction Script). Наоборот. Именно поэтому никто (включая Эванса и других лидеров DDD) не предлагает применять тактические паттерны DDD в простых микросервисах, где нет реально сложной бизнес-логики.

Я лично за всю карьеру сталкивался с таким кейсом всего один раз. Во всех остальных случаях мне удавалось разделить сложную бизнес-логику проекта в целом на кучку относительно несложных Bounded Context (обычно по одному на микросервис), чтобы в каждом отдельно взятом микросервисе тактический DDD не требовался. (Правда, стоит отметить, что конкретно онлайн-магазины я не писал, а обычно именно эта сфера используется в примерах тактического DDD.)

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

Ну, на первый взгляд звучит как Transaction Script. Часть бизнес-логики при таком подходе может оказаться не только в этом классе (в слое приложения/use cases, если в терминах Clean Architecture), но и в слое инфраструктуры (в реализации репозитория - та часть, которую необходимо делать в рамках транзакции БД). Слой домена при таком подходе может вообще отсутствовать (то, что называется слоем домена в терминах Clean Architecture, в терминах DDD называется Shared Kernel и это не совсем "слой").

Я вижу, что во многих случаях его пытаются убрать и поместить в сущность вообще всё

Сложно угадать что именно Вы видели и почему оно было реализовано именно так.

Иногда всю бизнес-логику пытаются перенести из слоёв приложения и инфраструктуры в слой домена (и да, большую её часть именно в методы модели). По сути - перейти от Transaction Script к DDD. Делают это обычно тогда, когда бизнес-логика становится насколько сложной, что разработчики перестают справляться с поддержкой бизнес-логики в стиле Transaction Script (бизнес-логику становится сложно понимать, из-за этого возникает много багов, которые очень сложно фиксить не порождая новые баги). И в такой ситуации это вполне уместно.

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

Основная проблема DDD в том, что с ним довольно сложно разобраться, порог входа довольно высокий. Сделать это самостоятельно за несколько дней, по вечерам и выходным - практически невозможно. Лично у меня ушло примерно две недели, с утра до вечера, на чтение книг, статей и просмотр докладов, пока не случился "aha moment". И это только на тактическую часть DDD (потому что будучи архитектом с 30+ годами опыта стратегическую часть DDD я к этому моменту уже давно хорошо понимал и практиковал). Из-за этого порога входа большинство понимает и применяет тактику DDD некорректно (stackoverflow и блоги завалены вопросами по таким кейсам), получая в результате с таким "DDD" ещё больше проблем, чем без него.

Если применять тактику DDD корректно, то проблем именно с бизнес-логикой гарантированно не будет. На данный момент DDD - это единственный (известный мне) подход, который может справится с действительно сложной бизнес-логикой, когда другие подходы уже не работают. Из-за DDD могут быть другие проблемы - он полностью игнорирует инфраструктуру, и у этого есть свои последствия, ну и плюс то, что я писал про саги в своей статье. Но при реально сложной бизнес-логике эту цену приходится платить, потому что альтернатив пока нет.

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

Что до анемичных моделей и вынесения бизнес-логики отдельно от моделей - это гораздо более распространённый подход чем DDD, который обычно называют Transaction Script. Он прекрасно работает, пока в одном Bounded Context не оказывается реально много сложной бизнес-логики (что бывает довольно редко). К сожалению, Ваш стиль изложения сильно усложнил попытку понять, чем Ваш подход отличается (если отличается) от Transaction Script.

По ООП у класса должна быть одна причина для изменения

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

Так а если часть не сохранится, какая разница transaction script у нас или нет, неконситетность будет в базе же?

Не будет никакой неконсистентности.

Если брать простой (без сторонних вызовов API) кейс DDD "считать, изменить, сохранить", то в Transaction Script вместо всего этого будет один вызов репо.

Если брать комбинацию вызова API и модификации модели, то тут ни от DDD ни от Transaction Script ничего не зависит: для консистентности обычно необходимо вызов API делать либо до либо после транзакции но никогда в середине транзакции. Просто в DDD эти "до/после транзакции" зачастую приводят к лишним сложностям (разделение одного use case на два шага и добавление доменных событий для перехода между ними), а в Transaction Script это легко и явно пишется в соседних строках use case.

Если же у нас какая-то распределённая транзакция включающая вызов API (который необходимо отменить другим вызовом API если наша транзакция в БД провалится), то здесь тоже техники и DDD и Transaction Script будут одинаковые: ручная реализация WAL (write-ahead log) запланированных и выполненных действий через последовательность транзакций в нашей БД. Просто, опять же, в Transaction Script вся эта последовательность будет аккуратно записана в одном use case, а в DDD её придётся разделять на шаги и посыпать кучкой доменных событий.

1
23 ...

Information

Rating
1,235-th
Location
Харьков, Харьковская обл., Украина
Date of birth
Registered
Activity