Chain Of Responsibilityパターン

概要

ある処理を実行したときに、クラスが自分の管轄外だと判断したら次のクラスにおまかせする。というのを繰り返すスタイル。(クラスじゃなくてもメソッドなどでもOK)

イメージは、トラブル発生時に

  1. アルバイト: 僕には対処できません…。社員さんお願いします。
  2. 社員: 私には対処できません…。店長お願いします。
  3. 店長: 私には対処できません…。エリアマネージャーお願いします
  4. エリアマネージャー: 了解。対応しましょう。

使い方

bad?

const handleOrder = (order: Order[]) => {
  if (orders.length < 10) {
    partTimeJob.make(orders);
  } else if (orderCount < 50) {
    staff.make(orders);
  } else {
    chef.make(orders);
  }
}

このくらいの分岐ならifやcase文での分岐でいいかもしれないが、

分岐の条件が複雑になったり、分岐の数が増えたりすると可読性が一気に下がる

また、handleOrderの実装を変えたいってなったらhandleOrderを変えないといけなくなるため、結合度が高くなってしまっている

good

例1: クラスを使うパターン

abstract class Cook {
  private nextCook: Cook | null = null;

  setCook(cook: Cook) {
    this.nextCook = cook;
    return cook; // メソッドチェーンできるように返す
  }

  handle(orders: Order[]) {
    if (this.canMake(orders)) {
      this.make(orders);
    } else if (this.nextCook) {
      // 対処できない → 次の担当者に渡す
      this.nextCook(orders);
    } else {
      console.log('failed to handle')
    }
  }
  
  abstract canMake(orders: Order[]): boolean;
  abstract make(orders: Order[]): void;
}

class PartTimeJob extends Cook {
  canMake(orders: Order[]) {
    return orders.length < 10
  }
  make(orders: Order[]) {
    console.log('made by partTimeJob')
  }
}

class Staff extends Cook {
  canMake(orders: Order[]) {
    return orders.length < 50
  }
  make(orders: Order[]) {
    console.log('made by staff')
  }
}

class Chef extends Cook {
  canMake(orders: Order[]) {
    return true
  }
  make(orders: Order[]) {
    console.log('made by chef')
  }
}

partTimeJob = new PartTimeJob()
staff = new Staff()
chef = new Chef()

partTimeJob.nextCook(staff).nextCook(chef)

partTimeJob.handle(orders)

例2: クラスを使わないパターン

// 成功したらtrue, 失敗したらfalse
type Handler = (orders: Order[]) => boolean;

const partTimeJobHandler: Handler = (orders) => {
  if (orders < 10) return false;
  console.log('made by partTimeJob')
  return true
}

const staffHandler: Handler = (orders) => {
  if (orders < 10) return false;
  console.log('made by staff')
  return true
}

const chefHandler: Handler = (orders) => {
  console.log('made by chef')
  return true
}

const chain = [partTimeJobHandler, staffHandler, chefHandler]

const handleOrders = (chain: Handlers, orders: Order[]) => {
  for (const handler of chain) {
    if (handler(orders)) return;
  }
  console.log("failed to handle");
}

この2つの例のようにそれぞれの処理の中で条件を書くことで、条件を可視化できる。

また、chainの組み合わせは自由に変えられるので、以下のようなケースになっても他のコックへの影響範囲を小さくすることができる

  • staffが欠勤した
  • 新しいstaffが追加された

よく使われる例はHTTPリクエストを捌く時。

使い所

  • 処理の数や順序が変わりやすい時
  • 処理の分岐が複雑なとき

参考