カナデKanade

3. ストラテジーパターン (Strategy)

行動をインターフェースに分離して実行中に自由に入れ替えるストラテジーパターンについて学びましょう。

ひとことで言うと
同じ仕事をするいくつもの方法をそれぞれクラスに切り出し、使う側はその中から1つを選んで差し込んで使うパターンです。方法を変えたければ、別のものに差し替えるだけです。

問題: 方法が増えるたびに大きくなるクラス

お店のレジを作るとしましょう。最初は割引がありませんでしたが、お店が繁盛するにつれて割引の方法が増えてきます。

  • 割引なし: 金額そのまま
  • 割合割引: 10%のように割合の分だけ引く
  • クーポン割引: 3000円のように決まった金額だけ引く

まず思いつくのは、レジが割引方法の名前を覚えておいて、〜によって分けようで計算を分ける方法です。

【レジ】を設計しよう:
    『割引方式』を【文字列】の「なし」にしよう
    『割引率』を【数字】の0にしよう
    『クーポン額』を【数字】の0にしよう

    〈最終金額〉を作ろう(【数字】の『金額』):
        『私』の『割引方式』によって分けよう:
            「なし」の場合:
                『金額』を返そう
            「割合」の場合:
                (『金額』 - 『金額』 * 『私』の『割引率』 / 100)を返そう
            「クーポン」の場合:
                もし(『金額』が『私』の『クーポン額』より小さい)なら:
                    0を返そう
                (『金額』 - 『私』の『クーポン額』)を返そう
        『金額』を返そう

『レジ1』を【レジ】の新しい【レジ】()にしよう
枠「割引なし: {『レジ1』の〈最終金額〉(20000)}円」を出力しよう

『レジ1』の『割引方式』を「割合」にしよう
『レジ1』の『割引率』を10にしよう
枠「10%割引: {『レジ1』の〈最終金額〉(20000)}円」を出力しよう

『レジ1』の『割引方式』を「クーポン」にしよう
『レジ1』の『クーポン額』を3000にしよう
枠「3000円クーポン: {『レジ1』の〈最終金額〉(20000)}円」を出力しよう

実行結果
割引なし: 20000円
10%割引: 18000円
3000円クーポン: 17000円

ちゃんと動きます。分けているのも〈最終金額〉の1か所だけなので、ステートパターンで見た「すべてのメソッドで同じ分け方がくり返される」問題もありません。ところが、割引の方法が増えると別のところが痛くなります。

  • レジがすべての割引方法を知っていなければなりません。「2つ買うと1つ無料」「会員ランク割引」ができるたびに、【レジ】クラスを開いて直す必要があります。レジの仕事は「計算すること」なのに、割引イベントができるたびにレジが大きくなります。
  • 使わない値がたまります。 クーポンを使っている間も、『割引率』はそのまま残っています。割引方法が5つになれば、そのうち1つしか使わないのに、値は5種類レジにくっついています。
  • 組み合わせが合わなくても静かです。『割引方式』を「割合」に変えて『割引率』を決め忘れると、0%で計算されます。「パーセント」のようにない名前を書くと、最後の行のせいで割引なしで計算されます。どちらもエラーになりません。
  • 割引1つだけを別に試すのが大変です。 クーポンの計算が合っているか確かめるには、レジを丸ごと作って、方式の名前と値をそろえて入れなければなりません。

解決策: 方法をオブジェクトとして切り出す

ストラテジーパターンは、「どう割引するか」をレジの外に取り出します。

  1. すべての割引方法が守る約束、つまり規定を作ります。割引なら、金額を受け取って〈適用する〉ができなければなりません。
  2. 割引方法ごとにクラスを1つずつ作ります。その方法に必要な値(割引率、クーポン額)は、そのクラスが自分で持ちます。
  3. レジは『割引』を1つだけ持っていて、計算する時にその割引に任せます。
  4. 割引方法を変えたければ、使う側が別の割引オブジェクトを選んで差し込みます。

たとえ: 乗り換え案内アプリ

乗り換え案内アプリで、目的地はそのままでも徒歩、車、電車のどれを選ぶかでルートが変わります。アプリは「どう行くか」をボタン1つで差し替えるだけで、検索画面そのものは変わりません。新しい移動手段(自転車)ができても、画面はそのままでルートの計算方法を1つ足すだけです。この移動手段の1つ1つが「ストラテジー(戦略)」です。

