Commandパターン

概要

処理をCommand(関数やオブジェクト)としてラップすることで、処理の呼び出し側(Invoker)と処理内容(Receiver)を疎結合にできるパターン。

呼び出し側はCommandに向かって、実行しろと命令するだけでいい(具体的な処理を書かなくていい)ので、呼び出し側のコードをシンプルに保てる上に、疎結合になるためテストしやすくなる。

使い方

bad

type CookLevel = 'rare' | 'well-done' | 'medium'
type FishType = 'tuna' | 'salmon' | 'yellowtail'

class Shef {
  cook(type: FoodType, ingredients: Ingredient[], options)
}
class Kitchen {
  constructor(private shefs: Shef[]) {}

  selectShef() {}
  selectIngredients(type: FoodType) {}

  cookSteak(cookLevel: CookLevel = 'medium') {
    const shef = this.selectShef();
    const ingredients = this.selectIngredients(type: 'steak')
    shef.cook(type: 'steak', ingredients, { cookLevel })
  }
  
  cookSushi(fishType: FishType = 'tuna') {
    const shef = this.selectShef();
    const ingredients = this.selectIngredients(type: 'sushi')
    shef.cook(type: 'sushi', ingredients, { fishType })
  }
}

class Waiter {
  constructor(private kitchen: Kitchen)
  order(foodType: FoodType, options: { cookLevel?: CookLevel, fishType: FishType }) {
    if (foodType == 'steak') {
      this.kitchen.cookSteak(options.cookLevel);
    } else if (foodType == 'sushi') {
      this.kitchen.cookSushi(options.fishType);
    } else {
      ...
    }
  }
}

const chef1 = new Chef();
const chef2 = new Chef();
const kitchen = new Kitchen([chef1, chef2]);
const waiter = new Waiter(kitchen);
waiter.order('steak', { cookLevel: 'rare' });

何を作るかという情報(foodType)をwaiterが知ってる必要がある。 もし、ramenなどのfoodTypeが増えるとwaiter側の分岐コードを変更する必要が出てくる

good

type CookLevel = 'rare' | 'well-done' | 'medium'
type FishType = 'tuna' | 'salmon' | 'yellowtail'

class Shef {
  cook(type: FoodType, ingredients: Ingredient[])
}
class Kitchen {
  constructor(private shefs: Shef[]) {}

  selectShef() {}
  selectIngredients(type: FoodType) {}

  cookSteak(cookLevel: CookLevel = 'medium') {
    const shef = this.selectShef();
    const ingredients = this.selectIngredients(type: 'steak')
    shef.cook(type: 'steak', ingredients, { cookLevel })
  }
  
  cookSushi(fishType: FishType = 'tuna') {
    const shef = this.selectShef();
    const ingredients = this.selectIngredients(type: 'sushi')
    shef.cook(type: 'sushi', ingredients, { fishType })
  }
}

interface Command {
  execute: () => void;
}

class CookSteakCommand implements Command {
  constructor(private kitchen Kitchen, private cookLevel: CookLevel) {}
  execute() {
    this.kitchen.cookSteak(this.cookLevel)
  }
}

class CookSushiCommand implements Command{
  constructor(private kitchen: Kitchen, private fishType: FishType) {}
  execute() {
    this.kitchen.cookSushi(this.fishType)
  }
}

class Waiter {
  order(command: Command) {
    command.execute();
  }
}

const chef1 = new Chef();
const chef2 = new Chef();
const kitchen = new Kitchen([chef1, chef2]);
const waiter = new Waiter();

const command = new CookSteakCommand(kitchen, 'rare')

waiter.order(command)

品物を作るという処理をCommandに切り出したことによって、何を作るかとその処理をorderに注入できるようになった

waiter側はorderのコマンドを実行するだけでよくなり、品物が増えた時にwaiterに対して変更をしなくて良くなった

使い所

  • どの処理をするかの判断とその実行を別々で行いたいとき
  • 処理自体に状態を持たせたくなった時

参考