9. コマンドパターン (Command)
要求自体をオブジェクトとしてカプセル化して伝達し実行するコマンドパターンについて学びましょう。
ひとことで言うと
「やるべきこと」の1つ1つをオブジェクトにするパターンです。仕事がオブジェクトになると、実行を後回しにしたり、積んでおいたり、元に戻すを付けたりしやすくなります。
問題: やり方と戻し方がばらばらのコード
かんたんなメモ帳を作るとしましょう。できることは2つです。
- 書く: 最後に文字をつなげます。
- 消す: 最後から文字をいくつか消します。
そしてメモ帳なら、元に戻すが絶対に必要ですよね。まず思いつくのは、エディターが何かするたびに「何をしたか」を記録しておき、戻す時にその記録を見て逆のことをする方法です。
【エディター】を設計しよう:
『内容』を【文字列】の「」にしよう
『記録』を【】に隠そう
〈書く〉を作ろう(【文字列】の『文字』):
『私』の『内容』を(『私』の『内容』 + 『文字』)にしよう
『私』の『記録』に{「種類」: 「書く」, 「文字」: 『文字』}を追加しよう
〈消す〉を作ろう(【数字】の『個数』):
『長さ』を『私』の『内容』の『長さ』にしよう
『消した文字』を『私』の『内容』の〈切り取り〉(『長さ』 - 『個数』 + 1, 『長さ』)にしよう
『私』の『内容』を『私』の『内容』の〈切り取り〉(1, 『長さ』 - 『個数』)にしよう
『私』の『記録』に{「種類」: 「消す」, 「文字」: 『消した文字』}を追加しよう
(参考:元に戻すには、すべての動作を逆にする方法を1か所で全部知っていなければなりません)
〈元に戻す〉を作ろう():
『最後』を『私』の『記録』の後から取り出した値にしよう
(『最後』の「種類」)によって分けよう:
「書く」の場合:
『長さ』を『私』の『内容』の『長さ』にしよう
『私』の『内容』を『私』の『内容』の〈切り取り〉(1, 『長さ』 - (『最後』の「文字」)の『長さ』)にしよう
「消す」の場合:
『私』の『内容』を(『私』の『内容』 + (『最後』の「文字」))にしよう
『メモ』を新しい【エディター】()にしよう
『メモ』の〈書く〉(「こんにちは」)を実行しよう
『メモ』の〈書く〉(「世界」)を実行しよう
『メモ』の〈消す〉(1)を実行しよう
枠「今: {『メモ』の『内容』}」を出力しよう
『メモ』の〈元に戻す〉()を実行しよう
枠「戻した後: {『メモ』の『内容』}」を出力しよう
『メモ』の〈元に戻す〉()を実行しよう
枠「もう一度: {『メモ』の『内容』}」を出力しよう
実行結果
今: こんにちは世
戻した後: こんにちは世界
もう一度: こんにちは
元に戻すがちゃんと動きます。でも、できることが増えると痛くなります。
- やり方と戻し方が離れています。「消す」のやり方は
〈消す〉に、消すを元に戻す方法は〈元に戻す〉のによって分けようの中にあります。1つの仕事の話が2か所に分かれています。 - 新しい動作のたびに2か所を直さなければなりません。「貼り付け」や「全部大文字に」を入れるには、新しいメソッドを作り、
〈元に戻す〉にも場合を1つ足す必要があります。元に戻す側を忘れると、合う道がないので黙って何も起こりません。 - 記録が文字の名札でできています。
「種類」や「書く」のような名前を1文字でも間違えると、エラーにならないまま食い違います。 - エディターがやることが多すぎます。 文字を扱うこと、記録すること、元に戻すことが、全部
【エディター】1つのクラスに入っています。記録が空の時に元に戻すを押したらどうするか、といったことまでここにくっつきます。
解決策: やることをオブジェクトにする
コマンドパターンは、「書く」と「消す」をメソッドではなくオブジェクトにします。
- エディターは文字をつなげて消す基本機能だけを持ちます。
- コマンドなら守る約束、つまり規定を作ります。コマンドは
〈実行する〉と〈取り消す〉ができなければなりません。 - 動作ごとにコマンドクラスを1つずつ作ります。やり方と戻し方が1つのクラスの中に並び、戻す時に必要なもの(消した文字)はコマンドが自分で覚えておきます。
- 履歴係はコマンドを実行して積んでおくだけです。戻す時はいちばん上のコマンドを取り出して
〈取り消す〉を呼べば終わりです。それがどんなコマンドかは知らなくてかまいません。
たとえ: レストランの注文票
お客さんが注文すると、店員は注文票に書いてキッチンに渡します。注文票の1枚1枚が「やるべきこと」です。注文票があるので、順番に積んでおくこともできますし、お客さんが「さっきのあれ、キャンセルしてください」と言えば、その注文票を探して取り消すこともできます。店員は料理の作り方を知らなくてかまいません。注文票を受け取って渡すだけでいいからです。注文票こそが「コマンド」です。
だれが何をするの?
| 役割 | この例では | やること |
|---|---|---|
| コマンドの約束 | 【コマンド】(規定) |
すべてのコマンドができなければならないこと(実行、取り消し)を決めます |
| 具体的なコマンド | 【書くコマンド】、【消すコマンド】 |
やり方と戻し方を一緒に知っていて、必要なものを覚えています |
| 受け取る側 | 【エディター】 |
実際に文字を扱います。コマンドがこのオブジェクトに仕事をさせます |
| 呼び出す側 | 【履歴係】 |
コマンドを実行して積んでおき、戻す時に取り出します |
カナデで作る
同じメモ帳をコマンドパターンで作り直してみましょう。最後には、もう戻せるものがない時にも押してみました。
(参考:ステップ1 — エディターは文字をつなげて消す基本機能だけを持ちます)
【エディター】を設計しよう:
『内容』を【文字列】の「」にしよう
〈つなげる〉を作ろう(【文字列】の『文字』):
『私』の『内容』を(『私』の『内容』 + 『文字』)にしよう
【文字列】を返す〈後ろから消す〉を作ろう(【数字】の『個数』):
『長さ』を『私』の『内容』の『長さ』にしよう
『消した文字』を『私』の『内容』の〈切り取り〉(『長さ』 - 『個数』 + 1, 『長さ』)にしよう
『私』の『内容』を『私』の『内容』の〈切り取り〉(1, 『長さ』 - 『個数』)にしよう
『消した文字』を返そう
(参考:ステップ2 — コマンドなら、やり方と戻し方を一緒に知っています)
【コマンド】を規定しよう:
〈実行する〉がなければならない()
〈取り消す〉がなければならない()
(参考:ステップ3 — 動作ごとにコマンドを1つ。戻す時に必要なものはコマンドが覚えておきます)
【コマンド】に従う【書くコマンド】を設計しよう:
『エディター』を【エディター】の空っぽに隠そう
『文字』を【文字列】の「」に隠そう
最初に作られる時(【エディター】の『エディター』, 【文字列】の『文字』)次のようにしよう:
『私』の『エディター』を『エディター』にしよう
『私』の『文字』を『文字』にしよう
〈実行する〉を作ろう():
『私』の『エディター』の〈つなげる〉(『私』の『文字』)を実行しよう
〈取り消す〉を作ろう():
『私』の『エディター』の〈後ろから消す〉(『私』の『文字』の『長さ』)を実行しよう
【コマンド】に従う【消すコマンド】を設計しよう:
『エディター』を【エディター】の空っぽに隠そう
『個数』を【数字】の0に隠そう
『消した文字』を【文字列】の「」に隠そう
最初に作られる時(【エディター】の『エディター』, 【数字】の『個数』)次のようにしよう:
『私』の『エディター』を『エディター』にしよう
『私』の『個数』を『個数』にしよう
〈実行する〉を作ろう():
『私』の『消した文字』を『私』の『エディター』の〈後ろから消す〉(『私』の『個数』)にしよう
〈取り消す〉を作ろう():
『私』の『エディター』の〈つなげる〉(『私』の『消した文字』)を実行しよう
(参考:ステップ4 — 履歴係はコマンドを実行して積んでおくだけです。何のコマンドかは知りません)
【履歴係】を設計しよう:
『これまでのコマンド』を【】に隠そう
〈する〉を作ろう(【コマンド】の『コマンド』):
『コマンド』の〈実行する〉()を実行しよう
『私』の『これまでのコマンド』に『コマンド』を追加しよう
〈元に戻す〉を作ろう():
もし(『私』の『これまでのコマンド』の『長さ』が0と同じだ)なら:
「戻せるものがありません。」を出力しよう
返そう
『最後』を『私』の『これまでのコマンド』の後から取り出した値にしよう
『最後』の〈取り消す〉()を実行しよう
『メモ』を新しい【エディター】()にしよう
『履歴』を新しい【履歴係】()にしよう
『履歴』の〈する〉(新しい【書くコマンド】(『メモ』, 「こんにちは」))を実行しよう
『履歴』の〈する〉(新しい【書くコマンド】(『メモ』, 「世界」))を実行しよう
『履歴』の〈する〉(新しい【消すコマンド】(『メモ』, 1))を実行しよう
枠「今: {『メモ』の『内容』}」を出力しよう
『履歴』の〈元に戻す〉()を実行しよう
枠「戻した後: {『メモ』の『内容』}」を出力しよう
『履歴』の〈元に戻す〉()を実行しよう
枠「もう一度: {『メモ』の『内容』}」を出力しよう
『履歴』の〈元に戻す〉()を実行しよう
『履歴』の〈元に戻す〉()を実行しよう
実行結果
今: こんにちは世
戻した後: こんにちは世界
もう一度: こんにちは
戻せるものがありません。
最初の3行はリファクタリング前と同じです。3回目の元に戻すで「こんにちは」まで消えてメモが空になり、4回目にはもう戻せるものがないと教えてくれます。
ステップごとにもう一度
- ステップ1 — エディターは基本機能だけ。
【エディター】は記録も、元に戻すことも知りません。文字をつなげて、最後から消した文字を返すだけです。 - ステップ2 — 約束を作る。
【コマンド】は「実行できて、取り消しもできればコマンド」という約束です。新しいコマンドを作る時に〈取り消す〉を忘れると、カナデがすぐに教えてくれます。前のやり方のように、元に戻すが黙って効かないということがなくなります。 - ステップ3 — コマンドごとに自分の話を1か所に。
【消すコマンド】は〈実行する〉の時に消した文字を『消した文字』に覚えておき、〈取り消す〉の時にその文字をまたつなげます。やり方と戻し方が1つのクラスの中に並んでいます。 - ステップ4 — 呼び出す側は中身を知らなくていい。
【履歴係】はコマンドを実行してリストに積むだけです。戻す時はいちばん後ろのコマンドを取り出して〈取り消す〉を呼びます。書くのか消すのかは気にしません。
どうして後ろから戻すの?
最後にしたことが、いちばん先に戻されなければならないからです。「こんにちは」→「世界」→1文字消す、の順にしたなら、戻す時は 消す →「世界」→「こんにちは」の順でないと、メモが正しく戻りません。積み重ねたお皿を上から取るのと同じです。
新しいコマンドを足してみると
「全部消す」を入れるとしましょう。
- 前のやり方:
【エディター】に〈全部消す〉を作り、〈元に戻す〉のによって分けようにも「全部消す」の場合:を入れなければなりません。 - コマンドパターン:
【コマンド】に従う【全部消すコマンド】を1つだけ作れば済みます。〈実行する〉で今の内容を覚えてから空にし、〈取り消す〉で覚えた内容をつなげ直せば終わりです。【エディター】も【履歴係】もそのままです。
やってみよう
- 上のコードに
【全部消すコマンド】を足してみてください。【エディター】の〈後ろから消す〉に今の文字数を渡せば、全部消せます。- もっとやってみたいなら、
【履歴係】にやり直しを付けてみましょう。〈元に戻す〉で取り出したコマンドを『戻したコマンド』リストに積んでおき、〈やり直す〉でそこから取り出して〈実行する〉すればいいんです。コマンドがオブジェクトだからこそできることです。
どんな時に使うといいの?
- 元に戻す(そしてやり直し)が必要な時
- やることをすぐにはせず積んでおき、後で、または順番に実行したい時(作業の待ち行列、予約実行)
- ボタン、ショートカット、メニューのようにいくつもの場所から同じ仕事をさせる時。同じコマンドオブジェクトを分け合えば済みます
- やったことを記録したり、もう一度再生したりしたい時(マクロ)
いいところと気になるところ
| いいところ | 気になるところ |
|---|---|
| やり方と戻し方が1つのクラスにまとまっています | 動作1つごとにクラスが1つずつ増えます |
| 新しい動作を足しても、呼び出す側(履歴係)を直しません | コマンドが受け取る側(エディター)を知る必要があり、つながりが1つ増えます |
| 元に戻す、やり直し、積んでおく、後で実行するのが簡単です | 元に戻すを正確に作るには、「何を覚えておくか」をよく決める必要があります |
規定のおかげで、〈取り消す〉を忘れるとすぐに教えてくれます |
早すぎるパターンは毒です。
元に戻すも、積んでおくことも必要なく、ボタン1つが1つの仕事しかしないなら、その仕事を直接呼ぶのがいちばん読みやすいです。「元に戻すを付けてください」と言われた時が、コマンドパターンに移す時です。
ほかのパターンとの関係
- ストラテジーパターンと形が似ています。 どちらも仕事をオブジェクトに切り出します。ストラテジーはどうやってするか(方法)を差し替えることに集中し、コマンドは何をするか(1回の仕事)をオブジェクトにして、積んだり、後回しにしたり、戻したりすることに集中します。
- オブザーバーパターンと一緒に使えます。 何か起きた時に実行するコマンドたちをリストに購読させておけば、知らせが来た時に全部実行されます。
- デコレーターパターンでコマンドを包めます。 コマンドを実行する前後に記録を残す殻をかぶせれば、コマンドのクラスを直さなくても、すべてのコマンドの記録が残せます。