だれが何をするの?

役割 この例では やること
戦略の約束 【割引】(規定) すべての割引方法が持つべきメソッドを決めます
具体的な戦略 【割引なし】、【割合割引】、【クーポン割引】 割引を実際に計算し、必要な値を自分で持っています
文脈(使う場所) 【レジ】 戦略を1つ持っていて、計算を任せます
選ぶ側 プログラムのいちばん下 どの戦略を差し込むか決めます

カナデで作る

同じレジをストラテジーパターンで作り直してみましょう。コードの中の(参考:ステップn)に沿って読んでみてください。

(参考:ステップ1 — すべての割引方法が守る約束)
【割引】を規定しよう:
    〈適用する〉がなければならない(【数字】の『金額』)

(参考:ステップ2 — 割引方法ごとにクラスを1つ、必要な値は自分で持ちます)
【割引】に従う【割引なし】を設計しよう:
    〈適用する〉を作ろう(【数字】の『金額』):
        『金額』を返そう

【割引】に従う【割合割引】を設計しよう:
    『割引率』を【数字】の0にしよう

    最初に作られる時(【数字】の『割引率』)次のようにしよう:
        『私』の『割引率』を『割引率』にしよう

    〈適用する〉を作ろう(【数字】の『金額』):
        (『金額』 - 『金額』 * 『私』の『割引率』 / 100)を返そう

【割引】に従う【クーポン割引】を設計しよう:
    『クーポン額』を【数字】の0にしよう

    最初に作られる時(【数字】の『クーポン額』)次のようにしよう:
        『私』の『クーポン額』を『クーポン額』にしよう

    〈適用する〉を作ろう(【数字】の『金額』):
        もし(『金額』が『私』の『クーポン額』より小さい)なら:
            0を返そう
        (『金額』 - 『私』の『クーポン額』)を返そう

(参考:ステップ3 — レジは割引を1つ持っていて、任せるだけ)
【レジ】を設計しよう:
    『割引』を【割引】の新しい【割引なし】()に隠そう

    〈割引を変える〉を作ろう(【割引】の『新しい割引』):
        『私』の『割引』を『新しい割引』にしよう

    〈最終金額〉を作ろう(【数字】の『金額』):
        『私』の『割引』の〈適用する〉(『金額』)を返そう

『レジ1』を【レジ】の新しい【レジ】()にしよう
枠「割引なし: {『レジ1』の〈最終金額〉(20000)}円」を出力しよう

『レジ1』の〈割引を変える〉(新しい【割合割引】(10))を実行しよう
枠「10%割引: {『レジ1』の〈最終金額〉(20000)}円」を出力しよう

『レジ1』の〈割引を変える〉(新しい【クーポン割引】(3000))を実行しよう
枠「3000円クーポン: {『レジ1』の〈最終金額〉(20000)}円」を出力しよう

実行結果
割引なし: 20000円
10%割引: 18000円
3000円クーポン: 17000円

結果は前のコードと同じです。外から見た動きはそのままで、中の構造だけを変えたリファクタリングです。

ステップごとにもう一度

  • ステップ1 — 約束を作る。【割引】を規定しようは「割引なら〈適用する〉が必ずなきゃダメ」という約束です。レジは割引がどのクラスなのか知らなくても、この約束だけを信じて〈適用する〉を呼びます。
  • ステップ2 — 方法ごとにクラスを1つ。『割引率』は【割合割引】の中だけに、『クーポン額』は【クーポン割引】の中だけにあります。使わない値がレジにたまりません。それに、新しい【割合割引】(10)のように作る時に値を必ず入れるので、割引率を忘れることもありません。
  • ステップ3 — レジは任せるだけ。【レジ】の〈最終金額〉は1行です。割引方法が10個になっても、この行はそのままです。

見当違いのものを差し込むとどうなるでしょう?〈割引を変える〉は【割引】の『新しい割引』だけを受け取ると決めたので、前のコードのように割引方法の名前(文字列)を渡すと、すぐにエラーになります。

【割引】を規定しよう:
    〈適用する〉がなければならない(【数字】の『金額』)

【割引】に従う【割引なし】を設計しよう:
    〈適用する〉を作ろう(【数字】の『金額』):
        『金額』を返そう

