Pull to refresh

Comments 65

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

Скиньте ссылку на репозиторий. Сложно смотреть в картинки/скриншоты кода.

Покажите реализацию сущности db. Которая умеет Users..
Это ORM? За конкуренцию в запросах вы сами следите?

Что с идемпотентностью?
Обновил раздел Service, показал БД.
Весь проект не уверен что смогу показать, это не прототип, а реальное приложение.
посмотрел Database. Получается у вас Сервисы жестко зависят от реализации Database.
Если вам скажут перейти на Postgres, к примеру, — вам придется переписать весь Services. DDD который вы читали и применяете — говорит что так делать нельзя, как вы реализовали.

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

Непонятно почему самоцель отказаться от маппинга?

Тесты тоже странные. Зачем-то пришлось писать билдеры — лишняя работа. Я еще понимаю, интеграционные тесты — там билдеры состояний для кейсов. Но здесь это явно лишнее.
Еще стиль тестов — вы внутри теста делаете ребус — угадай какое состояние — угадай какое оно будет в конце теста. зачем??? всякие byte.MaxValue. Чтобы программист тренировал память на константы?

Видите ли, CQRS (как и DDD в принципе) придумали умные дяденьки и тетеньки для приложений других масштабов. Если:


  • вы работаете над приложением один;
  • вся бизнес-логика сводится к краду над парочкой моделей;
  • "[BL] 80% вызывает 1 метод и прокидывает результат выше",

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


А вот когда:


  • над приложением работает команда программистов (а то и несколько);
  • бизнес-логика сложна и одна операция затрагивает одновременно несколько сущностей;
  • у вас несколько стейкхолдеров, и каждый хочет от системы что-то свое,

вот тогда вам внезапно начинают нравится Domain Services, Aggregate Roots, Events и прочие штуки. Не говоря уж об интерфейсах и всяких там солидах.


А пока что ваши рассуждения выглядят как-то так: "Что за люди придумали экскаваторы? Сложно, куча абстракций, топливо залей, за рычаги подергай… Я вон взял лопату, грядку у бабушки на огороде вскопал, и нормально". При этом и лопата, и экскаватор – отличные инструменты. Только задачи они решают разные...


А в приведенных вами кусках кода – там действительно не DDD надо впихивать. Там бы сначала решить проблемы попроще, например, с инкапсуляцией. Вот если вам надо withdraw у пользователя сделать – что мне как программисту мешает просто написать user.Wallet[currency] -= amount? Как я должен узнать, что мне надо использовать расширение user.Withdraw()? А еще, я так понимаю, перед этим я должен убедиться, что user.HasOnBalance()? И где этот код должен быть – в контроллере? Так у вас тогда логика просто размажется по контроллерам, и поддержка превратится в ад. Ну и так далее.

Я рассказывал про код приложения где использовалась N-layer, это был проект который писало 7 разрабов.

А пока что ваши рассуждения выглядят как-то так: «Что за люди придумали экскаваторы? Сложно, куча абстракций, топливо залей, за рычаги подергай…

Я не говорил что абстракции — сложно, я говорю что их часто не оправдано много. А 4+ вложеностей абстакций с паарой реализаций, очень удобно дебажить, ведь так? Тыкаешь F12, оба посмотрел на список реализаций, ой, а теперь нужно понять какую из них заюзает runTime? а там, за выбор реализации по Id отвечает фабрика, с фаричным методом в придачу, и не забываем что у нас абстрактный репозиторий, с unit of work который обернул репозиторий EF (Db context). Время занимает просто понять как сработает код, не то чтобы ошибку искать. А че? зато заменяемо.

Я не говорил что абстракции — сложно, я говорю что их часто не оправдано много. А 4+ вложеностей абстакций с паарой реализаций, очень удобно дебажить, ведь так?

А какая разница? F11 все равно приведет в ту, которая сейчас используется.

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

А для тех, кто код читает, абстракции обычно пишутся так, чтобы за них не надо было ходить. Потому что они, ну знаете, абстракции.

Я рассказывал про код приложения где использовалась N-layer, это был проект который писало 7 разрабов.

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


  1. Разработчики были не очень хорошие, и они нагородили неправильных абстракций.
  2. Разработчики были нормальные, просто они игрались с разными подходами.
  3. Разработчики были нормальные, и они все сделали правильно, но вам не хватило опыта (времени, желания etc) осознать, что это была необходимая сложность.

Но вполне вероятно, что эта была смесь из вышеперечисленного.


А 4+ вложеностей абстакций с паарой реализаций, очень удобно дебажить, ведь так?

