8. ステートパターン (State)
オブジェクトの内部状態が変わる時、行動も一緒に変わるようにするステートパターンについて学びましょう。
ひとことで言うと
オブジェクトが「今どんな状態か」によって違う動きをしなければならない時、状態ごとにクラスを1つずつ作り、オブジェクトは今の状態に仕事を渡すだけにするパターンです。
問題: 状態が増えるほど大きくなる分かれ道
自販機を作るとしましょう。自販機は次の3つの状態のどれかです。
- コインなし: コインを待っているところ
- コインあり: ボタンを押せば飲み物が出るところ
- 売り切れ: 在庫がなくて何もできないところ
同じボタンを押しても、状態によってやることが違います。まず思いつくのは、状態を文字列で覚えておいて、メソッドごとに〜によって分けようで道を分ける方法です。1つの値を見ていくつもの道に分ける時、いちばんすっきりした書き方ですね。
【自販機】を設計しよう:
『状態』を【文字列】の「コインなし」にしよう
『在庫』を【数字】の0にしよう
最初に作られる時(【数字】の『在庫』)次のようにしよう:
『私』の『在庫』を『在庫』にしよう
もし(『在庫』が0と同じだ)なら:
『私』の『状態』を「売り切れ」にしよう
〈コインを入れる〉を作ろう():
『私』の『状態』によって分けよう:
「コインなし」の場合:
「コインを入れました。」を出力しよう
『私』の『状態』を「コインあり」にしよう
「コインあり」の場合:
「もうコインが入っています。」を出力しよう
「売り切れ」の場合:
「売り切れです。コインをお返しします。」を出力しよう
〈引く〉を作ろう():
『私』の『状態』によって分けよう:
「コインなし」の場合:
「先にコインを入れてください!」を出力しよう
「コインあり」の場合:
「飲み物が出ます!ガチャン!」を出力しよう
『私』の『在庫』を(『私』の『在庫』 - 1)にしよう
もし(『私』の『在庫』が0と同じだ)なら:
『私』の『状態』を「売り切れ」にしよう
それ以外なら:
『私』の『状態』を「コインなし」にしよう
「売り切れ」の場合:
「売り切れです。」を出力しよう
『機械』を【自販機】の新しい【自販機】(1)にしよう
『機械』の〈引く〉()を実行しよう
『機械』の〈コインを入れる〉()を実行しよう
『機械』の〈引く〉()を実行しよう
『機械』の〈コインを入れる〉()を実行しよう
実行してみるとちゃんと動きますし、コードもすっきりして見えます。
実行結果
先にコインを入れてください!
コインを入れました。
飲み物が出ます!ガチャン!
売り切れです。コインをお返しします。
ところが、このコードは育つにつれて痛くなります。「点検中」という状態をもう1つ足すことを考えてみてください。
〈コインを入れる〉にも〈引く〉にも、すべてのメソッドのによって分けように「点検中」の場合:を1つずつ足さなければなりません。メソッドが10個なら10か所です。- 1か所でも忘れると、その状態でボタンを押した時に何も起こりません。 合う道がなくてもエラーにならないので、見つけるのも大変です。
「コインあり」を「コイン有り」と1回書き間違えただけで、その道は黙って飛ばされます。- 「コインありの時、自販機はどう動くんだっけ?」を知るには、メソッドを全部見て回る必要があります。1つの状態の話があちこちに散らばっているからです。
によって分けようでもできないこと
によって分けようは、1つのメソッドの中の長いもしのはしごをすっきり整理してくれます。でも、いくつものメソッドで同じ分け方がくり返されることは直してくれません。状態が増えた時に痛いのは、まさにこのくり返しです。
解決策: 状態をオブジェクトにする
ステートパターンは考え方を逆にします。「メソッドの中で状態を分ける」のではなく、「状態の中にメソッドを集める」のです。
- すべての状態が守る約束、つまり規定を作ります。状態なら
〈コインを入れる〉と〈引く〉ができなければなりません。 - 状態ごとにクラスを1つずつ作り、その状態の時にやることだけを書きます。
- 自販機は
『現在の状態』を1つだけ覚えておき、ボタンが押されたら今の状態にそのまま渡します。 - 状態を変えたい時は、
『現在の状態』に別の状態オブジェクトを差し込みます。
たとえ: スマホのマナーモード
スマホに電話がかかってくること自体はいつも同じなのに、着信音モードなら鳴り、バイブモードなら震え、サイレントモードなら画面が光るだけです。スマホが「もし着信音モードなら…」を毎回確かめているのではなく、モードのボタンで反応のしかたそのものを差し替えていると考えてください。ステートパターンの状態オブジェクトは、まさにこの「モード」です。
だれが何をするの?
| 役割 | この例では | やること |
|---|---|---|
| 状態の約束 | 【状態】(規定) |
すべての状態が持つべきメソッドを決めます |
| 具体的な状態 | 【コインなし状態】、【コインあり状態】、【売り切れ状態】 |
その状態の時の動きと、次の状態に移るタイミングを知っています |
| 文脈(持ち主) | 【自販機】 |
今の状態を1つ持っていて、仕事を渡すだけです |
カナデで作る
同じ自販機をステートパターンで作り直してみましょう。コードの中の(参考:ステップn)に沿って読んでみてください。
(参考:ステップ1 — すべての状態が守る約束)
【状態】を規定しよう:
〈コインを入れる〉がなければならない()
〈引く〉がなければならない()
(参考:ステップ2 — 自販機は今の状態に仕事を渡すだけ)
【自販機】を設計しよう:
『現在の状態』を【状態】の空っぽに隠そう
『在庫』を【数字】の0にしよう
最初に作られる時(【数字】の『在庫』)次のようにしよう:
『私』の『在庫』を『在庫』にしよう
もし(『在庫』が0と同じだ)なら:
『私』の『現在の状態』を新しい【売り切れ状態】()にしよう
それ以外なら:
『私』の『現在の状態』を新しい【コインなし状態】(『私』)にしよう
〈状態を変える〉を作ろう(【状態】の『新しい状態』):
『私』の『現在の状態』を『新しい状態』にしよう
〈コインを入れる〉を作ろう():
『私』の『現在の状態』の〈コインを入れる〉()を実行しよう
〈引く〉を作ろう():
『私』の『現在の状態』の〈引く〉()を実行しよう
(参考:ステップ3 — 状態ごとにクラスを1つ、その状態でやることだけを書きます)
【状態】に従う【コインなし状態】を設計しよう:
『機械』を【自販機】の空っぽに隠そう
最初に作られる時(【自販機】の『機械』)次のようにしよう:
『私』の『機械』を『機械』にしよう
〈コインを入れる〉を作ろう():
「コインを入れました。」を出力しよう
『私』の『機械』の〈状態を変える〉(新しい【コインあり状態】(『私』の『機械』))を実行しよう
〈引く〉を作ろう():
「先にコインを入れてください!」を出力しよう
【状態】に従う【コインあり状態】を設計しよう:
『機械』を【自販機】の空っぽに隠そう
最初に作られる時(【自販機】の『機械』)次のようにしよう:
『私』の『機械』を『機械』にしよう
〈コインを入れる〉を作ろう():
「もうコインが入っています。」を出力しよう
〈引く〉を作ろう():
「飲み物が出ます!ガチャン!」を出力しよう
『私』の『機械』の『在庫』を(『私』の『機械』の『在庫』 - 1)にしよう
もし(『私』の『機械』の『在庫』が0と同じだ)なら:
『私』の『機械』の〈状態を変える〉(新しい【売り切れ状態】())を実行しよう
それ以外なら:
『私』の『機械』の〈状態を変える〉(新しい【コインなし状態】(『私』の『機械』))を実行しよう
【状態】に従う【売り切れ状態】を設計しよう:
〈コインを入れる〉を作ろう():
「売り切れです。コインをお返しします。」を出力しよう
〈引く〉を作ろう():
「売り切れです。」を出力しよう
『機械』を【自販機】の新しい【自販機】(1)にしよう
『機械』の〈引く〉()を実行しよう
『機械』の〈コインを入れる〉()を実行しよう
『機械』の〈引く〉()を実行しよう
『機械』の〈コインを入れる〉()を実行しよう
結果は前のコードと1文字も変わりません。 変わったのはコードの形だけです。このように、外から見た動きはそのままで中の構造だけを直すことをリファクタリングと呼びます。
実行結果
先にコインを入れてください!
コインを入れました。
飲み物が出ます!ガチャン!
売り切れです。コインをお返しします。
ステップごとにもう一度
- ステップ1 — 約束を作る。
【状態】を規定しようは「状態ならこのメソッドが必ずなきゃダメ」という約束です。新しい状態を作る時に〈引く〉を忘れると、カナデがすぐに教えてくれます(下の例)。前のコードのように、黙って何も起こらないということがなくなります。 - ステップ2 — 自販機は仕事を渡すだけ。
【自販機】の〈コインを入れる〉と〈引く〉には、によって分けようももしもありません。今の状態が何であっても、『現在の状態』にそのまま渡します。 - ステップ3 — 状態ごとにクラスを1つ。 「コインありの時はどう動くんだっけ?」が気になったら、
【コインあり状態】だけを見れば大丈夫です。次にどの状態に移るかも、状態が自分で決めます。
約束を破るとどうなるか、直接見てみましょう。〈引く〉を忘れた状態を作ると、その状態を使う前に、クラスを作った時点でエラーになります。
【状態】を規定しよう:
〈コインを入れる〉がなければならない()
〈引く〉がなければならない()
【状態】に従う【点検中状態】を設計しよう:
〈コインを入れる〉を作ろう():
「点検中です。」を出力しよう
【売り切れ状態】にはなぜ『機械』がないの?
売り切れは終わりの状態なので、ほかの状態に移ることがありません。自販機を変える必要がないので、覚えておく必要もないんです。状態オブジェクトは、本当に必要なものだけを持っていれば大丈夫です。
新しい状態を足してみると
この構造の本当の力は、状態をもう1つ足す時に見えてきます。「点検中」の状態を作るなら:
- 前のやり方: すべてのメソッドを開いて
「点検中」の場合:を足さなければなりません。 - ステートパターン:
【状態】に従う【点検中状態】という新しいクラスを1つ作るだけです。すでにある状態クラスはほとんど触りません。
やってみよう
上のコードに【点検中状態】を足してみてください。コインを入れると「点検中です。」を出力し、引いても何も出てきません。それから【自販機】に〈点検を始める〉を作って、今の状態を【点検中状態】に変えてみましょう。今ある状態クラスは1つも直さなくていいはずです。
どんな時に使うといいの?
- オブジェクトが状態によってまったく違う動きをし、状態が3つ以上ある、またはこれから増えそうな時
- いくつものメソッドで同じ状態の値による分かれ道(
によって分けようやもしのはしご)がくり返される時 - 「この状態からあの状態に移るルール」が複雑で、ひと目で見渡したい時
いいところと気になるところ
| いいところ | 気になるところ |
|---|---|
| 1つの状態の動きがクラス1つにまとまっていて読みやすいです | クラスの数が増えます |
| 新しい状態を足す時、今あるコードをほとんど直しません | 状態が2つだけで増える予定もないなら、かえってやりすぎです |
| 規定のおかげで、書き忘れた動きをカナデが見つけてくれます | 状態どうしがお互いを知る必要があり(次の状態を作るため)、絡まることがあります |
| 状態の値を文字列で比べないので、打ち間違いで間違えることがありません |
早すぎるパターンは毒です。
状態が「オン/オフ」の2つだけでメソッドも1つなら、もし1回のほうが読みやすいです。分かれ道が本当に痛くなり始めてからステートパターンに移っても、遅くはありません。
ほかのパターンとの関係
- ストラテジーパターンに似ています。 どちらも「規定に従うオブジェクトを差し替える」形です。違いはだれが差し替えるかです。ストラテジーは外側(使う側)が選んで差し込み、ストラテジーどうしはお互いを知りません。ステートは状態が自分で次の状態に移ります。
- シングルトンパターンと一緒に使えます。
【売り切れ状態】のように何も覚えていない状態は、毎回新しく作らなくても1つだけ作ってみんなで使えます。 - コマンドパターンとも相性がいいです。 ボタンを押すことをコマンドオブジェクトにして、そのコマンドが今の状態に仕事を渡すようにすると、「元に戻す」も付けやすくなります。