Command Pattern

このエントリの目指すところ

以下の観点で調査/検討する.

  1. どのようなシーンで使えるのか
  2. なぜそのシーンで使えるのか

必要に応じてプログラムに起こす.

調査

語られる像

  • Wikipedia - Command パターン

    リクエストのために必要な手続きとデータをCommandオブジェクトとしてカプセル化した上で取り回し[1]、必要に応じてExecute(実行)するパターンである。オブジェクトであることを生かして命令のキューイングやロギング、Undo等が可能になり[2]、Executeを分離したことで手続きと実行を疎結合にできる。

  • TECHSCORE - 22. Commandパターン

    あるオブジェクトに対して要求を送るということは、そのオブジェクトのメソッドを呼び出すことと同じです。 そして、メソッドにどのような引数を渡すか、ということによって要求の内容は表現されます。さまざまな要求を送ろうとすると、引数の数や種類を増やさなければなりませんが、 それには限界があります。そこで要求自体をオブジェクトにしてしまい、そのオブジェクトを引数に渡すようにします。それがCommandパターンです。

  • フロントエンドのデザインパターン - コマンドパターン

    コマンドパターン (command pattern) を用いると、あるタスクを実行するオブジェクトと、そのメソッドを呼び出すオブジェクトを切り離すことができことができます。

  • IT専科 - Command パターン

    「Command」という英単語は、「命令」を意味します。 このパターンでは、1つもしくは複数の命令を1つのオブジェクトで表現(命令の詳細処理をカプセル化)します。また、命令をオブジェクトとして管理するため、その命令の履歴管理、UNDO(取消し)機能の実装等が容易に行えます。

整理

  1. Command Pattern の典型的な用例として Undo/Redo が挙げられる
  2. 手続き一式を隠ぺいし,オブジェクトとして取り扱えるようにする
  3. 手続きがオブジェクトとして扱われることで,キューイングや呼び出し履歴の記録が可能になる
  4. ある手続きを定義したとして,それを実行する責務と分離される

疑問

  1. Redo はともかく Undo をどのように実現するのか
    • Command に対する入力と出力を保持しておくということか

要点

Command Pattern の要点は大きく以下 2 つであると考える.

  1. 一連の処理をオブジェクトに隠蔽できる
  2. 処理の定義とその実行をそれぞれ別の責務として扱える

この結果,Command を実行する責務を負う Invoker で Execute したという事実が保持でき,条件さえそろえば実行履歴やその順序を保持できる.実行の順序が保持できていれば工夫次第で Redo/Undo も実現できる.

一連の処理をオブジェクトにすることを目的と据えるのであれば Redu/Undo は結果的に導かれるだけであって,例えば非同期に実行するタスクを積む一種の Producer-Consumer Pattern のベースとも考えられる.

実例

キャンバスに図形を描くプログラムを考える.Canvas に対して Line や Triangle などの図形を配置する Command,図形を Move したり Resize したりする Command も定義し,これらの実行履歴を保持する Invoker を定義する.

設計

環境

$ cat /etc/os-release 
PRETTY_NAME="Ubuntu 22.04.2 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.2 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy
$ clang++ --version
Ubuntu clang version 14.0.0-1ubuntu1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

実装

design_pattern/command_pattern.cpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#include "command_pattern/command_pattern.hpp"

using design_pattern::command_pattern::Canvas;
using design_pattern::command_pattern::Command;
using design_pattern::command_pattern::CreateLineDiagramCommand;
using design_pattern::command_pattern::CreateTriangleDiagramCommand;
using design_pattern::command_pattern::Executor;
using design_pattern::command_pattern::MoveDiagramCommand;
using design_pattern::command_pattern::ResizeDiagramCommand;

int main() {
  auto canvas = std::make_shared<Canvas>();
  auto execution_queue = std::queue<std::shared_ptr<Command>>();
  auto execution_history = std::list<std::shared_ptr<Command>>();
  auto executor = Executor();

  auto dumper = [&canvas, &executor]() {
    std::cout << "# state" << std::endl;
    std::cout << "## canvas" << std::endl;
    canvas->Dump();

    std::cout << "## executor" << std::endl;
    executor.DumpExecutionHistory();
  };

  // 初期状態の出力
  dumper();

  {  // 図形をキャンバスに追加する
    std::cout << "# add diagrams" << std::endl;
    executor += std::make_shared<CreateLineDiagramCommand>(canvas);
    executor += std::make_shared<CreateTriangleDiagramCommand>(canvas);
  }
  dumper();

  {  // 諸図形に対して操作する
    std::cout << "# operate diagrams" << std::endl;

    executor += std::make_shared<ResizeDiagramCommand>(canvas, 0, 100, 200);
    executor += std::make_shared<MoveDiagramCommand>(canvas, 1, 250, 50);
    executor += std::make_shared<MoveDiagramCommand>(canvas, 0, 100, 500);
  }
  dumper();
}
design_pattern/command_pattern/command_pattern.hpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#ifndef DESIGN_PATTERN_COMMAND_PATTERN_COMMAND_PATTERN_HPP_
#define DESIGN_PATTERN_COMMAND_PATTERN_COMMAND_PATTERN_HPP_