Да вроде не так уж и сложно. Вот асинхронный код на каких-нибудь event'ах – да, неудобно, но к количеству абстракций это отношения не имеет, его всегда неудобно дебажить (но мы его любим не за это).


я говорю что их часто не оправдано много.

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

На счет инкапсуляции, согласен, нужно подумать как зыкрыть set и не потерять в удобстве Core
Если функции нужен баланс юзера, то МЫ достаем баланс, и передаем в функцию, а НЕ пихаем сервис юзеров в BL.

Круто. Один простой вопрос: если у вас поменялись требования на валидацию (операции или сущности), и этой валидации нужно на один источник данных больше, в скольких местах придется поменять код?


Скажем (пусть это и не валидация, зато прямо в коде пример есть), операции user.MakeToken() понадобился CRNG. Что происходит дальше?


В этом классе собраны все возможные ошибки приложения, на которые реагирует exception handler

… а не на уровне фронта у вас ошибок не бывает?


(кстати, счастливой отладки вам при таком "логгировании")


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

Это, кстати, неправда.

Недавно была задача добавить ReferId в User, а дальше начилсять рефералу бонус после неких действтий. Задача решилась минут за 15. Добавил свойство в модель, расширил метод makeToken чтоб закинуть в JWT referId для быстрого доступа, в нужных местах достал id и по Id начилил бонус.

Это не то, что я спросил.


Повторю вопрос еще раз: если вашей "чистой функции" становятся нужны дополнительные данные, в скольких местах надо менять код?

В самой функции + 2 места использования, регистрация и вход.

Бинго. Во всех местах использования, а если те — тоже "чистые функции", то во всех местах их использования, и так дальше по цепочке. Хотя, казалось бы, изменение-то локальное, на внешний контракт влиять не должно, и вот это вот всё. А как же принцип единой ответственности (точнее, единственной причины изменения)? А как же инкапсуляция?

MakeToken() это метод на уровне апишки, это не Core, в этой задаче домен даже не будет изменен

Во-первых, какая разница, все равно проблемы остались.


Во-вторых, а что, на уровне бизнес-логики такого не бывает? Ну представьте, что вам в Deposit нужно подписание каждой транзакции.

UFO just landed and posted this here

… а дальше возникает вопрос, чем это с практической точки зрения отличается от вызова метода на объекте, который представляет собой тот же environment. По большому счету, замыкание оно и есть замыкание.

UFO just landed and posted this here
… а не на уровне фронта у вас ошибок не бывает?

Обратите внимание что я возвращаю не только текст, а и код ошибки.
Текст нужен разрабу, код для справочника ошибок. Фронт по коду достает из словаря (eng ru) текст и дает внятный ответ ошибки

А толку? Как вам это поможет разобраться с конкретной ошибкой в беке? Стектрейс, оригинальная ошибка — это все вам не нужно?

Стектрейс присылается в логи

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

UFO just landed and posted this here

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

1) Без миграции, в массиве у пользователя не будет новой currency — гарантированный exception
2) Выглядит грамоздко
3) Хотелось чтоб ошибки были собственного типа (AppError) для удобной обработки.
Ну что тут можно сказать, все когда-то были на этом этапе). Ваш код нелох сам по себе, но вы же понимаете, что вся критика тут только лишь потому, что вы это преподносите как идеал, к которому все должны стреимться.
но файлов было так много, что просто кошмар

Так а простите, куда вы будете девать свои файлы, когда проект разрастётся?
Я вынес методы как методы расширения дабы класс не раздувался, а функционал можно группировать по фичам

Ага, а дальше будет этап когда фичи начнут раздуваться и в каждый метод вынесете в отдельный класс и придёте к CQRS.
Вы немного лукавите предоставив только тут одну простую сущность «пользователь». Но вот представьте, например, что вашему пользователю придётся играть в три игры рулетку, бандита и блекджек. Потом выяснится, что в рулетку играют в 5 раз чаще чем в блекджек, а в бандита раз в день и то не факт. Потом захочется показывать аналитику по всем трём играм. Потом захочется видеть аналитику по всем пользователям и тд.
Жду ещё одну статью как вы всё это без моков масштабируете
Как я и говорил тестировать нужно только логику...

Ну, допустим, что так.


Есть места где я ещё точно не знаю могу ли я вообще работать с юзером, возможно у него банально нет денег для этого действия...

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

