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

88346ba483454378c243b1c16670542f.png

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

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

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

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

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

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

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

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

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

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

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

© Habrahabr.ru