Как стать автором
Поиск
Написать публикацию
Обновить

Комментарии 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 нужно подписание каждой транзакции.

НЛО прилетело и опубликовало эту надпись здесь

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

НЛО прилетело и опубликовало эту надпись здесь

И?..

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

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

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

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

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

НЛО прилетело и опубликовало эту надпись здесь

Потому что они дают нечитаемую ошибку (особенно 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, и чем это отличается от бизнес-логики?


Всегда.

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

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

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


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

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

Одна в сервисах, одна в бл

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


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

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

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

Публикации