Моки всего лишь перенесутся с одного уровня, на другой. То есть вы упрощаете тесты в одном месте, но усложняете в другом.
Вы взяли простой пример, чуть сложнее «Hello World», и пытаетесь показать, что для «Hello World» фабрики не нужны (KISS). Но даже к такому простому коду много вопросов, к примеру, самый первый метод контроллера:
if (user != null) throw ...
тестировать уже не нужно?
Далее, это веб -> параллелизм, что часто забывают начинающие разработчики, и управление им невозможно спихнуть на кого-то еще. Где у вас расположиться транзакция? Но предположим, вы полагаетесь на уникальный индекс, зачем тогда эта проверка?
В целом, похвальная попытка, но когда вы испытаете боль от изменений в вашей реализации, на что вам пытаются намекнуть в комментариях, у вас появится лучшее понимание назначения DDD, SOLID, dependency inversion и пр.
Я рассказал про решение параллелизма.
Проверка нужна в случае если id нет, очевидно же
В разделе Service, скриншот метода Update
Вы, видимо, меня не поняли. Я все время говорил про метод Register и, соответственно, Create.
И вместо даты я бы посоветовал простой counter — проще и надежнее.
А про тестирование всего кода я тоже написал, сейчас в разработке e2e тесты
Я вижу тестирование того, что вы называете Core. Проверка находится в методе контролера, вы ее тестируете?

А вы в курсе, что если вызвать параллельно несколько раз метод play, то у вас база придет в неконсистентное состояние? У вас баланс сохраненный в wallet начнет отличаться от суммы транзакций.

Нет, такого не будет. В методе обновления проверяется версия файла (Дата изменения). Если она не совпадет, то изменения не будет, запрос выдаст ошибку. Почитайте код Update.
Это тестировалось в 50 потоков.

Пропустил этот метод. Есть уверенность что UtcNow всегда будет разным? А если будет больше одного сервера, на котором работает приложение?


Довольно странно выглядит такой вариант “optimistic concurrency”, там где она не нужна. Добавление транзакции в список можно делать параллельно.

В принципе дату можно замени гуидом
Даже если будет 10 серверов, база одна, и запрос FindAndReplace дает мне гарантию как транзакции. Если у метода не выйдет найти и обновить эту сущность, вернет null

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

В PoEAA такое структурирование кода называется Transaction Script. И очень зря вы использовали монгу, так как транзакции она как раз не поддерживает. В этом случае вам как раз-таки стоило бы использовать её функции, типа "найти запись и увеличить значение на 10", чтобы код работал корректно. Но тогда архитектура получилась бы не такой.


У вас вся бизнес-логика это всего одна функция, которая представлена в этом примере. Причем эта функция работает всего с одной сущностью, точнее двумя master-detail, то этот код можно было было одинаково "красиво" написать и в виде DDD, и в виде CQRS, и в виде вообще любого подхода к структурированию кода.


Часть, связанная с вынесением обработки ошибок и логирования отдельно называется Aspect-Oriented Programming, сокращенно AOP. Это когда фреймворк, в данном случае ASP.NET Core берет на себя работ по вставке вызовов "водопроводного" кода, отчищая от него бизнес-логику. Чтобы в итоге бизнес-логика не обращалась к логгерам, обработке ошибок итд.


Это все известно уже лет двадцать, активно применяется более десяти.


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

Кстати.
Покажите еще метод, который вам вернет сколько заработала ваша система за период. Он скорее всего у вас есть.

А вот это часть какого слоя, кстати? Это же бизнес-логика, или нет?

Это находится в сервисе, так как решается одним запросом.

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

Теперь у вас два метода, из которых один в слое бизнес-логики, а второй где?


Ну и да, у вас юзер всегда при загрузке из БД приносит все свои транзакции?

Второй в Core.

А что такое Core, и чем это отличается от бизнес-логики?


Всегда.

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

У вас все равно она хуже, чем моя.

Это весьма бессмысленное утверждение.


Но не суть. Что такое Core, и чем это отличается от бизнес-логики?

Ну то есть из двух приведенных выше функций обе лежат в слое бизнес-логики?

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


Проще говоря, в вашей "чистой" архитектуре бизнес-логика размазана по всем слоям, которые у вас вообще есть.

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

Подскажите пожалуйста что вы делаете в случае если у вас есть сложная валидация параметров запроса, запросы в базу, разные условия, и часть этой валидации повторяется для разных роутов. Как это решается и в каком слое? Пишите ли вы логику валидации прямо в экшэнах котроллеров и дублируете ее для разных экшенов или выносите в какой то слой?
Первичтная валидация в основном атрибутами. Вторичная (если нужна, к примеру проверить что юзер есть) уже вызовом сервиса из метода контролера.
Sign up to leave a comment.

Articles