Commandパターン

APIを使った商品の発注を柔軟に行おう

受注した商品に対して、各部品に対して別々のベンダーに発注するとします。
在庫がなかった場合、それまで注文した部品の発注をさかのぼってキャンセルするような仕組みを作ります。

例えば、デスクトップパソコンが発注された場合、ケース、マザーボード、CPU、メモリ、ハードディスクをそれぞれ別ベンダーに発注するとして、
実装してみましょう。
(認証などについては割愛します)

<?php

function orderPc(string $cpu, string $memory, string $harddisk): int
{
  // CPUの在庫確認
  echo "CPU「{$cpu}」の在庫問い合わせAPIを呼び出します\n";
  $inventoryCount = 3;
  if ($inventoryCount < 1) {
    return 0;
  }
  // CPUの発注
  echo "CPU「{$cpu}」の発注APIを呼び出します\n";
  $orderStatus = 1;
  if ($orderStatus != 1) {
    return 0;
  }

  // Memoryの在庫確認
  echo "Memory「{$memory}」の在庫問い合わせAPIを呼び出します\n";
  $inventoryCount = 4;
  if ($memory === '16GB') {
    $inventoryCount = 0;
  }
  if ($inventoryCount < 1) {
    // CPUキャンセル
    echo "CPU「{$cpu}」のキャンセルAPIを呼び出します\n";
    return 0;
  }
  // Memoryの発注
  echo "Memory「{$memory}」の発注APIを呼び出します\n";
  $orderStatus = 1;
  if ($orderStatus != 1) {
    // CPUキャンセル
    echo "CPU「{$cpu}」のキャンセルAPIを呼び出します\n";
    return 0;
  }

  // Harddiskの在庫確認
  echo "HDD「{$harddisk}」の在庫問い合わせAPIを呼び出します\n";
  $inventoryCount = 8;
  if ($inventoryCount < 1) {
    // CPUキャンセル
    echo "CPU「{$cpu}」のキャンセルAPIを呼び出します\n";
    // Memoryキャンセル
    echo "Memory「{$memory}」のキャンセルAPIを呼び出します\n";
    return 0;
  }
  // Harddiskの発注
  echo "HDD「{$harddisk}」の発注APIを呼び出します\n";
  $orderStatus = 1;
  if ($orderStatus != 1) {
    // CPUキャンセル
    echo "CPU「{$cpu}」のキャンセルAPIを呼び出します\n";
    // Memoryキャンセル
    echo "Memory「{$memory}」のキャンセルAPIを呼び出します\n";
    return 0;
  }
  return 1;
}

function order(string $cpu, string $memory, string $harddisk)
{
  if (orderPc($cpu, $memory, $harddisk) == 1) {
    echo "PCのオーダーが完了しました\n";
  } else {
    echo "PCのオーダーをキャンセルしました\n";
  };
}

order('はやいCPU', '32GB', 'SSD8テラ');
order('おそいCPU', '16GB', 'SSD256GB');

キャンセル時の処理が冗長ですし、他の部品が増えたときに分岐が増えてしまいます。
また、発注時のパラメータが増えた場合にも処理が巨大化し、修正の影響範囲も広くなっています。

APIの実行をそれぞれCommand(命令)にして処理をするのが有効です。

を作って実行してみましょう。

Commandパターンとは

Commandパターンとは「命令」を定義し、インスタンスとして利用するパターンです。

使ってみよう

<?php

interface ApiCommand
{
  public function execute(): int;
  public function cancel(): void;
}

/**
 * CPU在庫問い合わせコマンド
 */
class CpuStockCommand implements ApiCommand
{
  private string $productName;
  public function CpuStockCommand(String $productName)
  {
    $this->productName = $productName;
  }
  public function execute(): int
  {
    echo "CPU「{$this->productName}」の在庫問い合わせAPIを呼び出します\n";
    return 3;
  }
  public function cancel(): void
  {
    // no-op
  }
}

/**
 * CPU発注コマンド
 */
class CpuOrderCommand implements ApiCommand
{
  private string $productName;
  public function CpuOrderCommand(String $productName)
  {
    $this->productName = $productName;
  }
  public function execute(): int
  {
    echo "CPU「{$this->productName}」の発注APIを呼び出します\n";
    return 1;
  }
  public function cancel(): void
  {
    echo "CPU「{$this->productName}」のキャンセルAPIを呼び出します\n";
  }
}
/**
 * メモリ在庫問い合わせコマンド
 */
class MemoryStockCommand implements ApiCommand
{
  private string $productName;
  public function MemoryStockCommand(String $productName)
  {
    $this->productName = $productName;
  }
  public function execute(): int
  {
    echo "Memory「{$this->productName}」の在庫問い合わせAPIを呼び出します\n";
    if ($this->productName === '16GB') {
      return 0;
    }
    return 4;
  }
  public function cancel(): void
  {
    // no-op
  }
}

/**
 * メモリ発注コマンド
 */
