Чистая Архитектура Решения, Тесты Без Моков И Как Я К Этому Пришел

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

Расскажу о недостатках популярных подходов и покажу свой.

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

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

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

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

  • [DA] извлекает\записывает\изменяет данные, даже сложный запрос - ОК
  • [BL] 80% вызывает 1 метод и выкидывает результат выше - Почему этот пустой слой?
  • [Просмотр] 80% Вызывает 1 метод. BL выдает результат выше.

    Почему этот пустой слой?

К тому же это модно оборачивать в интерфейсы, чтобы потом тестировать и тестировать — ух, просто ух!
  • Зачем намокать?
  • Ну и чтобы исключить побочные эффекты при испытаниях.

  • То есть протестовать будем без побочных эффектов, а в производстве с ними? .

Это главное, что мне не понравилось в этой архитектуре, потому что решить задачу типа: «Отобразить список лайков пользователя» — это большой процесс, но реально в базе 1 запрос и маппинг возможен.

Примерное решение 1) [DA] Добавить запрос в DA 2) [BL] Переадресация ответа DA 3) [Просмотр] Переслать результат BA, можно сопоставить Не забывайте, что все эти методы еще нужно добавить в интерфейс; мы пишем проект ради издевательства, а не ради решения.

В другом месте я видел реализацию API с подходом CQRS. Решение выглядело не плохо, 1 папка — 1 функция.

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

Модели запросов/ответов, валидаторы, помощники, сама логика.

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

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



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

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

На видео было показано:

  • Вот код C#, это херня
  • F# великолепен, я написал меньше – супер.

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

Основной принцип был в том, что БЛ - это не вещь, которая звонит ДА, обслуживает и делает всю работу, а это чистая функция .

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

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

В решении я создал следующие проекты:

  1. API
  2. Основной
  3. Услуги
  4. Тесты
Я также использовал возможности C# 8, особенно ссылочный тип, допускающий значение NULL, я покажу его использование.

Коротко о задачах слоев, которые я им дал.

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

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

2) Вызов функций из ядра и сервисов Подробнее

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

Здесь мы видим простой, читаемый код, думаю, все поймут, что здесь написано.

Наблюдается четкая закономерность 1) Получить данные 2) Процесс, изменение и т. д. — это та часть, которую необходимо протестировать.

3) Сохранить.

3) Картирование, если необходимо.

4) Обработка ошибок (ведение журнала + реакция человека) Подробнее Этот класс содержит все возможные ошибки приложения, на которые реагирует обработчик исключений.



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



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

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

У меня AppError.Bug эта ошибка по неясному случаю.

У меня есть CallBack от другого сервиса, он будет содержать userId в моей системе, и если я не найду пользователя с этим ID, то значит либо с пользователем что-то случилось, либо вообще непонятно, вылетает такая ошибка для меня КРИТИЧЕСКОЕ, по идее оно не должно возникать, но если оно возникает, то требует моего вмешательства.



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

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

И было важно, чтобы внутри функций не было побочных эффектов; все, что нужно функции, входило в нее в качестве параметра.

Если функции нужен баланс пользователя, то МЫ получаем баланс и передаем его функции, а НЕ помещаем пользовательский сервис в BL. 1) Основные действия сущностей Подробнее

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



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

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



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



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

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

Например, у меня есть пользователь, у пользователя есть балансы в нескольких валютах.

Одним из типичных решений, которые я принял, не задумываясь, была сущность «Баланс» и просто поместил в пользователя массив балансов.

Но какие неудобства принесло такое решение? 1) Добавить\удалить валюту.

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

В итоге всё, что нужно было — это расширить enum для новой валюты, но ещё написали фичу по созданию кошельков с помощью кнопки, а ещё кинули задачу во фронт. 2) В коде есть константы FirstOrDefault(s=> s.Currency == валюта) и проверка на ноль Мое решение

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

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



Услуги

Этот слой предоставляет мне удобные инструменты для работы с различными сервисами.

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

Подробнее Сам репозиторий

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

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

А еще в Монге есть методы поиска сущности + действие над ней, например: «Найти пользователя по id и добавить 10 к его текущему балансу» А теперь о возможности C# 8.

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



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

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

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

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



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

Также хочу обратить ваше внимание на то, что здесь нет try catch, потому что исключения могут быть только из «странных ситуаций» и неверных данных, которые сюда не должны попадать, так как есть валидация.

На уровне API также нет try-catch, есть только один глобальный обработчик исключений.

Существует только один метод, который генерирует исключение, — это метод Update. Реализована защита от потери данных в многопоточном режиме.



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

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



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

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

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



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



И наконец тесты

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

Поэтому мы можем запускать наши тесты очень быстро и с разными параметрами.

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

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

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



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

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

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



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



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

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

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



Чипсы

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



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

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

Мы на самом деле написали решение, а не фреймворк, в котором много ненужных абстракций, а также моков.

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

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

Мы не писали ненужные функции, которые просто пересылают вызовы другим функциям.

Буду активно читать комментарии и дополнять статью, спасибо! Теги: #api #C++ #.

NET #архитектура приложения #Идеальный код #хороший код #asp.net core #веб-приложения #ASP #ASP

Вместе с данным постом часто просматривают: