Комментарии 73
Как человеку, который изучает Go, очень полезно на будущее, оставлю в избранное
А в чем проблема если в го у меня много файлов? Вы предлагаете решить ее запихав агрегаты, vo и сущности в один файл? Что будет там через год? Даже у вас в статье уже начинает пахнуть мясом.
Основная суть всего ддд это легко читаемый код, а особенно бизнес логика.
Основная суть DDD, это Домен драйвит > код. Прежде всего для дискавери, и чтобы починить "сломанный телефон" на этапе реализации, когда разработчик не понимает что хочет пользователь, а продакт не понимает, сколько стоит та или иная фича. Потому что коммуникация в больших проектах дороже чем имплементация
Когда Эванс писал про DDD, речь была про это, никак не про код. Это называется стратегический дизайн: границы контекстов, единый язык. То что приводит автор - тактический дизайн, агрегаты, VO, это придумал Вон Вернон, и разработчики дуреют с неё, и вообще не с неё надо начинать изучение
Go плох для DDD, даже не пытайтесь
Scala/F# > Rust > C#/Java > PHP 8+ > Go я бы так сказал
Что ему не хватает для нормальной работы с ддд:
Sum types (ADT) с exhaustive match - моделирование состояний и команд
Иммутабельность и неизменяемые поля - VO без дисциплины не сделать
Конструкторы-инварианты - zero value ломает защиту
Generics нормальные - есть, но слабые (нет ограничений на методы со своим типом, нет higher-kinded)
Обобщённый Result/Option и монадическая композиция - if err != nil вместо цепочек
Настоящие enum вместо const iota
Перегрузка операторов или хотя бы трейты/интерфейсы с операторами - money.Add вместо +
Pattern matching с деструктуризацией
Non-nullable типы по умолчанию - любые ссылки и интерфейсы могут быть nil
Readonly коллекции - срезы и мапы всегда мутабельны
Private поля с фабрикой в том же пакете - есть, но нет per-type инкапсуляции внутри пакета
C# самый DDDшный язык, там очень много средств, которые будто специально сделаны для чистой архитектуры и развитого ООП-моделирования (тот же уровень доступа internalили всякие ковариантные/контрвариантные дженерики).
Что касается Go, то определённая дисциплина от разработчика будет нужна, конечно. Но не сказал бы, что прям принципиально невозможно, и, если берёшь Go, нужна обязательно анемичная процедурщина с высоким зацеплением.
Справедливо для классического/устаревшего ООП-DDD (Эванс, синяя книга): богатая иерархия, internal для bounded context внутри сборки, вариативность для репозиториев и т.п. Тут C# реально один из лучших.
Но современный DDD (Влашин, “Domain Modeling Made Functional”) про другое: сделать невалидные состояния непредставимыми через типы. Здесь Rust сильнее: Sum types, new type pattern, Result<T, E> и ? делают ошибки домена частью сигнатуры, а не исключениями мимо типов
Ну это какое-то зумерское изобретение вы описываете. "Нео-DDD" для разработки в функциональном стиле. Вот у Ханонова книга 2022 года по DDD вполне опирается на Эванса.
Вылезайте уже из легаси, наконец!
Вы молитесь на инкапсуляцию и «защиту инвариантов внутри класса» как на священный грааль, но по факту вы просто строите крепость из костылей в ООП, который не даёт хороших гарантий. В классическом ООП-DDD по Ханонову ваш «чистый домен» это заложник рантайма. Вы надеетесь, что программист не забудет вызвать Validate(), не пропустит null и не поймает InvalidOperationException в самый неподходящий момент
Ну и мне 50 лет. Спасибо за зумера, бггг
Ну раз вы перешли на личности то отвечу:
вы сильно переоцениваете свои софтскилы, что для тимлида плохо
архитектурные и технологические знания у вас устаревают, рекомендую всё же прочитать про make illegal states impossible и современный DDD хотя бы в кратком изложении Беспоясова https://bespoyasov.ru/blog/domain-modelling-made-functional/
Интересно и познавательно, но в пересказе опять ничего насчет производительности.
Очень редко бизнес приложения являются CPU bounded. Обычно узкое место бд и сеть. А часто они вообще не нагруженные
Если event sourcing проблема, то снапшоты.
Реже загружать агрегат целиком, для чтения CQRS (читать таблицы по отдельности или денормализованные данные из отдельной бд)
И добавлю: Беспоясов увлёкся событийным подходом (и обследование и реализация). Но у Влашина основная мысль всё же защита домена, гарантированная компилятором. Event Storming, bounded contexts, context mapping - это не его территория, он ссылается на Эванса/Вернона и идёт дальше в реализацию на F# (у Беспоясова ещё TS)
Я уже апнулся до архитектора, спасибо.
Появление альтернативных прочтений какого-то подхода не делает автоматически всё остальное устаревшим. Придумали, как совместить функциональщину с DDD, это круто, и наверняка находит свои места применения. Но всерьёз говорить о полной замене классического подхода это примерно то же самое, что считать, будто с появлением грузовых автомобилей больше не нужны легковые. Просто конкретно вы, как я понимаю, противник ООП, имеете право, но превращать отрицание ООП в максиму всё-таки не стоит.
А вообще, приходите на какую-нибудь крупную конференцию с докладом DDD made functional. Во-первых, тема интересная, и многие с удовольствием послушают и поучаствуют в обсуждении. Во-вторых, защитите свои продвинутые архитектурные и технологические знания и утрёте нос закостенелым адептам старья вроде меня. В-третьих, там сразу и на софт-скиллы посмотрим )
Какую конференцию посоветуете?
А почему PHP на предпоследнем месте? Это вроде дефолтный подход в PHP при использовании Symfony.
Я бы C#/Java/PHP вообще на первое место поставил, самые выразительные языки под это всё дело.
Мой уточнённый порядок пригодности языка для DDD (писать можно на всём, но справа больше боли):
Scala/F# > Rust > TS >= C#/Java > PHP 8+ > Go > JS
Про PHP+symphony. Коротко: язык исторически хуже подходит по удобству моделирования домена, хоть Symfony и даёт архитектурную дисциплину
Слабая и поздняя статическая типизация, нет ADT/exhaustiveness — сложнее гарантировать инварианты до выполнения
Мутабельность и неоднозначная семантика объектов/копирования затрудняют безопасные VO и агрегаты.
Большой пласт legacy и императивных практик — требует строгой дисциплины и внешних инструментов (PHPStan/PSalm), чтобы DDD работал.
TS примерно на уровне C#/Java, может чуть выше за счёт structural typing и discriminated unions из коробки (type Result = Success | Failure работает естественно).
JS - ниже Go. Без типов DDD превращается в боль: value objects, агрегаты, инварианты держать не на чем.
TS выше Rust не ставлю - в Rust система типов сильнее (exhaustiveness, ownership помогает моделировать жизненный цикл агрегата), но Rust заставляет думать про память там, где для DDD это шум.
Ну и, кстати, node.js + TS неплохой вариант для DDD. Rust, Scala, F# редко встречается в живой природе
TS >= C#/Java
Транспилятор над однопоточным языком, у которого в базе вообще нет адекватного ООП, выше, чем хардкорные ООП энтерпрайз языки.
В целом понятно, вы описываете пригодность для какого-то любимого вами определённого вида переосмысления DDD, но статья всё-таки про классический.
Зря вы так про TS. В плане выразительности он на две головы выше Java/C#.
Строгая структурная типизация и всякие ништяки на типах ставят на колени традиционные языки с номинальной типизацией.
Однопоточность - да, тут не поспоришь.
Возможно вы ведёте речь про абстрактный код в вакууме без доступа к интернету, написанный в блокноте. Тогда всё действительно так, да и то не везде.
Теперь по-порядку.
Слабая и поздняя статическая типизация, нет ADT/exhaustiveness — сложнее гарантировать инварианты до выполнения
Это не правда. Переключение между сильной и слабой делается 1ой строкой “declare”. Да, гарантия строгости исключительно контрактная, а локальный флоу метода остаётся слабым. Но для решения этого используются линтеры.
Типизация тоже так же хоть и опциональная, но в контрактах статическая. Более того, копилятор для некоторых типов гарантирует их на этапе компиляции (ранняя), а не рантайма: https://onlinephp.io/c/25100
Это действительно может усложняться динамической линковкой всех символов в языке (автолоадинг), но это by design, а при большом желании можно заранее скомпилировать нужный код до запуска через preload функционал, помимо непосредственно “прогрева” получив ошибки компиляции, а не времени выполнения (спойлер: никто этим не пользуется, так как смысла нет, прогрев и так моментальный, JIT тоже, а все проблемы решает линтер).
Т.е. прям так заявлять “этого нет” - обман. Это всё есть и зависит как от самого кода, так и от экосистемы. Если не нужно, то нету, если нужно - всё есть и глубоко проникло в экосистему, так же как тот же TypeScript, даже если убрать все типы вообще и фигачить исключительно на mixed (any):