class MemoryOrderCommand implements ApiCommand
{
  private string $productName;
  public function MemoryOrderCommand(String $productName)
  {
    $this->productName = $productName;
  }
  public function execute(): int
  {
    echo "Memory「{$this->productName}」の発注APIを呼び出します\n";
    return 1;
  }
  public function cancel(): void
  {
    echo "Memory「{$this->productName}」のキャンセルAPIを呼び出します\n";
  }
}

/**
 * HDD在庫問い合わせコマンド
 */
class HarddiskStockCommand implements ApiCommand
{
  private string $productName;
  public function HarddiskStockCommand(String $productName)
  {
    $this->productName = $productName;
  }
  public function execute(): int
  {
    echo "HDD「{$this->productName}」の在庫問い合わせAPIを呼び出します\n";
    return 8;
  }
  public function cancel(): void
  {
    // no-op
  }
}

/**
 * HDD発注コマンド
 */
class HarddiskOrderCommand implements ApiCommand
{
  private string $productName;
  public function HarddiskOrderCommand(String $productName)
  {
    $this->productName = $productName;
  }
  public function execute(): int
  {
    echo "HDD「{$this->productName}」の発注APIを呼び出します\n";
    return 1;
  }
  public function cancel(): void
  {
    echo "HDD「{$this->productName}」のキャンセルAPIを呼び出します\n";
  }
}

class PcOrderCommand implements ApiCommand
{
  private array $commands;
  private array $executed;
  public function PcOrderCommand(PcOrderParameter $parameter)
  {
    $this->commands = [];

    $this->commands[] = new CpuStockCommand($parameter->getCpu());
    $this->commands[] = new CpuOrderCommand($parameter->getCpu());
    $this->commands[] = new MemoryStockCommand($parameter->getMemory());
    $this->commands[] = new MemoryOrderCommand($parameter->getMemory());
    $this->commands[] = new HarddiskStockCommand($parameter->getHarddisk());
    $this->commands[] = new HarddiskOrderCommand($parameter->getHarddisk());

    $this->executed = [];
  }
  public function addCommand(ApiCommand $command)
  {
    $this->commands[] = $command;
  }

  public function execute(): int
  {
    $isCancel = false;
    foreach ($this->commands as $command) {
      $result = $command->execute();
      if ($result !== 0) {
        $this->executed[] = $command;
      } else {
        // トランザクションキャンセル
        $isCancel = true;
        break;
      }
    }
    if ($isCancel) {
      $this->cancel();
      return 0;
    }
    return 1;
  }
  public function cancel(): void
  {
    foreach ($this->executed as $command) {
      $command->cancel();
    }
  }
}

class PcOrderInvoker
{
  private array $commands;
  public function PcOrderInvoker()
  {
    $this->commands = [];
  }
  public function addCommand(ApiCommand $api)
  {
    $this->commands[] = $api;
  }
  public function invoke()
  {
    foreach ($this->commands as $command) {
      if ($command->execute() == 1) {
        echo "PCのオーダーが完了しました\n";
      } else {
        echo "PCのオーダーをキャンセルしました\n";
      }
    }
  }
}

class PcOrderParameter
{
  private string $cpu;
  private string $memory;
  private string $harddisk;

  public function setCpu(string $cpu)
  {
    $this->cpu = $cpu;
  }
  public function setMemory(string $memory)
  {
    $this->memory = $memory;
  }
  public function setHarddisk(string $harddisk)
  {
    $this->harddisk = $harddisk;
  }

  public function getCpu()
  {
    return $this->cpu;
  }
  public function getMemory()
  {
    return $this->memory;
  }
  public function getHarddisk()
  {
    return $this->harddisk;
  }
}

// PC発注.
$rapidPcParameter = new PcOrderParameter();
$rapidPcParameter->setCpu('はやいCPU');
$rapidPcParameter->setMemory('32GB');
$rapidPcParameter->setHarddisk('SSD8テラ');
$invoker = new PcOrderInvoker();
$invoker->addCommand(new PcOrderCommand($rapidPcParameter));

$slowPcParameter = new PcOrderParameter();
$slowPcParameter->setCpu('おそいCPU');
$slowPcParameter->setMemory('16GB');
$slowPcParameter->setHarddisk('SSD256GB');
$invoker->addCommand(new PcOrderCommand($slowPcParameter));

$invoker->invoke();

こうすると、部品が増えた際にもクラスを増やすだけで対応できます。
そして、実行したコマンドを配列に保持ておくことで、発注をさかのぼってキャンセルすることができ、キャンセル処理が容易になります。
キャンセル処理ができるということは、Undo処理にも転用できますね。


また、APIが要求するプロパティが増えた場合でも、Command内に処理が閉じているため影響範囲を最小限にできます。

他のパターンでも多く当てはまりますが、ユニットテストが容易になることも大きなメリットとなるでしょう。

デメリットとしては、あらゆるデザインパターンにも当てはまりますが、やはりコード量が増えてしまうことでしょうか。
IDEやバージョン管理システムの助けを借りて、実装しやすく管理しやすい環境を作ることで、それらのデメリットはクリアできるでしょう。