【レジ】を設計しよう:
    『割引』を【割引】の新しい【割引なし】()に隠そう

    〈割引を変える〉を作ろう(【割引】の『新しい割引』):
        『私』の『割引』を『新しい割引』にしよう

『レジ1』を【レジ】の新しい【レジ】()にしよう
『レジ1』の〈割引を変える〉(「クーポン」)を実行しよう

新しい割引を足してみると

「どんな時でも1000円引き」のイベントができたとしましょう。

  • 前のやり方: 【レジ】を開いて「千円」の場合:を足さなければなりません。割引額が変わるかもしれないなら、レジに新しい値もくっつける必要があります。
  • ストラテジーパターン: 【割引】に従う【千円割引】という新しいクラスを1つ作るだけです。【レジ】は1文字も直しません。ほかのファイルやほかの人が作った割引も、約束さえ守ればそのまま差し込めます。

やってみよう
上のコードに【千円割引】を足してみてください。金額から1000円を引きますが、0円より小さくはならないようにします。そして『レジ1』の〈割引を変える〉(新しい【千円割引】())で差し込んで、結果を確かめてみましょう。【レジ】は直さなくていいはずです。

もう使ったことのあるストラテジーパターン: 関数を渡す

実はストラテジーパターンは、標準ライブラリ2ですでに使っています。〈基準並べ替え〉は並べ替えのしかたは自分で知っていて、「何を基準に並べるか」だけを関数で受け取ります。渡す関数こそが戦略です。

【リスト】から〈基準並べ替え〉を持ってこよう

〈短い順〉を作ろう(『文』):
    『文』の『長さ』を返そう

〈長い順〉を作ろう(『文』):
    (0 - 『文』の『長さ』)を返そう

『果物』を【「りんご」, 「柿」, 「さくらんぼ」】にしよう

〈基準並べ替え〉(『果物』, 〈短い順〉)を出力しよう
〈基準並べ替え〉(『果物』, 〈長い順〉)を出力しよう

実行結果
[柿, りんご, さくらんぼ]
[さくらんぼ, りんご, 柿]

クラスにする?関数を渡す?
戦略が関数1つで済み、覚えておく値もないなら、関数を渡すのがいちばん簡単です。割引率のように覚えておく値がある時や、戦略のやることがいくつもある時は、規定とクラスで作るのがおすすめです。

どんな時に使うといいの?

  • 同じ仕事をする方法がいくつもあり、これからもっと増えそうな時
  • 方法ごとに必要な値が違い、1つのクラスにまとめると使わない値がたまる時
  • プログラムが動いている途中で方法を差し替えたい時(ユーザー設定、イベント期間など)
  • 方法を作る人と使う人が違う時(別のファイル、別のパッケージ)

いいところと気になるところ

いいところ 気になるところ
新しい方法を足しても、使う側(レジ)を直しません クラスの数が増えます
方法ごとに必要な値がそのクラスの中だけにあります 使う側がどんな戦略があるかを知って選ぶ必要があります
方法を1つだけ別に作って試しやすいです 方法が2〜3個で増える予定もないなら、によって分けようのほうが簡単です
実行中に方法を差し替えられます

早すぎるパターンは毒です。
割引が「なし」と「10%」の2つだけで変わる予定もないなら、もし1回のほうが読みやすいです。方法が増え始めたり、方法ごとに値がくっつき始めたりしてからストラテジーパターンに移っても、遅くはありません。

ほかのパターンとの関係

  • ステートパターンと形が同じです。 どちらも「規定に従うオブジェクトを差し替える」構造です。違いはだれが差し替えるかです。ストラテジーは使う側が選んで差し込み、ストラテジーどうしはお互いを知りません。ステートは状態が自分で次の状態に移ります。
  • ファクトリーパターンと一緒に使うと便利です。「VIPなら割合割引、クーポンがあればクーポン割引」のように、どの戦略を作るかを決める仕事をファクトリーに任せれば、選ぶコードも1か所にまとまります。
  • デコレーターパターンと比べてみましょう。 ストラテジーは仕事の中身(方法)を変え、デコレーターは仕事を外側から包んで機能を足します。
プレイグラウンド
結果
コードを書いて「実行」ボタンを押してください。