Здравствуйте, дорогие читатели! В этой статье я хочу рассказать об архитектуре моего проекта, который я рефакторил 4 раза на старте, потому что результат меня не удовлетворил.
Расскажу о недостатках популярных подходов и покажу свой.
Сразу хочу сказать, что это моя первая статья, я не говорю, что поступать так, как я, правильно.
Я просто хочу показать, что я сделал, рассказать, как я пришел к конечному результату, и самое главное узнать мнение других.
Я работал над несколькими кампаниями и видел массу вещей, которые я бы сделал по-другому.
Например, я часто вижу архитектуру N-Layer, есть уровень обработки данных (DA), есть уровень с бизнес-логикой (BL), который работает с использованием DA и возможно еще каких-то сервисов, а еще есть представление\ Уровень API, на котором запрос принимается и обрабатывается с использованием BL. Вроде удобно, но глядя на код вижу следующую ситуацию:
- [DA] извлекает\записывает\изменяет данные, даже сложный запрос - ОК
- [BL] 80% вызывает 1 метод и выкидывает результат выше - Почему этот пустой слой?
- [Просмотр] 80% Вызывает 1 метод. BL выдает результат выше.
Почему этот пустой слой?
- Зачем намокать?
- Ну и чтобы исключить побочные эффекты при испытаниях.
- То есть протестовать будем без побочных эффектов, а в производстве с ними?
.
Примерное решение 1) [DA] Добавить запрос в DA 2) [BL] Переадресация ответа DA 3) [Просмотр] Переслать результат BA, можно сопоставить Не забывайте, что все эти методы еще нужно добавить в интерфейс; мы пишем проект ради издевательства, а не ради решения.
В другом месте я видел реализацию API с подходом CQRS. Решение выглядело не плохо, 1 папка — 1 функция.
Разработчик, делающий фичу, сидит в своей папке и почти всегда может забыть о влиянии своего кода на другие фичи, но файлов было так много, что это был просто кошмар.
Модели запросов/ответов, валидаторы, помощники, сама логика.
Поиск в студии практически отказался работать, были установлены расширения для поиска нужного в коде.
Можно еще много чего сказать, но я выделил основные причины, которые заставили меня отказаться от этого.
И наконец мой проект
Как я уже говорил, я несколько раз рефакторил свой проект, в тот момент у меня была «программистская депрессия», я был просто недоволен своим кодом, и рефакторил его снова и снова, в конце концов я начал смотреть видео об архитектуре приложения, чтобы увидеть, как другие делают. Наткнулся на доклады Антона Молдавана по DDD и функциональному программированию и подумал: «Вот оно, мне нужен F#!» Потратив пару дней на F#, я понял, что в принципе на C# сделаю то же самое и не хуже.На видео было показано:
- Вот код C#, это херня
- F# великолепен, я написал меньше – супер.
Основной принцип был в том, что БЛ - это не вещь, которая звонит ДА, обслуживает и делает всю работу, а это чистая функция .
Конечно, F# хорош, некоторые функции мне понравились, но, как и C#, это просто инструмент, который можно использовать по-разному.
И я снова вернулся к C# и начал творить.
В решении я создал следующие проекты:
- API
- Основной
- Услуги
- Тесты
Коротко о задачах слоев, которые я им дал.
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
-
Надежный Формирователь Трафика Antamedia
19 Oct, 24 -
Мур, Джордж Эдвард
19 Oct, 24 -
Что Произошло В Мире Финансов На 14 Неделе
19 Oct, 24