Pull to refresh
4
0,1
Rating
Send message

>Тот же необработанный ArgumentException сообщает пользователю название параметра с неверными данными. Зачем это надо? (вопрос риторический)

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

Тут поддержу. Когда у тебя в методе один сервис возвращает "монаду", другой метод возвращает "монаду", но уже другую, то как-то грустно становится со всеми этими Optional. Несколько раз пытался перейти на этот паттерн и всякий раз бросал.

P.S. Правда это про общий случай. Почему в ваших примерах исключения вызывают у читателей вопросы я, в целом, понимаю.

>Со статическим методом — для чистого switch это сработает, но это процедурный подход. В ООП логичнее инкапсулировать логику в сам объект. Логика принятия решения живёт в самом объекте решения — ни зависимостей, ни моков, ни статических утилит.

А если подход назвать не процедурным, а функциональным, то чем плохо?

>Смена ORM ломает бизнес-логику. Прямая работа с _dbContext.Add() и SaveChangesAsync() — это привязка к EF. Решили перейти на Dapper или вообще на нереляционную базу — переписываем метод целиком, хотя бизнес-правила не менялись.

Вы не сможете перейти на Dapper c EF, какими бы слоями абстракций не обмазывались. EF умеет в ChangeTrackker, Dapper - нет. Весь этот "бизнес-код", отделённый от "инфраструктуры" посредством "абстракции"-репозитория написан так, как написан, именно потому, что знает, что изменения в модели могут быть сохранены в персистентном слое, т.е., бизнес-слой неявно много знает о том, что стоит за IOrderRepository и за его SaveChanges().

Можно, конечно, пойти другим путём, как вы примерно и переделали пример. Не использовать паттерны вида UnitOfWork (SaveChanges()), сделать операции сохранения более гранулярными. Но это означает:
1. Ради абстракции лишать себя удобства, которые предоставляют фреймворки.
2. ChangeTracker для Dapper вы в рукопашку всё равно не забабахаете, поэтому ваша имплементация неизбежно будет пытаться сохранить агрегат целиком, вместо того, чтобы одну строчку в табличку добавить. Со всеми вытекающими проблемами производительности и конфликтов изменений.
3. Опять же приходим к тому, что интерфейс репозитория диктуется не одной только бизнес логикой (сверху-вниз), но и соображениями о персистентности. Если вас (зря) волнует перспектива перехода с EF на Dapper или на произвольную NoSQL, то и в интерфейс вы должны закладывать возможность простой имплементации на любом из этих движков. Это намного труднее, чем просто выбрать один framework.

>В идеале стоит пойти дальше — к полноценной ООП-модели, где логика расчёта рейтинга инкапсулирована в самом объекте User.

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

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

Большая ложь. Если вы модель EF прикроете фиговым листочком интерфейса вы никуда не уйдёте от зависимости ваших моделей от фреймворков. Весь классический DDD намертво прибит гвоздями к используемой в проекте ORM, мнимая абстракция от персистентности - ложь, которая только запутывает дело.

>В коде встретилось огромное количество повторяющихся строковых констант. Это очень плохо: занимает лишнюю память

Не должны повторяющиеся строковые константы занимать "лишнюю память" ибо string pool.

По поводу отношения к функциональщине не соглашусь.

>Сервис написан на C#, где всё — классы.

C# - мультипарадигменный язык, который сочетает классический ООП с различными функциональными фишками. То, что вы описываете - файлы по три тысячи строк, 15 аргументов в методе, обращение к БД и парсинг аргументов в середине бизнес-логики безусловно заслуживает всяческого порицания, но рефакторить это можно разными способами и ООП тут не панацея и даже не must-have.

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

Разбиение GOD-методов на маленькие, хорошо структурированные и хорошо читаемые классы? Хорошо, но ведь их можно было бы так же разбить на маленькие, хорошо структурированные и легко читаемые статические хелперы.

Дальше. Тестирование статики. It depends. Я понимаю, что тот код, который Вы описываете, где из каждого угла может происходить обращение к внешним зависимостям, покрыть юнит-тестами действительно нельзя. Но нет ничего проще для тестирования, чем чистая _статическая_ функция, которая имеет только параметры на входе и данные на выходе и не обращается к инфраструктурным зависимостям. Тогда блок // Arrange - это формирование параметров, // Assert - проверка ответа. И никакой возни с моками и с "удобными" библиотеками мокирования (есть исключения, но не буду вдаваться в подробности). Для того, чтобы такой подход возможно было применить, надо выносить всю сложную бизнес-логику, ориентированную на данные, в функциональное ядро, а возню с инфраструктурными зависимостями оставлять сервисам. Статику - юнит-тестам, сервисы - интеграционным. Т.е., здесь опять есть решение не в чистом ООП, а на стыке двух парадигм и я считаю его оптимальным, потому что вынос функционального ядра и покрытие тестами именно этого ядра делает тесты более устойчивыми к дальнейшему рефакторингу. Мне за последние пару лет пришлось в этом много раз убедиться.

А так спасибо за статью, многие описанные вещи до боли знакомы.

А откуда в данном процессе возьмётся агрегат User, если User ещё не создан?

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

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

Почему же ваш услужливый оператор работает сразу в двух агрегатах ("создаёт учётную запись" и "отмечает номер учётной записи в своей табличке с приглашениями")? Кто такой этот "оператор" в программном коде?

Ну и в чём тогда выигрыш. Вот мы разложили всё по "сервисам" - переиспользуемую бизнес-логику и "use-case'ы" - конкретные бизнес-сценарий. У нас система развивается и "конкретный бизнес-сценарий" превратился в "переиспользуемую бизнес-логику". Так что границы между этими понятиями, которые мы для себя нарисовали, оказались весьма условны.

Транзакцией в БД все проблемы, конечно, не решишь, а вот если на транзакции в БД забить, то можно поиметь очень много новых проблем на пустом месте.

Ну и как сохранять то, без ChangeTracker? Хочу услышать ответ. Написать свой ChangeTracker вместо ORM?

Так очевидно же. Как выглядит типичный код с применением DDD?

var request = repository.GetRequest(requestId);
request.Approve(userId);
repository.SaveChanges();

Ну и как вы в данном случае реализуете репозиторий без ORM? ChangeTracker в рукопашку забабахаете?

Ну и сами "доменные модели". Я застал ещё время, когда вся эта доменка представляла собой набор свойств с {get; set}и конструктором по умолчанию. Потому что EF (без Core) ничего сложнее мапить не умел. Сейчас EF многому научился, но уши персистентности из моделей всё равно там и тут торчат. Вот и получается, что проектирование _моделей_, самого сердца DDD, прочно связано с возможностями ORM.

Подпишусь под многим. Правда, тут не в ASP.NET и не в EF дело. И даже не в DDD, а в IT-евангелизме как таковом.

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

Вообще, я вообще очень люблю книгу Эванса, но больше в soft-части, там где речь идёт о важности дистилляции понятий и моделей. К тактическим паттернам немало вопросов, но в книге для меня они не главные.

Вот у меня есть сценарий - принятие пользователем приглашения о присоединении к системе.

Пользователь проходит по ссылке-приглашению, в результате запись из таблички invitations удаляется, а запись в табличку users добавляется. Кто тут кому root aggregate и как через DDD должны быть организованы события, чтобы всё это дело не рассыпалось? Как это сделать без DDD я и так знаю.

>Мне казалось, что DDD без ORM не живут.

Вот это интересный момент, конечно. Один мой коллега-дотнетчик, когда его отправили учить жизни php-шников, в разговоре с их начальником проронил такую фразу: "ну, я не знаю, есть у вас ORM или нет". От этого по его мнению зависело, можно на их проекте внедрить DDD или нельзя.

И тут корень противоречия. DDD на "философском уровне" про "Tackling Complexity in the Heart of Software" и про полное абстрагирование от способов хранения. А на практике - "если у вас нет ORM, то нет DDD". И ведь действительно очень сложно реализовать стандартные DDD-паттерны проектирования без ORM, если у вас не документоориентированная база данных.

1. Вот был у меня UseCase - создание приглашения о присоединении к системе для пользователя (в табличку запись добавляется, пользователю письмо улетает).
Потом появился новый сценарий - принятие запроса на временное присоединение к пространству пользователя специалиста поддержки, в который так же входит отправка приглашения пользователю (в табличку запись добавляется, пользователю письмо улетает). Теперь мой первый UseCase - уже не UseCase, если это часть другого UseCase? Что мне делать с "последовательностью управляющих конструкций"? Продублировать в двух UseCase'ах? Вынести код из первого UseCase в разделяемый сервис/модель? Вызвать один UseCase из другого?

2. Если сервисы делать маленькими, то это уже UseCase'ы?

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

Information

Rating
4,300-th
Registered
Activity