😽

フロントでのCommandパターンの使い方

2025/03/04に公開1

コマンドパターンをフロントで使う方法がピンと来なかったので考えてみた

Command パターンとは?

「命令」を「順番」に実行する方法。

もう少し詳しく

それぞれの非同期処理の命令をオブジェクトとして扱い、それらを特定の順番に実行することで、矛盾なく処理を進める手法。

いつ使う?

  • 複数の非同期処理を順番に処理したいとき
  • 非同期処理の最中に発生する別の非同期処理も適切に処理したいとき
    • ロードを挟まずにさまざまな非同期処理を実行したい

例:Todo リストの API

以下のエンドポイントがあるとする。

POST /{id}/rename
// Todo の名前を変更する
GET /{id}
// Todo を取得する
DELETE /{id}
// Todo を削除する

名前を変更した後に、変更後の名前を確認したい。
もしくは、名前の変更中にTodoを削除したい

シンプルに実装すると?

キャッシュ操作を考慮せず、単純にリクエストを順番に実行するとこうなる。

function rename() {
await fetch(`/{id}/rename`); //この処理中に別のオペレーションを行いたい(例Todoの作成)
const todo = await fetch(`/{id}`);
setTodo(todo)//新しい名前を含んだTodo
}

この場合、fetch(/{id}/rename) の処理が終わるまで他の操作を受け付けなくなる。
ロードUIを実装することでマシにはなるが、もしユーザーが別のオペレーション(例:Todoの削除)を行いたい場合、この実装では操作を受け付けないので問題が発生する。

非同期で処理すると?

function rename() {
fetch(`/{id}/rename`);
const todo = fetch(`/{id}`);
setTodo(todo)//新しい名前を含んだTodo
}

これだと変更した名前が反映される前の古いデータを取得してしまう可能性がある。

ここで Command パターンの出番

やりたいことは:

  1. 複数の非同期処理を順番に実行したい。
  2. 非同期処理中でも他の操作ができるようにしたい。

これらは一見矛盾しているように見えるが、Command パターンを使えば解決できる。

Command パターンの活用

非同期処理を キュー に入れてバックグラウンドで順番に実行することで、他の操作を並行して行うことが可能になる。

これにより、ユーザーがスムーズに操作できる UI を維持しながら、非同期処理を適切な順序で実行できるようになる。

React での簡単な Command パターン実装例

以下は、Command パターンを活用して Todo の更新を管理する React の例。

  1. 非同期処理のクラスを定義
// RenameCommand: Todoの名前を変更する非同期処理
class RenameCommand {
  constructor(newName) {
    this.newName = newName;
  }

  execute() {
    return fetch("/api/todo/rename", {
      method: "POST",
      body: JSON.stringify({ name: this.newName }),
    }).then(() => {
      console.log("Todo renamed!");
    });
  }
}

// FetchCommand: Todoを取得する非同期処理
class FetchCommand {
  constructor(setTodo) {
    this.setTodo = setTodo;
  }

  execute() {
    return fetch("/api/todo")
      .then((res) => res.json())
      .then((todo) => {
        this.setTodo(todo.name);
        console.log("Todo fetched!");
      });
  }
}

// DeleteCommand: Todoを削除する非同期処理
class DeleteCommand {
  constructor(setIsDeleted) {
    this.setIsDeleted = setIsDeleted;
  }

  execute() {
    return fetch("/api/todo", {
      method: "DELETE",
    }).then(() => {
      console.log("Todo deleted!");
      this.setIsDeleted(true);
    });
  }
}
  1. CommandQueue の実装
class CommandQueue {
  constructor() {
    this.queue = [];
    this.executing = false;
  }

  add(command) {
    this.queue.push(command);
    this.execute(); // 特に待つなどはせず非同期的に処理をする
  }

  execute() {
    if (this.executing) return;
    this.executing = true;

    const runNextCommand = () => {
      if (this.queue.length === 0) {
        this.executing = false; // キューが空になったら実行中フラグを解除
        return;
      }

      const command = this.queue.shift(); // キューから次のコマンドを取り出す
      command
        .execute()
        .then((result) => {
          console.log("Command resolved:", result); // コマンドが成功した場合
        })
        .catch((error) => {
          console.error("Command failed:", error); // コマンドが失敗した場合
        })
        .finally(() => {
          runNextCommand(); // 次のコマンドを実行
        });
    };

    runNextCommand(); // 最初のコマンドを実行
  }
}
  1. React コンポーネントでの使用
import React, { useState } from "react";

const commandQueue = new CommandQueue();

const TodoApp = () => {
  const [todo, setTodo] = useState("Sample Todo");
  const [isDeleted, setIsDeleted] = useState(false);

  const renameTodo = (newName) => {
    // RenameCommand をキューに追加
    commandQueue.add(new RenameCommand(newName));
    // FetchCommand をキューに追加
    commandQueue.add(new FetchCommand(setTodo));
  };

  const deleteTodo = () => {
    // DeleteCommand をキューに追加
    commandQueue.add(new DeleteCommand(setIsDeleted));
    // FetchCommand をキューに追加
    commandQueue.add(new FetchCommand(setTodo));
  };

  return (
    <div>
      <h1>{isDeleted ? "Todo Deleted" : todo}</h1>
      <button onClick={() => renameTodo("New Todo Name")}>Rename</button>
      <button onClick={deleteTodo}>Delete</button>
    </div>
  );
};

export default TodoApp;

出力例
Rename ボタンをクリックした場合

Todo renamed!
Todo fetched!

Delete ボタンをクリックした場合

Todo deleted!
Todo fetched!

ここでのポイントは

  • クラス化による再利用性:
    • RenameCommand、FetchCommand、DeleteCommand は独立したクラスなので、他のコンポーネントやプロジェクトでも再利用できる
  • 非同期処理の順番保証:
    • CommandQueue がコマンドを順番に実行するため、非同期処理の順番が保証される
  • 非同期処理中の他の操作:
    • Command パターンを使うことで、非同期処理中でも他の操作(例: 別のTodoを操作する)が可能

Discussion

Honey32Honey32

失礼します

function rename() {
await fetch(`/{id}/rename`); //この処理中に別のオペレーションを行いたい(例Todoの作成)
const todo = await fetch(`/{id}`);
setTodo(todo)//新しい名前を含んだTodo
}

この場合、fetch(/{id}/rename) の処理が終わるまで他の操作を受け付けなくなる。
ロードUIを実装することでマシにはなるが、もしユーザーが別のオペレーション(例:Todoの削除)を行いたい場合、この実装では操作を受け付けないので問題が発生する。

という記述がありますが、実際はそうではありません。処理が終わるまでの間であっても、ユーザーのインタラクションは受け付けることができます。(むしろ、「受け付けてしまう」ことが不具合を誘発することもあるので、React 19 では、 Action という新しい機能によって「非同期処理が実行中なので、余計なことをしない」ような処理を書きやすくしてくれています)

https://ja.react.dev/blog/2024/12/05/react-19


なので、Command パターンを使ったコードは、単に async / await を使う例とほぼ変わりない挙動になっているはずです。

少し話がそれますが、Command をはじめ「GoF のデザインパターン」のちまたに出回っているサンプルコードは、無名関数、Promise が存在しない言語でも、何とかする ためのイディオムが多く、それらをわざわざ真似なくても、無名関数・Promise・React の機能などをキチンと活用すればうまく実装できることが多いです!

JS・HTML の基本的な機能が書かれている MDN や、React の仕様について細かく書かれている ja.react.dev/learn は、そういった必要な情報が詰まっているとおもうのでオススメです!