Хабрахабр

[Из песочницы] Чистая архитектура решения, тесты без моков и как я к этому пришел

В этой статье я хочу рассказать об архитектуре своего проекта, который я рефакторил 4 раза на его старте, так как не был удовлетворен результатом. Здравствуйте, дорогие читатели! Расскажу о минусах популярных подходов и покажу свой.

Я лишь хочу показать что у меня получилось, рассказать как я дошел до конечного результата и самое главное — получить мнения других. Сразу хочу сказать что это моя первая статья, я не говорю что делать как я — правильно.

Я работал в нескольких кампаниях и видел кучу всего что я б сделал по другому.

Вроде удобно, но посмотрев на код вижу такую ситуацию: К примеру, часто вижу N-Слойную архитектуру, есть слой работы с данными (DA), есть слой с бизнес логикой (BL), который работает используя DA и возможно ещё какие-то сервисы, а так же есть слой вьюшки\API в котором принимается запрос, обрабатывается используя BL.

  • [DA] вытягивает\записывает\меняет данные, пусть даже сложный запрос — OK
  • [BL] 80% вызывает 1 метод и прокидывает результат выше — Зачем этот пустой слой?
  • [View] 80% Вызывает 1 метод BL прокидывает результат выше — Зачем этот пустой слой?

Кроме этого, модно оборачивать в интерфейсы чтоб потом замокать и тестить — вау, просто вау!

  • А зачем мокать?
  • Ну, чтоб выпилить сайд эффекты на время тестов.
  • То-есть протестим без сайд-эффктов, а в прод с ними?
    ...

Это основанная вещь которая мне не нравилась в этой архитектуре, так как чтоб решить задачу по типу: "Вывести список лайков пользователя" это большой процесс, а на деле 1 запрос в БД и возможно маппинг.

Примерное решение

1) [DA] Добавить запрос в DA
2) [BL] Пробросить ответ DA
3) [View] Пробросить результат BA, может промаппить

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

В другом месте я видел реализацию API с подходом CQRS.

Разработчик делающий фичу сидит в своей папке и практически всегда может забыть про влияние своего кода на другие фичи, но файлов было так много, что просто кошмар. Решение выглядело не плохо, 1 папка — 1 фича. Поиск в студии практически отказывался работать, ставились расширения для поиска нужных вещей в коде. Модели запросов\ответов, валидаторы, хелперы, сама логика.

Ещё много чего можно рассказать, но я выделил основные причины которые заставили меня от этого отказаться

И наконец к моему проекту

Наткнулся на доклады Антона Молдована про DDD и функциональное программирование, и подумал: "Вот оно, мне нужен F#!". Как я говорил, я рефакторил свой проект несколько раз, в тот момент у меня была "депрессия программиста", я просто был не доволен своим кодом, и рефакторил его, снова и снова, в итоге начал смотреть видео про архитектуру приложения, чтоб увидеть как делают другие.

В видео показывали: Потратив пару дней на F# я понял что в принципе тоже-самое сделаю на C# и не хуже.

  • Вот код C#, он говно
  • Вот F# классный, меньше написал — супер.

Главный принцип был в том, что BL это не штука которая вызывает DA, сервисы и делает всю работу, а это чистая функция. Но прикол в том что решение на F# реализовали по другому, и против этого показывали плохую реализацию на C#.

Конечно F# хорош, мне понравились некие фичи но, как и C# это всего лишь инструмент, который можно использовать по-разному.

И я снова вернулся к C# и начал творить.

Создал я такие проекты в решении:

  1. API
  2. Core
  3. Services
  4. Tests

Так же я использовал фичи C# 8, особенно nullable refence type, её применение покажу.
Коротко о задачах слоев, которые я им дал.

API
1) Получение запросов, модели запросов + валидация, ограничения

Подробнее

image

2) Вызов функций из Core и Services

Подробнее

image

Тут мы видим простой, читабельный код, я думаю каждый поймет что тут написано.
Наблюдается четкий шаблон
1) Достать данные
2) Обработать, изменить и тд — Именно эту часть нужно тестировать.
3) Сохранить.

3) Маппинг, если нужен
4) Обработка ошибок (логирование + человеческий ответ)

Подробнее

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

image

image

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