#include <algorithm>
#include <iostream>
#include <iterator>
#include <list>
#include <memory>
#include <queue>
#include <sstream>
#include <string>
#include <vector>

#include "../demangle.hpp"
#include "../join.hpp"
#include "component/Canvas.hpp"
#include "component/Diagram.hpp"
#include "component/Line.hpp"
#include "component/Triangle.hpp"

using design_pattern::component::Diagram;
using design_pattern::component::Line;
using design_pattern::component::Triangle;

namespace design_pattern::command_pattern {

/**
 * 図形が描画されるキャンバス
 */
class Canvas {
 public:
  void AddDiagram(std::shared_ptr<Diagram> diagram) {
    this->diagram_sets.push_back(std::make_shared<DiagramSet>(diagram));
  }
  void Dump() { std::cout << (this->diagram_sets.size() != 0 ? this->ToString() : std::string("empty")) << std::endl; }
  std::string ToString() {
    auto messages = std::vector<std::string>();
    std::transform(diagram_sets.begin(), diagram_sets.end(), std::back_inserter(messages),
                   [](std::shared_ptr<DiagramSet> diagram_set) { return diagram_set->ToString(); });
    return Join("\n", messages);
  }
  void Move(size_t index, int offset_x, int offset_y) { this->diagram_sets.at(index)->Move(offset_x, offset_y); }
  std::vector<std::shared_ptr<Diagram>> diagrams() {
    auto result = std::vector<std::shared_ptr<Diagram>>();
    std::transform(this->diagram_sets.begin(), this->diagram_sets.end(), std::back_inserter(result),
                   [](std::shared_ptr<DiagramSet> diagram_set_) { return diagram_set_->diagram(); });
    return result;
  }

 private:
  /**
   * Canvas が管理する Diagram のそれぞれがどの位置に存在するかを管理する
   */
  class DiagramSet {
   public:
    explicit DiagramSet(std::shared_ptr<Diagram> diagram, int x = 0, int y = 0) : diagram_(diagram), x_(x), y_(y) {}
    void Move(int offset_x, int offset_y) {
      this->x_ += offset_x;
      this->y_ += offset_y;
    }
    std::shared_ptr<Diagram> diagram() { return this->diagram_; }
    std::string ToString() {
      char buffer[0xFF];
      sprintf(buffer, "x: %d, y: %d - %s", this->x_, this->y_, this->diagram_->ToString().c_str());
      return buffer;
    }

   private:
    std::shared_ptr<Diagram> diagram_;
    int x_;
    int y_;
  };
  std::vector<std::shared_ptr<DiagramSet>> diagram_sets;
};

/**
 * コマンドの抽象クラス
 */
class Command {
 public:
  explicit Command(std::shared_ptr<design_pattern::command_pattern::Canvas> canvas) : canvas_(canvas) {}
  virtual void Execute() = 0;
  virtual std::string ToString() = 0;

 protected:
  std::shared_ptr<Canvas> canvas_;
};

/*
 * 線分の生成を担うコマンド
 */
class CreateLineDiagramCommand : public Command {
 public:
  explicit CreateLineDiagramCommand(std::shared_ptr<design_pattern::command_pattern::Canvas> canvas)
      : Command(canvas) {}
  virtual void Execute() {
    auto new_diagram = std::make_shared<Line>();
    this->canvas_->AddDiagram(new_diagram);
  }

  virtual std::string ToString() { return Demangle(typeid(*this)); }
};

/*
 * 三角形の生成を担うコマンド
 */
class CreateTriangleDiagramCommand : public Command {
 public:
  explicit CreateTriangleDiagramCommand(std::shared_ptr<design_pattern::command_pattern::Canvas> canvas_)
      : Command(canvas_) {}
  virtual void Execute() {
    auto new_diagram = std::make_shared<Triangle>();
    this->canvas_->AddDiagram(new_diagram);
  }

  virtual std::string ToString() { return Demangle(typeid(*this)); }
};

/*
 * 図形の移動を担うコマンド
 */
class MoveDiagramCommand : public Command {
 public:
  MoveDiagramCommand(std::shared_ptr<design_pattern::command_pattern::Canvas> canvas_, size_t index, int offset_x,
                     int offset_y)
      : Command(canvas_), index_(index), offset_x_(offset_x), offset_y_(offset_y) {}
  virtual void Execute() { canvas_->Move(this->index_, this->offset_x_, this->offset_y_); }
  virtual std::string ToString() { return Demangle(typeid(*this)); }

 private:
  size_t index_;
  int offset_x_;
  int offset_y_;
};

/*
 * 図形のリサイズを担うコマンド
 */
class ResizeDiagramCommand : public Command {
 public:
  ResizeDiagramCommand(std::shared_ptr<design_pattern::command_pattern::Canvas> canvas_, size_t index,
                       int target_height, int target_width)
      : Command(canvas_), index_(index), height_(target_height), width_(target_width) {}
  virtual void Execute() { this->canvas_->diagrams().at(index_)->Resize(this->target_height(), this->target_width()); }
  int target_height() { return this->height_; }
  int target_width() { return this->width_; }

  virtual std::string ToString() { return Demangle(typeid(*this)); }

 private:
  size_t index_;
  int height_;
  int width_;
};

/**
 * コマンドの実行し,その履歴を保持する.
 */
class Executor {
 private:
  std::vector<std::shared_ptr<Command>> execution_history_;

 public:
  Executor &operator+=(std::shared_ptr<Command> &&command) {
    command->Execute();
    this->execution_history_.push_back(command);
    return *this;
  }

  void DumpExecutionHistory() {
    if (this->execution_history_.size()) {
      std::for_each(this->execution_history_.begin(), this->execution_history_.end(), [idx = 0](auto &command) mutable {
        std::cout << idx++ << ": " << command->ToString() << std::endl;
      });
    } else {
      std::cout << "empty" << std::endl;
    }
  }
};

}  // namespace design_pattern::command_pattern

#endif  // DESIGN_PATTERN_COMMAND_PATTERN_COMMAND_PATTERN_HPP_
demangle.hpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#ifndef DEMANGLE_HPP_
#define DEMANGLE_HPP_

#include <cxxabi.h>

#include <string>

/**
 * @brief 型名をデマングル化する.
 *
 * @param id デマングル化する型のtype_infoオブジェクト
 * @return std::string デマングル化された型名を表す文字列
 * @throws std::exception デマングル化が失敗した場合に投げられます
 *
 * @details
 * gcc および clang コンパイラでマングル化された型名をデマングル化します.
 */
std::string Demangle(const std::type_info& id) {
  int status;
  char* demangled_name = abi::__cxa_demangle(id.name(), 0, 0, &status);

  if (status == 0) {
    auto result = std::string(demangled_name);
    free(demangled_name);
    return result;
  }

  throw std::exception();
}

#endif  // DEMANGLE_HPP_
join.hpp
// Copyright (c) 2023, Kumazawa (sparrow-blue)
// This source code is licensed under the BSD 3-Clause License.
// See https://github.com/sparrow-blue/blog/blob/main/LICENSE for details.

#ifndef JOIN_HPP_
#define JOIN_HPP_

#include <numeric>
#include <string>
#include <vector>

/**
 * @brief 文字列の配列を指定したデリミタで結合する.
 *
 * @param delimitor 文字列を連結する際の区切り文字
 * @param messages 連結する文字列の配列
 * @return std::string 連結された文字列
 *
 * @details
 * 指定したデリミタで文字列の配列を連結する.
 * デリミタに "," を指定し,文字列の配列として {"Hello", * "World"}
 * を指定した場合,"Hello,World"を返します.
 */
std::string Join(const std::string &delimitor,
                 const std::vector<std::string> &messages) {
  return std::accumulate(
      std::next(messages.begin()), messages.end(), messages[0],
      [delimitor](std::string a, std::string b) { return a + delimitor + b; });
}

#endif  // JOIN_HPP_

実行結果

$ date; clang++ src/command_pattern.cpp  -o command_pattern.out && ./command_pattern.out
Thu Jun 15 01:50:06 JST 2023
# state
## canvas
empty
## executer
empty
# add diagrams
# state
## canvas
x: 0, y: 0 - design_pattern::command_pattern::Line (0,5)
x: 0, y: 0 - design_pattern::command_pattern::Triangle (5,10)
## executer
0: design_pattern::command_pattern::CreateLineDiagramCommand
1: design_pattern::command_pattern::CreateTriangleDiagramCommand
# operate diagrams
# state
## canvas
x: 100, y: 500 - design_pattern::command_pattern::Line (0,200)
x: 250, y: 50 - design_pattern::command_pattern::Triangle (5,10)
## executer
0: design_pattern::command_pattern::CreateLineDiagramCommand
1: design_pattern::command_pattern::CreateTriangleDiagramCommand
2: design_pattern::command_pattern::ResizeDiagramCommand
3: design_pattern::command_pattern::MoveDiagramCommand
4: design_pattern::command_pattern::MoveDiagramCommand