Асинхронные команды и запросы c @artstesh/postboy: упрощаем архитектуру приложений

Приветствую! Продолжаем разбирать возможности @artstesh/postboy и обсуждать, как сделать ваше приложение проще, а код элегантнее. Сегодня поговорим о том, что такое асинхронные команды и запросы, почему этот механизм так удобен, и как использовать его в реальных приложениях. Как всегда, всё покажу на живых примерах, чтобы можно было сразу применить на практике.
Команды и запросы: в чём суть?
Каждое приложение неизбежно сталкивается с задачами двух типов:
Запросы — задать вопрос и получить ответ. Например, запросить данные из кэша или с сервера. Запросы не меняют состояние системы. Они только запрашивают данные.
Команды — это действие. Вы говорите системе: «покажи модалку», «обнови запись», «измени состояние». Команда выполняет действие, но при этом ничего не возвращает.
Эта концепция встречается в архитектурном подходе CQRS (Command Query Responsibility Segregation), где работа разбивается на логически обособленные задачи. Однако применять такие идеи можно и без сложностей: достаточно настроить удобный механизм.
Запросы: стройная система получения данных
Давайте представим типичную задачу. У нас есть сервис, который обращается за данными к серверу, а потом сохраняет их в кэше, чтобы не дёргать сервер лишний раз. А ещё есть компонент, которому нужно получить эти данные. И нам бы хотелось, чтобы он ничего не знал не только о внутренней логике сервиса, но и о его существовании, и просто мог сделать абстрактный запрос данных.
Шаг 1. Определяем запрос
Запрос в системе postboy описывается отдельным классом:
export class GetUserQuery extends PostboyCallbackMessage {
static readonly ID = '0bcad518-7591-4831-b220-649ff186051b';
constructor(public userId: string) {
super();
}
}
userId
, который мы передаём, служит для идентификации данных, в данном примере мы хотим получить пользователя по его идентификатору. Кроме того, мы указываем тип возвращаемого значения типизируя наследуемый класс PostboyCallbackMessage
.
Шаг 2. Регистрируем запрос в регистраторе
@Injectable()
export class AppMessageRegister extends PostboyAbstractRegistrator {
constructor(postboy: AppPostboyService, users: UserService) {
super(postboy);
this.registerServices([users]);
}
protected _up(): void {
this.recordSubject(GetUserQuery);
}
}
Шаг 3. Реализация обработчика в сервисе
Теперь настроим, что будет происходить, когда запрос попадёт в систему.
import {IPostboyDependingService} from "@artstesh/postboy";
@Injectable()
export class UserService implements IPostboyDependingService {
private cache: Record = {}; // Локальный кэш
constructor(private postboy: AppPostboyService) {}
up(): void {
this.postboy.sub(GetUserQuery).subscribe(qry => {
if (this.cache[query.key]) query.finish(this.cache[query.key]);
else {
// Если данных нет, грузим с сервера
this.fetchFromServer(query.key).then((data) => {
this.cache[query.key] = data; // Сохраняем в кэш
query.finish(data); // Возвращаем результат
});
}
});
}
private fetchFromServer(key: string): Promise {
// Запрос на сервер
}
}
Эта часть демонстрирует, управление кэшем и динамическими данными. Обращаю внимание, что это, конечно же, не продуктовый код, а синтетика, призванная показать общий принцип; в реальности, логику, например, кэширования мы будем выстраивать совсем иначе, обеспечив очереди запросов на этапе ожидания заполнения кэша. Многое может измениться в коде конкретного проекта, но логика работы с postboy остается прежней: сервис подписывается на событие и возвращает отправителю результат по готовности.
Шаг 4. Отправляем запрос из компонента
А теперь покажем, как компонент запрашивает данные:
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit} from "@angular/core";
@Component({
selector: 'app-demo',
template: 'User: {{ user | json }}
',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class DemoComponent implements OnInit {
user?: User;
constructor(private postboy: AppPostboyService,
private detector: ChangeDetectorRef) {
}
ngOnInit(): void {
this.postboy.fireCallback(new GetUserQuery('exampleKey'), user => {
this.user = result; // Обновляем локальное состояние
this.detector.detectChanges();
});
}
}
Всё предельно просто: компоненты не заботятся о том, откуда берутся данные, и не входят в ненужную зависимость от сервисов. Мы просто «задали вопрос», а библиотека сделала остальную работу.
Команды: вызываем действия
Теперь разберём противоположный механизм — команды. Это случай, когда компонент говорит системе «сделай что-то».
Допустим, вы разрабатываете большое приложение, и в некоторых местах нужны заглушки (например, уведомление «Раздел в разработке»). С помощью команд мы можем реализовать это красиво и централизованно.
Шаг 1. Создаём команду
Определить команду так же просто, как и запрос:
export class NotReadyCommand extends PostboyGenericMessage {
public static readonly ID = '72a7986f-b8e2-459f-90f0-f6d88eb9cbda';
}
Шаг 2. Регистрируем команду в регистраторе
@Injectable()
export class AppMessageRegister extends PostboyAbstractRegistrator {
constructor(postboy: AppPostboyService) {
super(postboy);
}
protected _up(): void {
this.recordSubject(NotReadyCommand);
}
}
Уже готово. Теперь её можно отправить в любую часть приложения.
Шаг 3. Обработчик команды
Теперь настраиваем реакцию на команду. Например, вызываем модальное окно:
@Component({
selector: 'app-not-ready-modal',
standalone: true,
templateUrl: './app-not-ready-modal.component.html',
styleUrl: './app-not-ready-modal.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GenericModalMessageComponent implements OnInit {
constructor(private postboy: AppPostboyService) {}
ngOnInit(): void {
this.subs.push(
this.postboy.sub(NotReadyCommand).subscribe(cmd => this.open())
);
}
public open(): void {
// Логика открытия модалки
}
}
Обработчик один, а вызвать команду можно из любого модуля приложения. Это отличное решение для модульной архитектуры, когда одни части системы ничего не знают о других.
Шаг 4. Отправляем команду
Команда отправляется буквально в одну строку. Например, при клике на кнопку:
@Component({
selector: 'app-some',
templateUrl: './some.component.html',
})
export class SomeComponent {
constructor(private postboy: AppPostboyService) {}
showNotReady() {
this.postboy.fire(new NotReadyCommand());
}
}
Система берёт команду, передаёт её обработчику, и действие выполнено. Удобно, понятно, и что важно — без лишних связей между модулями.
Зачем это нужно?
Если подвести итог, то механизмы команд и запросов в @artstesh/postboy дают вам:
Чистую и модульную архитектуру — кода меньше, зависимости минимальны.
Элегантное решение для общения между компонентами и сервисами.
Единый стиль взаимодействия, который упрощает работу с проектом как вам, так и вашим коллегам.
Асинхронные команды и запросы делают ваш код проще и понятнее.
Если Вас заинтересовал функционал библиотеки, Вы можете посетить сайт проекта, а еще я буду рад услышать предложения по улучшению и дополнению функционала.