Алгебраические (в ADT не “абстрактные” ведь имелись ввиду?) типы данных в языке уже лет хз сколько (например callable или iterable - как раз оно и есть), если не учитывать возможность описания типов в CNF/DNF формах из коробки уже лет 6+ как и создания тайпалиасов (уже не “из коробки”, но всё же). Добавим к этому наличие дженериков, условные типы, возможность писать функциональные типы и прочее…
Более того, в современном мире никто без линтеров не работает (если это не “hello world”, конечно). А тут и исключительно сильная типизация, и исключительно ранняя, и гарантия “исчёрпываемости” и не только в рамках всяких перечислений, но и в union/intersection, в отличие от линтеров, например, в Python, где последнего нет.
Мутабельность и неоднозначная семантика объектов/копирования затрудняют безопасные VO и агрегаты.
Для иммутабельности есть соответствующие data-объекты стандартной библиотеки (правда очень мало) и модификатор readonly, как для классов, так и для полей. Есть даже удобные операторы клонирования объектов из-коробки, которые реализуют evolvable-структуры даже при наличии таких ридонли.
На счёт “неоднозначного” копирования вообще не понял. Есть передача объектов по ссылке, есть по указателю (копирование указателя), есть по значению (копирование значения). Первые 2 простые и однозначные, последний позволяет контролировать поведение копирования и контролировать какие данные следует копировать.
И это всё из коробки, в отличие от множества других языков (там, где есть какие-нибудь deep-copy библиотеки, например).
Большой пласт legacy и императивных практик — требует строгой дисциплины и внешних инструментов (PHPStan/PSalm), чтобы DDD работал.
Ну это действительно правда. Но это и в других языках так.
Могу разве что согласиться с тем, что PHP позволяет отстрелить себе ногу бОльшими способами, чем какой-нибудь C# или Java из-за более гибкой системы метапрограммирования и возможности писать всё на mixed (any) типах.
Другое дело что во-первых, линтеры в PHP (тот же самый PHPStan) лучшие из существующих и в современном мире не пользоваться ими - это примерно как писать на JS, вместо TS. Опять же, речь про продуктовую разработку, где DDD имеет смысл (как и сабжевый TS), а не “хелло ворлд” за пару вечеров.
А по поводу остального, вообще не понял причём тут DDD. Ну разве что structural typing касательно сравнения VO… Но в пыхе для этого есть оператор сравнения значений (==), которым никто не пользуется особо, т.к. для VO всегда реализуется equals (чтобы иметь возможность контролировать поведение сравнения VO).
Возможно вы ведёте речь про абстрактный код в вакууме без доступа к интернету, написанный в блокноте
Предлагаете использовать линтер+PHP doc вместо компилятора? Ну, анальным способом можно даже дженерики добавить, которых в самом языке нет.
Exhaustiveness для sum-типов эмулируется
default => assertNever(...)
ADT нет, вместо них абстрактный класс + наследники, что многословно и не даёт структурного сравнения
То есть всё можно сделать но многословно и без страховки на стадии компиляции от выстрела в ногу.
Можно отключить статанализатор, закоммитить и задеплоить). И тому подобное, это работает, пока:
Вся команда дисциплинирована.
CI/CD строгий (уровень 9 PHPStan + тесты).
Нет «быстрых фиксов» в прод через
eval()или Reflection.
Предлагаете использовать линтер+PHP doc вместо компилятора?
Это ровно точно такой же способ, как писать код на TS, вместо JS. Просто PHP пошёл по этому пути, дополнения языка, а в JS Flow не взлетел, зато TS взлетел, через препроцессинг.
Ну и да, линтер работает точно так же как и компилятор. Точно так же анализирует control flow graph, точно так же выводит типы, точно так же гарантирует схождение оных.
Отличие от компилятора только в том, что он не генерирует объектный код, не оптимизирует (а просто предлагает это сделать) всякие инструкции и весь выхлоп – это “ОК” или “не ОК”. А механизм идентичный.
ADT нет, вместо них абстрактный класс + наследники, что многословно и не даёт структурного сравнения
т.е. type X = Some | Any - это не ADT? Да просто Some | (Any & Ololo) - это что такое?
Можно отключить статанализатор, закоммитить и задеплоить
Можно отключить TS транспайлер в Vite/WebPack, закоммитить и задеплоить. Ага.
Мне нечего возразить из практики.
У нас нет PHP Stan: много ложно положительных сообщений и так исторически сложилось. Возможно, на системах уже недавно спроектированных и написанных на PHP 8+ он ведёт себя лучше.
А можно пример ложноположительного хоть одного? На моей практике он фолс-позитив даёт исключительно для нового функционала (например не очень дружит со свойствами: get => parent::$property::get()) или если какая-то странная логика.
У меня есть один легаси проект, написанный в стиле рукожопства, и там висит PHPStan 10lvl с baseline и ничего, потихоньку вычищается и ложноположительных ни одного.
Такое только на современном PHP 8.5 коде.
Добавлю про интерфейсы: простое правило, интерфейс нужен не для «нескольких возможных реализаций», а для абстракции без которой невозможно организовать зависимости между слоями.
Репы в слой домена, фабрика в слой апп (например если у нас сущности зависят от инфраструктуры, и описываются также интерфейсами).
Также я не вижу проблемы если сущность анемична, делайте бизнес логику в handler/command прямо в домене, какая в этом проблема?
В целом всё по делу, но я бы сказал, что у вас сейчас юзкейс почти буквально делает только orders.Save. В моей практике на уровне Application всегда объявляли DTO для запроса и ответа. Соответственно на вход в юзкейс приходит десериализованный createOrderRequest, а все маппинги и вызовы фабрик как раз в юзкейсе.
Сейчас хэндлер толстоват, имхо.
Забавная ирония судьбы.
При разработке Go авторы сделали все чтобы язык был максимально прямолинейным и простым, местами до тупости. Не все получилось просто но зато получилось сделать язык с максимально коротким сроком обучения. Что позволили ему быстро набрать популярность за счет переучивающихся разработчиков с других языков.
Но вот произошла типичная история про мигрантов - бежим из страны в которой невозможно жить чтобы в новой стране воссоздать ту страну из которой сбежали.
Так же по факту произошло и с Go. Сбежавшие джависты и еже с ними, уставшие от задравшего всех многословного переабстрагированного энтерпрайз подхода, принялись в Go строить заново все то от чего убегали.
По итогу Go, где пожертвовали всем ради простоты, с таким подходом превращается в кусок говна. Так как не остается ни простоты ни удобства.
А это везде так куда джависты приходят, как-то приходилось видеть проекты на питоне после джавистов...
Я пришёл из C# Rust в PHP легаси (правда, на больше на архитектора чем программиста). В результате, написал свой мини ОРМ :facepalm:
Data mapper вроде dapper/SQLx + UoW (точнее, транзакционна обёртка)
сознательно отказались от identity map/автотрекинга
Doctrine и пара других не подошли: Active Record плохо ложится на DDD, остальные слишком лезут в домен и управление коннекшеном. А мне нужно было сохранить огромный легаси read side как есть, поэтому написал минимальный маппер.
Через сколько ушёл и оставил ребят с этой самописной орм?
Ещё не ушёл
И все довольны: сильные программисты понимают, остальным и не надо туда лезть
А как давно написал? Когда планируешь уйти?
Тут долгая история...
Пришёл в проект 2 года назад. Проект - 15 летние госы, 100kloc страшной лапши, система ответственная, качество в приоритете (смех в зале). Изначально я пришёл (перешёл с другого проекта и другого стека) при условии что я архитектор (то есть свободен в архитектурных решениях, но код писать самому), а на рефакторинг будет 30% ресурсов. Первый год их не дали, кстати. И ресурсов на было и не разобрались ещё как оно должно работать и инцидентов много. Потом с моей стороны было несколько попыток. Первая серия пошла в корзину. Археология, восстановление замысла, создание домена - пока одного, но со сложными безумными инвариантами и хитрым агрегатом который цепляет тысяч 30 строк кода. 15-20.000 строк кода в сумме удалено, столько же добавлено. Это если не считать создания тестов. Страшные merge request на +10kloc, -10kloc, остальные чуть меньше
Характеризационные тесты. Меньше mr не получается, риски понимаю. ADR , глоссарий и описание схемы веду, но никто не читает.
30.000 строк ... Это просто объем модулей которые читают или пишут в эти таблицы. В итоге read side просто не стал трогать и реальное число меньше
И это всё без изменения функционала, только для надёжности.
Уходить не собираюсь пока на рынке труда не потеплеет
А надёжность как измеряли?
Предыдущая система (аналитика) раз в месяц глючил (падал или давал кривые данные) распределённый сбор данных по регионам (с ретраями, идемпотентностью и тасками больше часа), но очень неудачно, так что заказчик обнаруживал это раньше нас. Идемпотентность ломалась, что был особенный треш с удвоением данных. В логах чушь, никак не помогающая понять.
В итоге переписали полностью ядро, добавили конечные автоматы состояний таски. И все проблемы прекратились. Уже после переделки одну из проблем нашли: искрило локальное соединение апп-бд из-за кривого оборудования, эти ошибки не логировали потому что такого не может быть никогда)) да и как искать, если точно момент не определить, а в логах за неделю тысячи обрывов, восстановлений и ретраев
Сейчас примерно тоже самое: документооборот. На подпись министру (после множества согласований) приходит мусор. Потом скандалы. Несколько раз в год, но нам хватило.
Кроме непосредственно надёжности переделка должна создать автотесты для ускорения и безопасности дальнейших изменений.
Рефакторинг если всё суммировать (включая что ушло в мусорку) 6 человеко месяцев.
Просто Go не для этих задач. Он для сетевых утилит ака микросервисов
Так это не в мигрантах дело. Просто бизесы стали тащить Го в энтерпрайз со сложным доменом, ну потому что все ведь пишут на Го, значит и нам надо. И Авито переходит с C# на Go, переписывая тот бизнес, для которого был выбран C#. Результат понятен, но это же не разработчики решили так делать.
А кто решает если не разработчики? Бизнес приходит и говорит: "используете обязательно DDD"? Да бизнес это вообще не волнует. На Go можно писать хоть легкий домен хоть сложный домен хоть Kubernetes, если руки из правильного места.
Проблема в том что в языке не успела сформироваться культура написания таких проектов а "мигранты" по больше части ничего кроме ООП, слоистых архитектур, чистых кодом, интерфейса на каждый чих и прочего мракобесия не видели и не умеют, вот они и начинают строить джаву в абсолютно другом языке, с другими свойствами, плюсами и минусами, закономерно получая кусок говна. Потому что в Go нужно вывозить за счет минимализма который язык дает и тупой как доска простоты и банальности. А на деле, подумаешь в Go нет классического ООП - будем изображать накручивая костыли, подумаешь в Go нет средств упрощения абстракций - конечно же будем городить 10 слойные архитектуры, писать кучу бойлерплейта там где его нечем победить, подумаешь в Go совершенно другой механизм как работы так и философии интерфейсов - пофиг будем делать как в Java.
Не в бизнесе это дело а в людях который пришли со своим уставом в чужой монастырь. Такая же ровно история когда такие уходят в JS/TS или например в Rust.
Так а что нужно делать на Го по вашему мнению в сложном домене? Анемичные модели и процедурный код? Чтобы каждый сервис имел 20 зависимостей, а инвариант любой сущности можно было сломать где угодно?
Кубер это чисто техническая тулза. Сложная, да, но не с доменной точки зрения. Модели в среднестатистическом электронном документообороте будут по поведению и машине состояний в разы сложнее, чем модели в кубере.
В том то и дело, что ООП это буквально высокоуровневая надстройка над средствами менеджмента данных, которая позволяет заниматься бизнес-моделированием и немного забить на техническую часть (то, как именно данные хранятся и передаются). Есть задачи, где ООП удобен, и в бизнесе таких задач полно. Зачем нести туда не-ООП язык? Ради моды?
Так а что нужно делать на Го по вашему мнению в сложном домене?
Использовать методики и средства в соответствии с инструментами. Если философия Go банальность и простота то нужно использовать максимально линейные и простые подходы чтобы развить сильные стороны и избегать слабых.
Кубер это чисто техническая тулза. Сложная, да, но не с доменной точки зрения. Модели в среднестатистическом электронном документообороте будут по поведению и машине состояний в разы сложнее, чем модели в кубере.
Давайте будем честны.
Если на проекте будет действительно сложная бизнес-логика, то она уедет на какой-то вариант внешнего или самописного BPMN где ей будут уже заниматься аналитики, это и проще и правильнее чем в коде изображать сложный бизнес.
Если на проекте будет действительно сложная бизнес-логика, то весь ООП пойдет на помойку. Даже банальный Update большого количества записей скорей всего будет описан запросом, а не вытаскиванием из базы избыточных данных чтобы методы подергать. Если еще сложнее, то там уже информация будет строиться в специальные структуры данных которые можно быстро обрабатывать и удобно хранить.
Все эти игры в эмуляцию бизнес-объектов подходят только для тепличных условий, где в действительности большой сложности и нет и где DDD подход имеет сомнительную эффективность но вполне заметные издержки.
В том то и дело, что ООП это буквально высокоуровневая надстройка над средствами менеджмента данных, которая позволяет заниматься бизнес-моделированием и немного забить на техническую часть (то, как именно данные хранятся и передаются).
Вот только если посмотреть правде в глаза, за много десятилетий существования ООП, мы видим его вырождение. Большая часть backend проектов это про anemic model. Frontend/Mobile/Desktop(вот уже где самое место) практически поголовно отошли от ООП подхода. Уже много лет в языки ничего ООП-шного не добавляется а некоторые новые языки вообще отказались от этой парадигмы. И это не потому что ООП плохое. Оно очень узко местечковое, также как и ФП и нельзя на нем строить системы. Можно долго спорить но факты есть факты.
Зачем нести туда не-ООП язык? Ради моды?
Затем что мир не крутиться вокруг ООП и не крутиться вокруг архитектурных рюшечек. Есть и другие требования для проектов.
"Статья", написанная LLM. Ответы некоторых пользователей тоже. Тяжело коммуницировать и, тем более, верить людям, которые для формирования и\или выражения собственных мыслей используют генератор буков. Сам факт генерации всего кода\слов ценность текста опускает чуть ли не до плинтуса. Потому что я пришёл сюда статью читать, а не полотно генерации, которое сверху фактчекать надо.
Да, вот интересно, как автор планировал синхронизировать запись в БД и вызов Order.Confirm()
Забудьте уже про Мартина
Кажется, ежегодно на Хабре возникает всплеск обсуждений в духе: "Вы всё делали неправильно, общепринятые подходы это говно, их авторы дураки, а я сейчас расскажу вам, как надо". И хоть кто-нибудь из критиков Мартина выпустил бы книжку-бестселлер с феноменальным разрушением старых подходов, но что-то дальше комментариев в интернете дело так и не дошло.
А потом приходишь на работу в любой энтерпрайз, а там легаси-процедурщина с зацеплением, протечками, отсутствием моделирования, и погружение нового разработчика в домен занимает почему-то два года.
Так и в описанных мной ситуациях те, кто это всё проектировал и писал, уверены, что у них по красоте :)
Профессионалы инфоцыганщиной обычно не занимаются
Понятно. Книги пишут только посредственные графоманы, поэтому хорошей профессиональной литературы не существует. Клепманн тоже не профессионал, раз книгу написал. И вообще, кому нужны эти тупые книги? Комментаторы на Хабре лучше знают.
Нет разбора кейса где взаимодействует несколько aggregate root. Не всегда бизнес кейсы ложатся в один aggregate root. Иначе довольно простой кейс перевод денег превращает в корень все счета. Мы обычно используем доменные сервисы для таких кейсов в пакете domain. В целом описано хорошо в Learning Domain-Driven Design.
Опять нейрослоп. Жаль, а тема интересная.
Кажется метод Save хорошо бы убрать из Order и в usecase явно вызывать orderRepo.Save(ctx, order)
я раньше тоже описывал интерфейсы в пакете domain, но потом пришел к мысли, что это не по Гошному. И гораздо правильнее делать это в usecase, потому-что это позволяет минимизировать интерфейс. И в итоге получаются отлично изолированные сценарии, которые очень удобно разрабатывать и тестировать изолировано
И на моем опыте агрегаты в Go это боль, поэтому я всегда стараюсь их избегать
"Никакого UseCase-слоя. Добавите его, когда появится реальная логика." - Это антипаттерн Clean Architect.
Ваш бизнес сегодня действительно использует Ваш сервис как прокси. А завтра у него возникла идея, что логика всё же должна быть. От банальных проверок до жанглирования моделью данных через кучу адаптеров.
Даже если у вас сейчас dal.save в логике. Завтра это уже не так. Задача юзкейса изолировать не только бизнеслогику от деталей. Но и адаптеры от хендлеров. Это даёт коду устойчивость к изменениям.
То есть изолировать детали друг от друга тоже нужно. Хендлеры максимум понимают сигнатуру адаптеров (и то есть примеры, когда и это в DI заворачивают, что я бы назвал избыточным, но там дело вкуса). И просто доставляют их в юзкейс. То есть доставляют в логику то, чем она должна оперировать.
Если хендлер сразу пошёл в адаптер - это значит, что хендлер обязан знать как с ним работать. И вы создали одну деталь. Это больше не хендлер и адаптер - а обычный код. Который потом придётся рефакторить. Потом настен через день два.
Чистая архитектура это не просто разделение кода по папкам. А про разделение ответсвенности и границ кода в этих папках. Даже если одна строчка в логике - значит такова логика на момент сейчас. Но она есть)
На крошечные проектах всё понятно, но на остальных появляется много вопросов
Все статьи такого рода показывают очень простые примеры, которых на практике не бывает. Как только вам понадобится транзакция с логикой до/между/после вызовов нескольких репозиториев эта архитектура ломается. Появляются проброс транзакции через контекст, транзакшн менеджеры, юнит оф ворки и прочая ересь которых в домене в реальности не существуют да и clean уже такое назвать язык не поворачивается.
Впечатляет, как и всегда в статьях по DDD несколько вещей.
1. ValueObject с публичными полями которые можно мутировать. Метод Add конечно здорово, но это не имеет значения - объект не иммутабелен, как бы в этом не убеждал комментарий к коду.
2. Отсутствие валидации валюты в запросе (упрощение? удобная позиция).
3. Очередной энумератор статуса - который из строки преобразуется в строку. Хотя можно реализовать методы поддерживаемые интерфейсами библиотеки sql. А еще можно и сгенерировать сразу (благо тулов для генерации - вагон), но удобнее ясность поддерживать (прямым приведением к строке). Так чтобы на стадии сериализации/десериализации гарантированно получать не консистентные состояния.
4. Вишенка на торте обновить заказу статус. Полчаса назад отменили или ЗАВЕРШИЛИ. Ничего страшного - вернем к базовому статусу (а кто тут гарантирует, что снаружи не прилетит повтор того же заказа?).
5. Классический defer на методе с ошибкой Rollback'a, там ведь никогда не будет ничего интересного. А потом это недиагностируемая история, при изменении кода.
6. Неправильно работаем с ошибками - возвращаем ошибки из чужих модулей, коснтруируем статические.
Одна из прекрасных особенностей "архитекторов", забывать о деталях. В итоге ни чистой архитектуры, ни чистого кода.
На самом деле поднадоели такие статьи. Все довольно поверхностно. И они не отвечают на неудобные вопросы.
Я когда на более ооп-шном C# попробовал сделать DDD - там столько вопросов возникло. В реализации на Go из статьи они тоже есть. Вот например фабричный метод, в заказе может быть 100500 всяких бизнесовых полей - это вы все их пихать будете как параметры? То есть на каждый чих менять контракт? А не задолбаетесь?
Почему в таких статьях DDD рассматривается в отрыве от производительности? Автор, вы понимаете, что DDD со своим "восстановить сущность из хранилища и потом запускать на ней бизнес логику" - это жутко медленная штука? И если потребуется выжать больше 50 rps без деградации с ростом нагрузки, то придется переходить на нативный SQL / хранимки с логикой. А это уже ни разу не DDD.
Я думаю одна из основных проблем почему в итоге получается оверинжиниринг при использовании всех этих DDD и архитектур в том, что их пытаются применять везде и всюду. Как было верно отмечено в поясняющих статьях, основная проблема которую пытаются решить эти техники -- борьба со сложностью. На мой взгляд, в мире не так много по-настоящему сложных систем. Да и не все сложные системы гладко ложатся на DDD или/и какую-то архитектуру. К примеру, любой научный домен, на мой взгляд, как-то неправильно натягивать на DDD. Теряется "научная лаконичность". А сложность там может быть запредельно велика. Банковские системы, к примеру, или там трейдинговые системы или юридические, возможно, лучше всего ложатся на DDD. Потому что даже номинально, база знаний и зависимостей там очень велика Ну то есть это DDD скорее не нужно, чем нужно. Не стоит кидаться в DDD и Clean Architecture с каждым новым клиентом, который запросил электронный магазин или небольшую складскую программу. А если даже система и строится по всем этим вышеописанным принципам, то она вовсе не должна быть полностью ими покрыта, чтобы все эти value type и aggregate roots из всех щелей лезли :) В реальности, все зависит от сделанных компромиссов, которые придется сделать.
Кажется ddd - это про то что можем пожертвовать изоляцией ради перфоманса например А у вас все преимущества и эффекты про clean.
И ничего не написали какие паттерны архитектуры работают в java но и работают в go, хотя порекламировали во вступлении.


Clean Architecture + DDD в Go: как не превратить проект в 200 файлов ни о чём