Pull to refresh

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

Level of difficultyEasy
Reading time5 min
Views333

Приветствую! Продолжаем разбирать возможности @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. Единый стиль взаимодействия, который упрощает работу с проектом как вам, так и вашим коллегам.

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

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

Tags:
Hubs:
Total votes 1: ↑0 and ↓1-1
Comments5

Articles