Приветствую! Продолжаем разбирать возможности @artstesh/postboy и обсуждать, как сделать ваше приложение проще, а код элегантнее. Сегодня поговорим о том, что такое асинхронные команды и запросы, почему этот механизм так удобен, и как использовать его в реальных приложениях. Как всегда, всё покажу на живых примерах, чтобы можно было сразу применить на практике.
Команды и запросы: в чём суть?
Каждое приложение неизбежно сталкивается с задачами двух типов:
Запросы — задать вопрос и получить ответ. Например, запросить данные из кэша или с сервера. Запросы не меняют состояние системы. Они только запрашивают данные.
Команды — это действие. Вы говорите системе: "покажи модалку", "обнови запись", "измени состояние". Команда выполняет действие, но при этом ничего не возвращает.
Эта концепция встречается в архитектурном подходе CQRS (Command Query Responsibility Segregation), где работа разбивается на логически обособленные задачи. Однако применять такие идеи можно и без сложностей: достаточно настроить удобный механизм.
Запросы: стройная система получения данных
Давайте представим типичную задачу. У нас есть сервис, который обращается за данными к серверу, а потом сохраняет их в кэше, чтобы не дёргать сервер лишний раз. А ещё есть компонент, которому нужно получить эти данные. И нам бы хотелось, чтобы он ничего не знал не только о внутренней логике сервиса, но и о его существовании, и просто мог сделать абстрактный запрос данных.
Шаг 1. Определяем запрос
Запрос в системе postboy описывается отдельным классом:
export class GetUserQuery extends PostboyCallbackMessage<User|null> { 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<string, User> = {}; // Локальный кэш 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<User | null> { // Запрос на сервер } }
Эта часть демонстрирует, управление кэшем и динамическими данными. Обращаю внимание, что это, конечно же, не продуктовый код, а синтетика, призванная показать общий принцип; в реальности, логику, например, кэширования мы будем выстраивать совсем иначе, обеспечив очереди запросов на этапе ожидания заполнения кэша. Многое может измениться в коде конкретного проекта, но логика работы с postboy остается прежней: сервис подписывается на событие и возвращает отправителю результат по готовности.
Шаг 4. Отправляем запрос из компонента
А теперь покажем, как компонент запрашивает данные:
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit} from "@angular/core"; @Component({ selector: 'app-demo', template: '<p>User: {{ user | json }}</p>', 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. Отправляем команду
Команда отправляется буквально в одну строку. Например, при клике на кнопку:
<button (click)="showNotReady()">В разработке</button>
@Component({ selector: 'app-some', templateUrl: './some.component.html', }) export class SomeComponent { constructor(private postboy: AppPostboyService) {} showNotReady() { this.postboy.fire(new NotReadyCommand()); } }
Система берёт команду, передаёт её обработчику, и действие выполнено. Удобно, понятно, и что важно — без лишних связей между модулями.
Зачем это нужно?
Если подвести итог, то механизмы команд и запросов в @artstesh/postboy дают вам:
Чистую и модульную архитектуру — кода меньше, зависимости минимальны.
Элегантное решение для общения между компонентами и сервисами.
Единый стиль взаимодействия, который упрощает работу с проектом как вам, так и вашим коллегам.
Асинхронные команды и запросы делают ваш код проще и понятнее.
Если Вас заинтересовал функционал библиотеки, Вы можете посетить сайт проекта, а еще я буду рад услышать предложения по улучшению и дополнению функционала.