Bug эта ошибка на не понятный случай. Есть у меня AppError.

У меня есть CallBack от другого сервиса, в нем будет userId в моей системе, и если я не найду юзера с этим ID значит либо с юзером что-то случилось, либо вообще не понятно, такая ошибка летит мне как CRITICAL, по идее не должна возникать, но если возникнет, то требует моё вмешательство.

image

Core, самое интересное

Сложность кода в этом слое была на уровне лабораторной работы, не большие функции, которые четко и без ошибок делают свою работу. Я всегда держал в голове, что BL это просто функции которые при одинаковом входе дают одинаковый результат. И важно было чтоб внутри функций не было сайд эффектов, все что нужно функции заходит ей параметром.

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

1) Основные действия сущностей

Подробнее

image
image

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

image
image

Не менее важной темой считаю хорошее построение моделей сущностей.

Одно из типичных решений которое я взял не задумываясь это сущность "Баланс" и просто засунуть массив балансов в юзера. Вот к примеру, у меня есть юзер, у юзера есть балансы в нескольких валютах. Но какие не удобности принесло такое решение?

Эта задача сразу означает для нас не только написание нового кода, а и миграция, с наполнением\удалением всех существующих пользователей и это самый просто вариант. 1) Добавление\удаление валюты. В итоге нужно было всего enum расширить для новой валюты, а написали ещё фичу по созданию кошельков по кнопке, ещё задачу фронту кинули. Не дай бог, чтоб добавить новую валюту пришлось бы делать кнопку для юзера, которую он нажмет и инициирует создание нового кошелька по какому-то бизнес процессу.

Currency == currency) и проверка на null 2) В коде постоянные FirstOrDefault(s=> s.

Моё решение

image

Самой моделью я гарантирую себе, что баланс будет и никаких null, а создав оператор indexer я упростил себе код во всех местах взаимодействия с балансом.

Services

Этот слой предоставляет мне удобные иструмены для работы с различными сервисами.
В своем проекте я использую MongoDB и для удобной работы с ней, обернул колекции в такой себе репозиторий.

Подробнее

Сам репозиторий
image

А ещё в монге есть методы для поиска сущности + действию над ней, например: "Найти юзера с id и добавить к его текущему балансу 10" Монга блокирует документ на момент работы с ним, соответсвенно это поможет нам с решение проблем в конкуренции запросов.

А теперь про фичу C# 8.

image

image

Я сразу получаю предупреждение компилятора, и делаю проверку на null. Сигнатура метода мне говорит, что может вернутся User, а может Null, соответственно когда я вижу User?

image

Когда метод возвращает User я уверенно с ним работаю.

image

В слое API тоже нет try catch, есть только один глобальный exception handler. Ещё хочу обратить внимание на то, что нет try catch потому как исключения могут быть только от "странных ситуаций" и не верных данных, которые сюда доходить не должны так как есть валидация.

Есть только один метод который бросит Exception это метод Update.
В нем реализована защита от потери данных при многопоточном режиме.
image

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

image

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

А вот сама модель юзера, тут четко видно что реферал у юзера не обязательный, а со всем остальным можно работать не думая о null.

image

И наконец Tests

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

Подробнее

Я скачал nuget FSCheck который генерит рандомно входящие данные и позволяет проводить много разных кейсов.

Мне лишь нужно создавать различных юзеров, кормить их тесту и проверять изменения.

Для создания таких юзеров есть билдер, пока что маленький, но его легко расширять.

image

А вот и сами тесты

image

image

image

После каких-то изменения я запускаю тесты, через 1-2 секунду вижу что все в порядке.
Так же в планах написать E2E тесты, дабы проверять всю API из вне и быть уверенным что она работает так как нужно, от запроса, до ответа.

Фишки

Прикольные вещи, которые могут понадобиться

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

image

Подведем итог.

Мы сделали обработку ошибок в одном месте и те должны возникать очень редко. Мы написали действительно решение, а не фреймворк в котором куча лишних абстракций, а так же моков. Мы не писали лишних функций которые просто пробрасывают вызов других функций. Мы отделили BL и сайд эффекты, теперь BL это просто локальная логика, которую можно переиспользовать. Буду активно читать комментарии и дополнять статью, спасибо!

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть