Проектирование переиспользуемых модулей в NestJS
Reading time: 3 minutes
При разработке современных приложений часто возникает задача повторного использования класса или модуля в другом проекте, в связанном сервисе или при добавлении новых интерфейсов. Допустим, есть HTTP API, но теперь нужен ещё WebSocket или gRPC — стандартная ситуация. Хочется быстро и просто расширять функциональность, опираясь на уже написанный код.
Задача кажется тривиальной: берёшь нужный код, убираешь зависимости, оставляешь только входные параметры. Но книжный совет не работает, когда есть интеграции с другими частями системы и связи между сущностями. В таких условиях задача становится нетривиальной.
Проблема первая — связи между сущностями
Рассмотрим пример: домен — спальня со стульями, столами и кроватями.
Схема таблиц выглядит так:
Схема разумная и привычная для разработчиков. Сущности стульев, столов и кроватей имеют связи ManyToOne с сущностью спальни. Если строить HTTP API поверх такой схемы — получим проблемы с гибкостью, масштабируемостью и переиспользованием.
Когда понадобится разделить функциональность между микросервисами, окажется, что это невозможно: микросервис для стульев неизбежно тащит за собой часть, отвечающую за спальни. Такой подход даёт абсолютно монолитную архитектуру без масштабируемости и переиспользования кода.
Проблема вторая — инфраструктура
Протоколы, базы данных, интеграции с сервисами — у каждого своя специфика, от которой не абстрагируешься. HTTP — это HTTP, WebSocket — это WebSocket. Нет универсального «интерфейса сетевого взаимодействия» для обоих протоколов. Модули пишутся явно, под конкретные случаи. Попытка сделать один модуль на все случаи жизни — путь к монолиту: сложно поддерживать, много ошибок, непредсказуемое поведение.
Решение
Все эти проблемы решаемы. Программу можно сделать гибкой, а код — переиспользуемым без правок.
Решение проблемы связей
Нет связей — нет проблем со связями. Отказываемся от такой схемы данных. Связи между сущностями модулей — неверный путь. Спальня, стул, стол, кровать — четыре разных модуля, у которых нет ничего общего (никаких ссылок). Спальня — это агрегат для стульев, столов и кроватей. Схема данных меняется и выглядит так:
При такой схеме нижние компоненты можно перемещать и разделять. Их даже можно написать на другом стеке. Главное — обеспечить интерфейс взаимодействия для верхнего компонента (Bedroom), чтобы он мог получать столы, стулья и кровати.
Истинная модульность
Модули строятся по следующим принципам:
- Низкоуровневые модули содержат только бизнес-логику.
- Высокоуровневые модули (Http, Websocket и т.д.) создаются по мере необходимости и предоставляют внешние интерфейсы для низкоуровневых.
- Модуль отвечает только за свой домен и не предоставляет интерфейсы вложенных модулей. Например, модуль Zoo не должен раскрывать интерфейс дочернего модуля Dog для изменения имени сущности.
Низкоуровневый модуль содержит только бизнес-логику и работает с хранилищами. Его структура:
В таком модуле нет Controller, WebSocket Gateways, gRpcHandlers — только бизнес-логика в сервисах, зависящих исключительно от входных аргументов.
Если нужно предоставить HTTP API для этого модуля? Низкоуровневый модуль уже готов. Просто создаём новый HTTP-модуль, который использует низкоуровневый и предоставляет HTTP-контроллеры, работающие с методами сервисов.
На диаграмме убрал постфикс Module у Chair.
HttpChair оборачивает Chair и предоставляет контроллер.
Описываем каждую часть приложения — архитектура принимает вид:
Любой внешний интерфейс для низкоуровневых модулей — Http, Websocket, Rpc и другие — масштабируется и гибко настраивается.
Теперь создаём модуль Bedroom на основе других модулей:
Над Bedroom можно поднять Http и Websocket модули.
Важно: HttpBedroom предоставляет интерфейс только для спальни. Создавать, редактировать и удалять стулья через HttpBedroom нельзя.
Собираем HTTP-приложение из модулей:
Аналогично строится WebSocket-приложение из WebSocket-модулей:
При таком подходе получаем гибкость, масштабируемость и переиспользование. Из модулей собирается любой вариант приложения.
Пример кода: https://gitlab.com/cimpleo/blog/nest-modules-architect-example