Comments 65
Шикарно. Если кто узнает академическое название подхода, буду признателен.
Сам пришел к похожему подходу, но для нативного кода. Есть вертикаль IO — файлы, сеть, иные поставщики неструктурированных данных, есть вертикаль бизнес-логики(BL) — как вы описываете преимущественно чистые функции, выполняющие необходимые преобразования на уже структурированными и провалидированными сущностями. Есть вертикаль собственно приложения или библиотеки, которая тривиальным линейным кодом использует IO и BL. Иногда для унификации или упрощения вырастает отдельная как можно более тонкая вертикаль преобразования неструктурированных данных в структурированные — тоже тестировать легко и непринужденно через предподготовленные наборы данных.
Покажите реализацию сущности db. Которая умеет Users..
Это ORM? За конкуренцию в запросах вы сами следите?
Что с идемпотентностью?
Весь проект не уверен что смогу показать, это не прототип, а реальное приложение.
Если вам скажут перейти на 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()
? И где этот код должен быть – в контроллере? Так у вас тогда логика просто размажется по контроллерам, и поддержка превратится в ад. Ну и так далее.
А пока что ваши рассуждения выглядят как-то так: «Что за люди придумали экскаваторы? Сложно, куча абстракций, топливо залей, за рычаги подергай…
Я не говорил что абстракции — сложно, я говорю что их часто не оправдано много. А 4+ вложеностей абстакций с паарой реализаций, очень удобно дебажить, ведь так? Тыкаешь F12, оба посмотрел на список реализаций, ой, а теперь нужно понять какую из них заюзает runTime? а там, за выбор реализации по Id отвечает фабрика, с фаричным методом в придачу, и не забываем что у нас абстрактный репозиторий, с unit of work который обернул репозиторий EF (Db context). Время занимает просто понять как сработает код, не то чтобы ошибку искать. А че? зато заменяемо.
Я не говорил что абстракции — сложно, я говорю что их часто не оправдано много. А 4+ вложеностей абстакций с паарой реализаций, очень удобно дебажить, ведь так?
А какая разница? F11 все равно приведет в ту, которая сейчас используется.
Я рассказывал про код приложения где использовалась N-layer, это был проект который писало 7 разрабов.
Ну это само по себе не показатель, тут возможны несколько вариантов, например:
- Разработчики были не очень хорошие, и они нагородили неправильных абстракций.
- Разработчики были нормальные, просто они игрались с разными подходами.
- Разработчики были нормальные, и они все сделали правильно, но вам не хватило опыта (времени, желания etc) осознать, что это была необходимая сложность.
Но вполне вероятно, что эта была смесь из вышеперечисленного.
А 4+ вложеностей абстакций с паарой реализаций, очень удобно дебажить, ведь так?
Да вроде не так уж и сложно. Вот асинхронный код на каких-нибудь event'ах – да, неудобно, но к количеству абстракций это отношения не имеет, его всегда неудобно дебажить (но мы его любим не за это).
я говорю что их часто не оправдано много.
Таки да. Но вот если бы статья была в духе: "Не надо в простых проектах городить сложные абстракции. Можно сделать просто и понятно – вот так, например" – и показали бы поддерживаемый и легко читаемый код, то было бы просто отлично. Но я этого в статье, к сожалению, не увидел.
Если функции нужен баланс юзера, то МЫ достаем баланс, и передаем в функцию, а НЕ пихаем сервис юзеров в BL.
Круто. Один простой вопрос: если у вас поменялись требования на валидацию (операции или сущности), и этой валидации нужно на один источник данных больше, в скольких местах придется поменять код?
Скажем (пусть это и не валидация, зато прямо в коде пример есть), операции user.MakeToken()
понадобился CRNG. Что происходит дальше?
В этом классе собраны все возможные ошибки приложения, на которые реагирует exception handler
… а не на уровне фронта у вас ошибок не бывает?
(кстати, счастливой отладки вам при таком "логгировании")
могу легко воспроизвести баг, повторив запрос, ведь у моего API нет состояния, и каждый запрос зависит только от параметров запроса.
Это, кстати, неправда.
Это не то, что я спросил.
Повторю вопрос еще раз: если вашей "чистой функции" становятся нужны дополнительные данные, в скольких местах надо менять код?
Бинго. Во всех местах использования, а если те — тоже "чистые функции", то во всех местах их использования, и так дальше по цепочке. Хотя, казалось бы, изменение-то локальное, на внешний контракт влиять не должно, и вот это вот всё. А как же принцип единой ответственности (точнее, единственной причины изменения)? А как же инкапсуляция?
… а не на уровне фронта у вас ошибок не бывает?
Обратите внимание что я возвращаю не только текст, а и код ошибки.
Текст нужен разрабу, код для справочника ошибок. Фронт по коду достает из словаря (eng ru) текст и дает внятный ответ ошибки
А толку? Как вам это поможет разобраться с конкретной ошибкой в беке? Стектрейс, оригинальная ошибка — это все вам не нужно?
Да, это я пропустил (что, впрочем, говорит о качестве вашего кода). Но вы все равно теряете кучу информации (включая собственные свойства ошибки, оригинальную ошибку и все, что ее касалось, контекст операции, блаблабла). И я надеюсь, что тот логгер, который у вас там вбрасывается, по крайней мере пишет корреляционные идентификаторы, иначе понять, чем вызвана ошибка, которую вам "в телеграм прислали", будет вовсе невозможно.
Потому что они дают нечитаемую ошибку (особенно Single
). Я в свое время просто написал несколько хелперов, которые бросали нужный мне эксепшн, тогда стало легче.
2) Выглядит грамоздко
3) Хотелось чтоб ошибки были собственного типа (AppError) для удобной обработки.
но файлов было так много, что просто кошмар
Так а простите, куда вы будете девать свои файлы, когда проект разрастётся?
Я вынес методы как методы расширения дабы класс не раздувался, а функционал можно группировать по фичам
Ага, а дальше будет этап когда фичи начнут раздуваться и в каждый метод вынесете в отдельный класс и придёте к CQRS.
Вы немного лукавите предоставив только тут одну простую сущность «пользователь». Но вот представьте, например, что вашему пользователю придётся играть в три игры рулетку, бандита и блекджек. Потом выяснится, что в рулетку играют в 5 раз чаще чем в блекджек, а в бандита раз в день и то не факт. Потом захочется показывать аналитику по всем трём играм. Потом захочется видеть аналитику по всем пользователям и тд.
Жду ещё одну статью как вы всё это без моков масштабируете
Как я и говорил тестировать нужно только логику...
Ну, допустим, что так.
Есть места где я ещё точно не знаю могу ли я вообще работать с юзером, возможно у него банально нет денег для этого действия...
Но ведь это же и есть самая настоящая бизнес-логика. Но тестами оно у вас окажется не покрыто.
if (user != null) throw ...
тестировать уже не нужно?Далее, это веб -> параллелизм, что часто забывают начинающие разработчики, и управление им невозможно спихнуть на кого-то еще. Где у вас расположиться транзакция? Но предположим, вы полагаетесь на уникальный индекс, зачем тогда эта проверка?
В целом, похвальная попытка, но когда вы испытаете боль от изменений в вашей реализации, на что вам пытаются намекнуть в комментариях, у вас появится лучшее понимание назначения DDD, SOLID, dependency inversion и пр.
Проверка нужна в случае если id нет, очевидно же
А вы в курсе, что если вызвать параллельно несколько раз метод play, то у вас база придет в неконсистентное состояние? У вас баланс сохраненный в wallet начнет отличаться от суммы транзакций.
Это тестировалось в 50 потоков.
Код приведёте?
Пропустил этот метод. Есть уверенность что UtcNow всегда будет разным? А если будет больше одного сервера, на котором работает приложение?
Довольно странно выглядит такой вариант “optimistic concurrency”, там где она не нужна. Добавление транзакции в список можно делать параллельно.
В PoEAA такое структурирование кода называется Transaction Script. И очень зря вы использовали монгу, так как транзакции она как раз не поддерживает. В этом случае вам как раз-таки стоило бы использовать её функции, типа "найти запись и увеличить значение на 10", чтобы код работал корректно. Но тогда архитектура получилась бы не такой.
У вас вся бизнес-логика это всего одна функция, которая представлена в этом примере. Причем эта функция работает всего с одной сущностью, точнее двумя master-detail, то этот код можно было было одинаково "красиво" написать и в виде DDD, и в виде CQRS, и в виде вообще любого подхода к структурированию кода.
Часть, связанная с вынесением обработки ошибок и логирования отдельно называется Aspect-Oriented Programming, сокращенно AOP. Это когда фреймворк, в данном случае ASP.NET Core берет на себя работ по вставке вызовов "водопроводного" кода, отчищая от него бизнес-логику. Чтобы в итоге бизнес-логика не обращалась к логгерам, обработке ошибок итд.
Это все известно уже лет двадцать, активно применяется более десяти.
Но, к сожалению, большинство приложений работает с базой сложнее чем "взять-по-ИД" и "записать-по-ИД", поэтому реализация БЛ в виде чистой функции гораздо сложнее, чем в этом примере, и в тестах становятся нужны моки репозитариев и бд, чтобы в тестах можно было отследить какие изменения попадают в базу.
Кстати.
Покажите еще метод, который вам вернет сколько заработала ваша система за период. Он скорее всего у вас есть.
Пока что такая
А вот это часть какого слоя, кстати? Это же бизнес-логика, или нет?
То есть у вас бизнес-логика может находиться как в бизнес-слое, так и в слое сервисов, в зависимости от того, за сколько запросов она выполняется? А проверка "есть ли у пользователя деньги", которая тоже является частью бизнес-логики, и вовсе в контроллере лежит?
Исправил
Теперь у вас два метода, из которых один в слое бизнес-логики, а второй где?
Ну и да, у вас юзер всегда при загрузке из БД приносит все свои транзакции?
Всегда.
Второй в Core.
А что такое Core, и чем это отличается от бизнес-логики?
Всегда.
Какая прекрасная, должно быть, производительность у операции "выведи список имен пользователей", когда у каждого пользователя накопилось по несколько тысяч транзакций.
У вас все равно она хуже, чем моя.
Можете сами убедиться
crypto-games.space
У вас все равно она хуже, чем моя.
Это весьма бессмысленное утверждение.
Но не суть. Что такое Core, и чем это отличается от бизнес-логики?
Ну то есть из двух приведенных выше функций обе лежат в слое бизнес-логики?
Одна в сервисах, одна в бл
Ну то есть ничего не изменилось: есть две операции, обе отвечают за бизнес-логику, одна лежит в одном слое, другая — в другом. И, как говорилось выше, есть еще и куски бизнес-логики в контроллерах.
Проще говоря, в вашей "чистой" архитектуре бизнес-логика размазана по всем слоям, которые у вас вообще есть.
Такая же бизнес-логика, не хуже других.
Вы по сути сделали то, о чем писали в начале статьи.
У вас несколько слоёв, большая часть из них вызывает функции низлежащего слоя и прикидывает результат наверх. Нет Единого места где сосредоточена логика нужная бизнесу. Она и в сервисах, и в экстеншенах, и в контроллере, и в методе update.
Чистая архитектура решения, тесты без моков и как я к этому пришел