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

от автора

Приветствую! Продолжаем разбирать возможности @artstesh/postboy и обсуждать, как сделать ваше приложение проще, а код элегантнее. Сегодня поговорим о том, что такое асинхронные команды и запросы, почему этот механизм так удобен, и как использовать его в реальных приложениях. Как всегда, всё покажу на живых примерах, чтобы можно было сразу применить на практике.


Команды и запросы: в чём суть?

Каждое приложение неизбежно сталкивается с задачами двух типов:

  1. Запросы — задать вопрос и получить ответ. Например, запросить данные из кэша или с сервера. Запросы не меняют состояние системы. Они только запрашивают данные.

  2. Команды — это действие. Вы говорите системе: «покажи модалку», «обнови запись», «измени состояние». Команда выполняет действие, но при этом ничего не возвращает.

Эта концепция встречается в архитектурном подходе 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 дают вам:

  1. Чистую и модульную архитектуру — кода меньше, зависимости минимальны.

  2. Элегантное решение для общения между компонентами и сервисами.

  3. Единый стиль взаимодействия, который упрощает работу с проектом как вам, так и вашим коллегам.

Асинхронные команды и запросы делают ваш код проще и понятнее.

Если Вас заинтересовал функционал библиотеки, Вы можете посетить сайт проекта, а еще я буду рад услышать предложения по улучшению и дополнению функционала.


ссылка на оригинал статьи https://habr.com/ru/articles/887060/