архитектура веб приложений javascript
С орбитальной высоты
Этот раздел описывает набор современных стандартов для «веб-компонентов».
На текущий момент, эти стандарты находятся в процессе разработки. Некоторые фичи имеют хорошую поддержку и интеграцию в современный стандарт HTML/DOM, в то время как другие пока ещё в черновиках. Вы можете попробовать примеры в любом современном браузере (Google Chrome, скорее всего, имеет наиболее полную поддержку, так как ребята из Google стоят за большинством спецификаций по этой теме).
Что общего между…
В идее самостоятельного компонента нет ничего нового. Такой подход используется во многих фреймворках.
Прежде чем мы погрузимся в детали реализации, взгляните на это великое достижение человечества:
Это международная космическая станция (МКС).
А это то, как она устроена (приблизительно):
Международная космическая станция:
…И эта штука летает, поддерживая жизни людей в космосе!
Как создаются столь сложные устройства?
Какие принципы мы могли бы позаимствовать, чтобы сделать нашу разработку такой же надёжной и масштабируемой? Или, по крайней мере, приблизиться к такому уровню.
Компонентная архитектура
Хорошо известное правило разработки сложного программного обеспечения гласит: не создавай сложное программное обеспечение.
Если что-то становится сложным – раздели это на более простые части и соедини наиболее очевидным способом.
Хороший архитектор – это тот, кто может сделать сложное простым.
Мы можем разделить пользовательский интерфейс на визуальные компоненты: каждый из них занимает своё место на странице, выполняет определённую задачу, и отделен от остальных.
Рассмотрим какой-нибудь сайт, например Twitter.
Он естественным образом разделён на компоненты:
Компоненты могут содержать подкомпоненты, например сообщения могут быть частями родительского компонента «список сообщений». Кликабельное фото пользователя может быть самостоятельным компонентом и т.д.
Как мы определяем, что является компонентом? Это приходит из соображений здравого смысла, а также с интуицией и опытом. Обычно это объект, отделимый визуально, который мы можем описать с точки зрения того, что он делает и как он взаимодействует со страницей. В примере выше, страница содержит блоки, каждый из которых играет свою роль, и логично выделить их в компоненты.
Ещё раз заметим, в компонентном подходе как таковом нет ничего особенного.
Существует множество фреймворков и методов разработки для их создания, каждый из которых со своими плюсами и минусами. Обычно особые CSS классы и соглашения используются для эмуляции компонентов – области видимости CSS и инкапсуляция DOM.
«Веб-компоненты» предоставляют встроенные возможности браузера для этого, поэтому нам больше не нужно эмулировать их.
В следующей главе мы погрузимся в «пользовательские элементы» – фундаментальную для веб-компонентов технологию, имеющую хорошую поддержку в браузерах.
Архитектура современных корпоративных Node.js-приложений
Ох, не зря в названии намёк на нетленку Фаулера. И когда фронтенд-приложения успели стать настолько сложными, что мы начали рассуждать о высоких материях? Node.js… фронтенд… погодите, но Нода же на сервере, это бэкенд, а там ребята и так всё знают!
Давайте по порядку. И сразу небольшой дисклеймер: статья написана по мотивам моего выступления на Я.Субботнике Pro для фронтенд-разработчиков. Если вы занимаетесь бэкендом, то, возможно, ничего нового для себя не откроете. Здесь я попробую обобщить свой опыт фронтендера в крупном энтерпрайзе, объяснить, почему и как мы используем Node.js.
Определимся, что мы в рамках этой статьи будем считать фронтендом. Оставим в стороне споры о задачах и сконцентрируемся на сути.
Фронтенд — часть приложения, отвечающая за отображение. Он может быть разным: браузерным, десктопным, мобильным. Но всегда остаётся важная черта — фронтенду нужны данные. Без бэкенда, который эти данные предоставит, он бесполезен. Здесь и проходит достаточно чёткая граница. Бэкенд умеет ходить в базы данных, применять к полученным данным бизнес-правила и отдавать результат фронтенду, который данные примет, шаблонизирует и выдаст красоту пользователю.
Можно сказать, что концептуально бэкенд нужен фронтенду для получения и сохранения данных. Пример: типичный современный сайт с клиент-серверной архитектурой. Клиент в браузере (назвать его тонким язык уже не повернётся) стучится на сервер, где крутится бэкенд. И, конечно, везде есть исключения. Есть сложные браузерные приложения, которым не нужен сервер (этот случай мы не будем рассматривать), и есть необходимость исполнения фронтенда на сервере — то, что называется Server Side Rendering или SSR. Давайте с него и начнём, потому что это самый простой и понятный случай.
Идеальный мир для бэкенда выглядит примерно так: на вход приложения поступают HTTP-запросы с данными, на выходе мы имеем ответ с новыми данными в удобном формате. Например, JSON. HTTP API легко тестировать, понятно, как разрабатывать. Однако жизнь вносит коррективы: иногда одного API недостаточно.
Сервер должен отвечать готовым HTML, чтобы скормить его краулеру поисковой системы, отдать превью с метатегами для вставки в социальную сеть или, что ещё важнее, ускорить ответ на слабых устройствах. Совсем как в древние времена, когда мы разрабатывали Web 2.0 на PHP.
Всё знакомо и давно описано, но клиент изменился — в него пришли императивные клиентские шаблонизаторы. В современном вебе бал правит JSX, о плюсах и минусах которого можно рассуждать долго, вот только одно отрицать нельзя — в серверном рендеринге не обойтись без JavaScript-кода.
Получается, когда нужна реализация SSR силами бэкенд-разработки:
Так мы уже немножко пустили фронтенд-разработчиков на сервер. Давайте перейдём к более важной проблеме.
Получение данных
Популярное решение — создание универсальных API. Эту роль чаще всего берёт на себя API Gateway, умеющий опрашивать множество микросервисов. Однако здесь тоже возникают проблемы.
Во-первых, проблема команд и зон ответственности. Современное большое приложение разрабатывают множество команд. Каждая команда сконцентрирована на своём бизнес-домене, имеет свой микросервис (или даже несколько) на бэкенде и свои отображения на клиенте. Не будем вдаваться в проблему микрофронтендов и модульности, это отдельная сложная тема. Предположим, что клиентские отображения полностью разделены и являются мини-SPA (Single Page Application) в рамках одного большого сайта.
В каждой команде есть фронтенд и бэкенд-разработчики. Каждый работает над своим приложением. API Gateway может стать камнем преткновения. Кто за него отвечает? Кто будет добавлять новые эндпоинты? Отдельная суперкоманда API, которая будет вечно занята, решая проблемы всех остальных на проекте? Какова будет цена ошибки? Падение этого шлюза положит всю систему целиком.
Во-вторых, проблема избыточных/недостаточных данных. Давайте посмотрим, что происходит, когда два разных фронтенда используют один универсальный API.
Эти два фронтенда сильно отличаются. Им нужны разные наборы данных, у них разный релизный цикл. Вариативность версий мобильного фронтенда максимальна, поэтому мы вынуждены проектировать API с максимальной обратной совместимостью. Вариативность веб-клиента низка, фактически мы должны поддерживать только одну предыдущую версию, чтобы снизить количество ошибок в момент релиза. Но даже если «универсальный» API обслуживает только веб-клиентов, мы всё равно столкнёмся с проблемой избыточных или недостаточных данных.
Каждому отображению требуется отдельный набор данных, вытащить который желательно одним оптимальным запросом.
В таком случае нам не подойдёт универсальный API, придётся разделить интерфейсы. Значит, потребуется свой API Gateway под каждый фронтенд. Слово «каждый» здесь обозначает уникальное отображение, работающее со своим набором данных.
Мы можем поручить создание такого API бэкенд-разработчику, которому придётся работать с фронтендером и реализовывать его хотелки, либо, что гораздо интереснее и во многом эффективнее, отдать реализацию API команде фронтенда. Это снимет головную боль из-за реализации SSR: уже не нужно ставить прослойку, которая стучится в API, всё будет интегрировано в одно серверное приложение. К тому же, контролируя SSR, мы можем положить все необходимые первичные данные на страницу в момент рендера, не делая дополнительных запросов на сервер.
Такая архитектура называется Backend For Frontend или BFF. Идея проста: на сервере появляется новое приложение, которое слушает запросы клиента, опрашивает бэкенды и возвращает оптимальный ответ. И, конечно же, это приложение контролирует фронтенд-разработчик.
Больше одного сервера в бэкенде? Не проблема!
Независимо от того, какой протокол общения предпочитает бэкенд-разработка, мы можем использовать любой удобный способ общения с веб-клиентом. REST, RPC, GraphQL — выбираем сами.
Но разве GraphQL сам по себе не является решением проблемы получения данных одним запросом? Может, не нужно никакие промежуточные сервисы городить?
К сожалению, эффективная работа с GraphQL невозможна без тесного сотрудничества с бэкендерами, которые берут на себя разработку эффективных запросов к базе данных. Выбрав такое решение, мы снова потеряем контроль над данными и вернёмся к тому, с чего начинали.
Можно, конечно, но неинтересно (для фронтендера)
Что же, давайте реализовывать BFF. Конечно, на Node.js. Почему? Нам нужен единый язык на клиенте и сервере для переиспользования опыта фронтенд-разработчиков и JavaScript для работы с шаблонами. А как насчёт других сред исполнения?
GraalVM и прочие экзотические решения проигрывают V8 в производительности и слишком специфичны. Deno пока остаётся экспериментом и не используется в продакшене.
И ещё один момент. Node.js — удивительно хорошее решение для реализации API Gateway. Архитектура Ноды позволяет использовать однопоточный интерпретатор JavaScript, объединённый с libuv, библиотекой асинхронного I/O, которая, в свою очередь, использует тред-пул.
Долгие вычисления на стороне JavaScript бьют по производительности системы. Обойти это можно: запускать их в отдельных воркерах или уносить на уровень нативных бинарных модулей.
Но в базовом случае Node.js не подходит для операций, нагружающих CPU, и в то же время отлично работает с асинхронным вводом/выводом, обеспечивая высокую производительность. То есть мы получаем систему, которая сможет всегда быстро отвечать пользователю, независимо от того, насколько нагружен вычислениями бэкенд. Обработать эту ситуацию можно, мгновенно уведомляя пользователя о необходимости подождать окончания операции.
Где хранить бизнес-логику
В нашей системе теперь три большие части: бэкенд, фронтенд и BFF между ними. Возникает резонный (для архитектора) вопрос: где же держать бизнес-логику?
Конечно, архитектор не хочет размазывать бизнес-правила по всем слоям системы, источник правды должен быть один. И этот источник — бэкенд. Где ещё хранить высокоуровневые политики, как не в наиболее близкой к данным части системы?
Но в реальности это не всегда работает. Например, приходит бизнес-задача, которую можно эффективно и быстро реализовать на уровне BFF. Идеальный дизайн системы это классно, но время — деньги. Иногда приходится жертвовать чистотой архитектуры, а слои начинают протекать.
Можем ли мы получить идеальную архитектуру, отказавшись от BFF в пользу «полноценного» бэкенда на Node.js? Кажется, в этом случае не будет протечек.
Не факт. Найдутся бизнес-правила, перенос которых на сервер ударит по отзывчивости интерфейса. Можно до последнего сопротивляться этому, но избежать полностью, скорее всего, не получится. Логика уровня приложения тоже проникнет на клиент: в современных SPA она размазана между клиентом и сервером даже в случае, когда есть BFF.
Как бы мы ни старались, бизнес-логика проникнет в API Gateway на Node.js. Зафиксируем этот вывод и перейдём к самому вкусному — имплементации!
Big Ball of Mud
Самое популярное решение для Node.js-приложений в последние годы — Express. Проверенное, но уж больно низкоуровневое и не предлагающее хороших архитектурных подходов. Основной паттерн — middleware. Типичное приложение на Express напоминает большой комок грязи (это не обзывательство, а антипаттерн).
Все слои перемешаны, в одном файле находится контроллер, где есть всё: инфраструктурная логика, валидация, бизнес-логика. Работать с этим больно, поддерживать такой код не хочется. А можем ли мы писать на Node.js код энтерпрайз-уровня?
Для этого требуется кодовая база, которую легко поддерживать и развивать. Иначе говоря, нужна архитектура.
Архитектура Node.js-приложения (наконец-то)
«Цель архитектуры программного обеспечения — уменьшить человеческие трудозатраты на создание и сопровождение системы».
Архитектура состоит из двух важных вещей: слоёв и связей между ними. Мы должны разбить наше приложение на слои, не допустить протечек из одного в другой, правильно организовать иерархию слоёв и связи между ними.
Как разбить приложение на слои? Есть классический трёхуровневый подход: данные, логика, представление.
Сейчас такой подход считается устаревшим. Проблема в том, что основой являются данные, а значит, приложение проектируется в зависимости от того, как данные представлены в БД, а не от того, в каких бизнес-процессах они участвуют.
Более современный подход предполагает, что в приложении выделен доменный слой, который работает с бизнес-логикой и является представлением реальных бизнес-процессов в коде. Однако если мы обратимся к классическому труду Эрика Эванса Domain-Driven Design, то обнаружим там такую схему слоёв приложения:
Что здесь не так? Казалось бы, основой приложения, спроектированного по DDD, должен быть домен — высокоуровневые политики, самая важная и ценная логика. Но под этим слоем лежит вся инфраструктура: слой доступа к данным (DAL), логирование, мониторинг, и т. д. То есть политики гораздо более низкого уровня и меньшей важности.
Инфраструктура оказывается в центре приложения, и банальная замена логгера может привести к перетряхиванию всей бизнес-логики.
Если мы снова обратимся к Роберту Мартину, то обнаружим, что в книге Clean Architecture он постулирует иную иерархию слоёв в приложении, с доменом в центре.
Соответственно, все четыре слоя должны располагаться иначе:
Мы выделили слои и определили их иерархию. Теперь перейдём к связям.
Связи
Вернёмся к примеру с вызовом логики пользователя. Как избавиться от прямой зависимости от инфраструктуры, чтобы обеспечить правильную иерархию слоёв? Есть простой и давно известный способ разворота зависимостей — интерфейсы.
Теперь высокоуровневый UserEntity не зависит от низкоуровневого Logger. Наоборот, он диктует контракт, который нужно реализовать, чтобы включить Logger в систему. Замена логгера в данном случае сводится к подключению новой реализации, соблюдающей тот же контракт. Важный вопрос — как её подключать?
Слои связаны жёстко. Есть завязка и на файловую структуру, и на реализацию. Нам нужна инверсия зависимости (Dependency Inversion), делать которую мы будем с помощью внедрения зависимости (Dependency Injection).
Теперь «доменный» UserEntity больше ничего не знает о реализации логгера. Он предоставляет контракт и ожидает, что реализация будет соответствовать этому контракту.
Конечно, ручная генерация экземпляров инфраструктурных сущностей дело не самое приятное. Нужен корневой файл, в котором мы будем всё подготавливать, придётся как-то протащить созданный экземпляр логгера через всё приложение (выгодно иметь один, а не создавать множество). Утомительно. И здесь вступают в игру IoC-контейнеры, которые могут взять на себя эту боллерплейтную работу.
Как может выглядеть использование контейнера? Например, так:
Что здесь происходит? Мы воспользовались магией декораторов и написали инструкцию: «При создании экземпляра UserEntity внедри в его приватное поле _logger экземпляр той сущности, что лежит в IoC-контейнере под токеном LOGGER. Ожидается, что она соответствует интерфейсу ILogger». А дальше IoC-контейнер сделает всё сам.
Мы выделили слои, определились с тем, как будем их развязывать. Пора выбрать фреймворк.
Фреймворки и архитектура
Вопрос простой: уйдя от Express на современный фреймворк, получим ли мы хорошую архитектуру? Давайте посмотрим на Nest:
Dependency Injection в Nest
Давайте попробуем сделать всё по инструкции. Так как в Nest термин Entity применяется обычно к ORM, переименуем UserEntity в UserService. Логгер поставляется фреймворком, поэтому вместо него заинжектируем абстрактный FooService.
И… кажется, мы сделали шаг назад! Инъекция есть, а инверсии нет, зависимость
направлена на реализацию, а не на абстракцию.
Давайте попробуем исправить. Вариант номер один:
Где-то рядом описываем и экспортируем этот абстрактный сервис:
FooService теперь использует AbstractFooService. В таком виде мы регистрируем его вручную в IoC.
Второй вариант. Пробуем описанный ранее подход с интерфейсами. Так как в JavaScript не существует интерфейсов, вытащить требуемую сущность из IoC в рантайме, воспользовавшись рефлексией, уже не получится. Мы должны явно указать, что нам нужно. Используем для этого декоратор @Inject.
И регистрируем по токену:
Победили фреймворк! Но какой ценой? Мы отключили довольно много сахара. Это подозрительно и наводит на мысль, что не стоит укладывать всё приложение во фреймворк. Если я вас ещё не убедил, есть и другие проблемы.
Исключения
Nest прошит исключениями. Более того, он предлагает использовать выбрасывание исключений для описания логики поведения приложения.
Всё ли тут в порядке с точки зрения архитектуры? Снова обратимся к корифеям:
«Если ошибка — это ожидаемое поведение, то вы не должны использовать исключения».
Мартин Фаулер
Исключения предполагают исключительную ситуацию. При написании бизнес-логики мы должны избегать выбрасывания исключений. Хотя бы по той причине, что ни JavaScript, ни TypeScript не дают гарантий, что исключение будет обработано. Более того, оно запутывает поток исполнения, мы начинаем программировать в GOTO-стиле, а значит, во время исследования поведения кода читателю придётся прыгать по всей программе.
Есть простое правило, помогающее понять, законно ли использование исключений:
«Будет ли код работать, если я удалю все обработчики исключений?» Если ответ «нет», то, возможно, исключения используются в неисключительных обстоятельствах».
The Pragmatic Programmer
Можно ли избежать этого в бизнес-логике? Да! Необходимо минимизировать выбрасывание исключений, а для удобного возврата результата сложных операций использовать монаду Either, которая предоставляет контейнер, находящийся в состоянии успеха или ошибки (концепция, очень близкая к Promise).
К сожалению, внутри предоставляемых Nest сущностей мы часто не можем действовать иначе — приходится выбрасывать исключения. Так устроен фреймворк, и это очень неприятная особенность. И снова возникает вопрос: может быть, не стоит прошивать приложение фреймворком? Может, получится развести фреймворк и бизнес-логику по разным архитектурным слоям?
Сущности Nest и архитектурные слои
Суровая правда жизни: всё, что мы пишем с помощью Nest, можно уложить в один слой. Это Application Layer.
Мы не хотим пускать фреймворк глубже в бизнес-логику, чтобы он не прорастал в неё своими исключениями, декораторами и IoC-контейнером. Авторы фреймворка будут раскатывать, как здорово писать бизнес-логику, используя его сахар, но их задача — навсегда привязать вас к себе. Помните, что фреймворк — лишь способ удобно организовать логику уровня приложения, подключить к нему инфраструктуру и UI.
«Фреймворк — это деталь».
Роберт «Дядя Боб» Мартин
Приложение лучше проектировать как конструктор, в котором легко заменить составные части. Один из примеров такой реализации — гексагональная архитектура (архитектура портов и адаптеров). Идея интересна: доменное ядро со всей бизнес-логикой предоставляет порты для общения с внешним миром. Всё, что нужно, подключается снаружи через адаптеры.
Чистая Архитектура для веб-приложений
Хочу поделиться с вами подходом который я уже много лет использую в разработке приложений, в том числе и веб-приложений. Многим разработчикам настольных, серверных и мобильных приложений этот подход хорошо знаком, т.к. является фундаментальным при построении таких приложений, однако в вебе он представлен очень скудно, хотя желающие использовать такой подход однозначно есть. Кроме того на таком подходе написан редактор VS Code.
В результате применения этого подхода вы отвяжетесь от конкретного фреймворка. Сможете легко переключать библиотеку представления внутри вашего приложения, например React, Preact, Vue, Mithril без переписывания бизнес логики, а в большинстве случаев даже вьюхи. Если у вас есть приложение на Angular 1, вы без проблем сможете перевести его на Angular 2+, React, Svelte, WebComponents или даже свою библиотеку представления. Если у вас есть приложение на Angular 2+, но нету специалистов для него, то вы без проблем сможете перевести приложение на более популярную библиотеку без переписывания бизнес логики. А в итоге вообще забыть про проблему миграции с фремворка на фреймворк. Что же это за магия такая?
Что такое Чистая Архитектура
Для того что бы понять это, лучше всего прочитать книгу Мартина Роберта «Чистая Архитектура» (Robert C.Martin «Clean Architecture»). Краткая выдержка из которого приведена в статье по ссылке.
Основные идеи заложенные в архитектуру:
Достигается такая гибкость за счет разделения приложения на слои Service, Repository, Model. Я же добавил к Чистой Архитектуре подход MVC и получил следующие слои:
Кому подойдет Чистая Архитектура
Веб разработка прошла большой путь развития, начиная от простого скриптования на jquery до разработки больших SPA приложений. И сейчас веб приложения стали настолько большими что количество бизнес логики стало сопоставимо или даже превосходить серверные, настольные и мобильные приложения.
Разработчикам которые пишут сложные и большие приложения, а также переносят бизнес логику с сервера на веб приложения для экономии на стоимости серверов, Чистая Архитектура поможет организовать код и отмасштабироваться без проблем до огромных масштабов.
В тоже время если ваша задача просто верстка и анимация лендингов, то Чистую Архитектуру просто некуда вставить. Если ваша бизнес логика находиться на бекенде и ваша задача получить данные, отобразить клиенту и обработать клик по кнопке, то вы не прочувствуете гибкости Чистой Архитектуры, но она может стать отличным плацдармом для взрывного роста приложения.
Где уже применяется?
Чистая архитектура не привязана к какому то конкретному фреймворку, платформе или языку программирования. Десятилетия ее используют для написания настольных приложений. Его эталонную реализацию можно найти во фреймворках для серверных приложений Asp.Net Core, Java Spring и NestJS. Так же она очень популярна при написании iOs и Android приложений. Но в веб разработке он предстал в крайне неудачном виде во фреймворках Angular.
Так как я сам не только Typescript, но и C# разработчик, то для примера возьму эталонную реализацию этой архитектуры для Asp.Net Core.
Вот упрощенный пример приложения:
Если вы не понимаете что в нем написано ничего страшного, дальше мы разберем его по частям каждый фрагмент.
Пример приведен для Asp.Net Core приложения, но для Java Spring, WinForms, Android, React архитектура и код будут такие же, меняется только язык и работа с вьюхой (если она есть).
Применение в веб-приложениях
Единственный фреймворк который пытался использовать Чистую архитектуру был Angular. Но получилось это просто ужасно, что в 1, что в 2+.
И причин для этого много:
Начинаем создавать приложение
Рассматривать Чистую Архитектуру будем на примере выдуманного приложения, максимально приближенного к реальному веб-приложению. Это кабинет в страховой компании, который отображает профиль пользователя, страховые случаи, предлагаемые тарифы страхования и инструменты для работы с этими данными.
В примере будет реализована лишь малая часть функционала, но по ней можно понять где и как располагать остальной функционал. Начинать создавать приложение начнем со слоя Controller, а View слой подключим в самом конце. А по ходу создания рассмотрим каждый слой детальнее.
Паттерн Controller
Controller — отвечает за взаимодействие пользователя с приложением. Это может быть клик по кнопке на веб странице, настольном приложении, мобильном приложении, или ввод команды в консоли линукса, или сетевой запрос, или любое другое IO событие приходящее в приложение.
Самый простой контроллер в чистой архитектуре выглядит следующим образом:
Его задача получить от пользователя событие и запустить бизнес процессы. В идеальном случае Controller ничего не знает про View, и тогда его можно переиспользовать между платформами, например Web, React-Native или Electron.
А теперь давайте напишем контроллер для нашего приложения. Его задача получить профиль пользователя, имеющиеся тарифы и предложить наилучший тариф пользователю:
У нас получился обычной контроллер без чистой архитектуры, если отнаследовать его от React.Component то получим рабочий компонент с логикой. Так пишут очень много разработчиков веб-приложений, но у такого подхода есть много существенных недостатков. Главный из которых невозможность переиспользования логики между компонентами. Ведь рекомендуемый тариф может выводиться не только в личном кабинете, но и на лендинге и множестве других мест для привлечения клиента к услуге.
Для того что бы иметь возможность переиспользовать логику между компонентами ее необходимо вынести в специальный слой, который называется Service.
Паттерн Service
Service — отвечает за всю бизнес логику приложения. Если Controller’у понадобилось получить, обработать, отправить какие то данные — он делает это через Service. Если нескольким контроллерам понадобилась одна и та же логика, они работают с Service. Но сам слой Service ничего не должен знать о слое Controller и View и окружении в котором он работает.
Давайте вынесем логику из контроллера в сервис и внедрим сервис в контроллер:
Теперь если нескольким контроллерам понадобиться получить профиль пользователя или тарифы, они смогу переиспользовать одну и туже логику из сервисов. В сервисах главное не забывать про SOLID принципы и что каждый сервис отвечает за свою зону ответственности. В данном случае один сервис отвечает за работу с профилем пользователя, а другой сервис за работу с тарифами.
Но что делать если источник данных поменяется, например fetch может смениться на websocket или grps или базу данных, а реальные данные понадобиться заменить тестовыми? И вообще зачем бизнес логике что то знать о источнике данных? Для решениях этих проблем существует слой Repository.
Паттерн Repository
Repository — отвечает за общение с хранилищем данных. В качестве хранилища может выступать сервер, база данных, память, localstorage, sessionstorage или любое другое хранилище. Его задача абстрагировать слой Service от конкретной реализации хранилища.
Давайте вынесем сетевые запросы из сервисов в репозитории, контроллер при этом не меняем:
Теперь достаточно один раз написать запрос к данным и любой сервис сможет переиспользовать этот запрос. Позже мы рассмотрим пример как переопределить репозиторий не трогая код сервиса и внедрить моковый репозиторий на время тестирования.
В сервисе UserProfilService может показаться что он не нужен и контроллер может напрямую обратиться к репозиторию за данными, но это не так. В любой момент в бизнес слое могут появиться или измениться требования, может потребоваться дополнительный запрос или обогатить данные. Поэтому даже когда в слое сервиса нету логики цепочка Controller — Service — Repository должна сохраняться. Это вклад в ваше завтра.
Настало время разобраться что за заданные получает репозиторий, корректные ли они вообще. За это отвечает слой Models.
Модели: DTO, Entities, ViewModels
Models — отвечает за описание структур с которыми работает приложение. Такое описание очень помогает новым разработчикам проекта понять с чем работает приложение. Кроме того его очень удобно использовать для построения баз данных или проведений валидаций данных хранящихся в модели.
Добавим в приложение модель профиля пользователя и другие модели, и сообщим остальным слоям что теперь мы работаем не с абстрактным объектом, а с вполне конкретным профилем: