🐝

【TS】今さら聞けないストラテジパターン

2020/11/10に公開
2

はじめに

今回はストラテジパターン(Strategy Pattern)について解説します。
ポリモフィズム(多態性)を利用して、種別ごとに異なる挙動をさせたい時に便利なデザインパターンです。

ストラテジパターンとは?

有名どころでTECHSCOREさんの解説があります。

以下、引用です。

普通にプログラミングしていると、メソッドの中に溶け込んだ形でアルゴリズムを実装してしまうことがよくあります。if 文などで分岐させることでアルゴリズムを変更するような方法です。Strategy パターンでは、戦略の部分を意識して別クラスとして作成するようにしています。戦略x部分を別クラスとして作成しておき、戦略を変更したい場合には、利用する戦略クラスを変更するという方法で対応します。Strategy パターンを利用することで、メソッドの中に溶け込んだ形のアルゴリズムより柔軟でメンテナンスしやすい設計となります。

例題

以下のような遊園地のチケットがあったとします。

  • 種類として大人チケット、子供チケット、ペアチケット(大人2人)がある
  • 料金は大人チケットが3000円、子供チケットは1000円、ペアチケットは5000円
  • 子供用アトラクションは子供チケットでのみ利用できる

ストラテジパターンなし

例題の内容を特に何のデザインパターンも意識せずにコーディングすると以下のようになるかなと思います。


class Ticket {
  // 種別
  private _type: 'Adult' | 'Child' | 'Pair';
  
  constructor(type: 'Adult' | 'Child' | 'Pair' ) {
    this._type = type;
  }
  
  // 価格を取得
  public getPrice(): number {
    switch(this._type) {
      case 'Adult':
        return 3000;
      case 'Child':
        return 1000;
      case 'Pair':
        return 5000;
    }
  }
  
  // 利用人数を取得
  public getUserCount(): number {
    switch(this._type) {
      case 'Adult':
      case 'Child':
        return 1;
      case 'Pair':
        return 2;
    }
  }
  
  // 子供用アトラクション利用可否
  public isAvailableChildAttraction(): boolean {
    switch(this._type) {
      case 'Adult':
      case 'Pair':
        return false;
      case 'Child':
        return true;
    }
  }
}

なんだかswitch文だらけになっていますね。
もちろん「子供チケット以外かどうか」を判定させる場合等はif...elseを使っても問題ありませんが、いずれにせよ条件分岐が多い印象です。

問題は、ここに「種別で早割チケットを追加したい」場合や「大人専用アトラクションができたから判定処理を作りたい」場合のように新しい種別や処理が増えるごとに分岐がどんどん伸びていくことです。

現実の遊園地チケットも「シニアチケット」「団体チケット」「ネット予約チケット」のように種類が多岐に渡るため、今後種別が増えていくことは十分あり得ます。

ストラテジパターンで実装

そこで登場するのがストラテジパターンです。
ストラテジパターンはざっくり説明すると、アルゴリズムだけをクラスに切り出す考え方です。

まずアルゴリズムの定義だけをした基底となる「チケットストラテジ」を作り、各チケット種別に対応したアルゴリズムを実装した「XXXストラテジ」を作っていきます。

// チケットストラテジ
interface TicketStrategy {
    getPrice(): number;
    getUserCount(): number;
    isAvailableChildAttraction(): boolean;
  }

// 子供用チケットストラテジ
class ChildTicketStrategy implements TicketStrategy {
    getPrice = (): number => 1000; 
    getUserCount = (): number => 1;
    isAvailableChildAttraction = (): boolean => true;
}

// 大人用チケットストラテジ
class AdultTicketStrategy implements TicketStrategy {
    getPrice = (): number => 3000; 
    getUserCount = (): number => 1;
    isAvailableChildAttraction = (): boolean => false;
}

// ペアチケットストラテジ
class PairTicketStrategy implements TicketStrategy {
    getPrice = (): number => 5000; 
    getUserCount = (): number => 2;
    isAvailableChildAttraction = (): boolean => false;
}

class Ticket {
    private _strategy: TicketStrategy;
    constructor(strategy: TicketStrategy) {
        this._strategy = strategy;
    }

    getPrice = (): number => this._strategy.getPrice(); 
    getUserCount = (): number => this._strategy.getUserCount(); 
    isAvailableChildAttraction = (): boolean => this._strategy.isAvailableChildAttraction(); 
}

こうすることでアルゴリズム部分の取り回しが効きやすく、また新しく種別が増えた場合でも個別のストラテジを増やしていくことで対応できます。
要するにチケットの要件の変更や追加に対してメンテナンスしやすい設計となります。

ステートパターンとの違い

今回はTypescriptでデザインパターンの一つである「ストラテジパターン」について紹介しました。
ストラテジパターンはよく 「ステートパターン」 と似ているとされます。
というのもそれぞれのパターンを導入した場合の最終系のコードがほぼ同じだからです。

以下、私見になります。
ストラテジパターンが 「アルゴリズムの切り出しをポリモーフィズムで解決」 するアプローチを取っているのに対して、ステートパターンが 「状態による分岐をポリモーフィズムで解決」 するアプローチを取っています。

ポイントは「着目した点が違うだけで、解決方法としてポリモーフィズムを用いているという点は同じ」ということです。
従って 「起点となる思想は異なるがコードレベルに落とし込んだ場合の見た目は同じ」 ということになります。

ステートパターンについては、以下の記事でまとめています。

Discussion

hamazonhamazon

めちゃくちゃわかりやすい解説ありがとうございます!
本題とはそれてしまうのですが、「ストラテジーパターンなし」のサンプルコードにてbreakを記述しているのが気になりました。
直前にreturnしているのでそこで処理が止まり、breakには到達しないため不要なコードかと思います。
サンプルコードを真似て記述している際に気づいたので、もし間違っていなければ修正して頂けると🙏

nekonikinekoniki

コメントありがとうございます!

直前にreturnしているのでそこで処理が止まり、breakには到達しないため不要なコードかと思います。

こちら、まさしくご指摘の通りです。記事中の内容も修正しておきました。