はじめに
「C言語でトライ! デザインパターン」
今回はCommandパターン。考え方は使えますが、適用という意味ではCとは相性が悪そうです。ただ手を動かすと見えるものがありますね。
デザインパターン一覧
作成したライブラリパッケージの説明
公開コードサンプルはこちら, ライブラリパッケージはこちら
Commandパターン
wikipediaの説明は以下。
Command パターン(英: command pattern)はオブジェクト指向プログラミングにおけるデザインパターンの一つで、動作を表現するオブジェクトを示す。Command オブジェクトは、動作とそれに伴うパラメータをカプセル化したものである。
この文章から、最初はインスタンス作成者がコンストラクタ等初期設定を頑張り、使用者は"コマンド実行!"ってだけで終わり。かと思っていましたがクラス図的にはもっと奥が深そうです。
まずしっくり来たサイトはこちらの方の説明。
食堂の例を参考に、ちょっと舞台をラーメン屋に変えさせてもらいます。
登場人物 | クラス図 |
---|---|
客 | Client |
新人バイト | Invoker |
調理場の親方 | Receiver |
食券制度 | Command |
客の購入した食券(注文) | ConcreteCommand |
食券が調理場に届く | Command.execute() |
こんな順番で調理が行われます。
- 客は食券を購入し、新人バイトに食券を渡す
- 新人バイトは食券をそのまま調理場に食券を届けます。
- 親方は客の注文に従い調理。料理完成。
新人バイトは食券を届ける以外に皿洗いやお冷の入れ替え等もしています(本編と関係なし
客(Client)は食券を購入(ConcreteCommandの作成)。調理は親方(Receiver)に委ねられているので注文(ConcreteCommand)は親方(Receiver)が紐づきます。
新人バイト(Invoker)は親方が何をしているか知らなくても、調理場に食券を届ける(Command.execute())だけでお仕事完了です。
また、新人バイト(Invoker)の出来ることはどのお店でも変わらないので、食券制度導入店ならやることは同じ(Command.execute())です。
- Invokerは単純なCommand.execute()実行だけ。変更はなし
- Clientの作成するConcreteCommandはキーとなるReceiverを利用してexecuteを実現。
- ConcreteCommandを差し替えればReceiverの扱い方が変わりCommand.execute()結果も変わる。(でもReceiver変更はいらない)
とこんな感じでしょうか。
後はお店を変えれば醤油ラーメンの味が変わるように、ReceiverとConcreteCommandの組み合わせで色々な操作を表現できるのも魅力のですかね。
また、コードを書いて思ったのはConcreteCommandがReceiverを利用していることが一目でわかるってのもメリットだなと思いました。
例えば
ConcreteCommand command = ConcreteCommandNew(&receiver);
生成部分を見ただけで「ConcreteCommandNewとreceiverが関係している」というのがわかります。
こういう関連性の見える化って大事ですね。
使用例? Docker
Commandパターンの例、世の中に何かないかなと思って思いついたのがDocker。こんな組み合わせになるのかなと思っています。
登場人物 | クラス図 |
---|---|
コンテナ作成者 | Client |
コンテナ利用者 | Invoker |
Dockerfile | Receiver |
docker run コマンド |
Command |
作成コンテナのdocker run 実装 |
ConcreteCommand |
- コンテナ作成者はDockerfileを利用してDockerコンテナを作成
- コンテナ利用者はどんなDockerfileなのかは意識せず、コンテナに対してコマンド
docker run
だけで対象コンテナを作成出来る - 実際の
docker run
はDockerfileの内容を元に処理が実行される。 - コンテナを差し替えれば当然
docker run
の結果も変わる。
と、Commandパターンチックな設計になっているのかなと感じました。
サンプル
Cで書くのはしっくりこないけど作りました。
何故か舞台はコンピュータにより管理された近未来の地下都市「アルファ・コンプレックス」。
インフラレッド・レッド・ブルー(クリアランス低い順)の色をした市民が何やら会話をするようです。
まずReceiverにあたるクラスは回答者Answer
としました。
自身の色・名前・いくつかの質問への回答を行います。
メソッド入りのインスタンスを渡さないと意味がないのでインターフェイスチックな定義にしています。
enum question_e {
DO_YOU_HAVE_CAR,
DO_YOU_KNOW_MUTANT,
ARE_YOU_HAPPY,
HIGH_CLEARANCE
};
typedef enum color_e {
INFRA_RED,
RED,
BLUE,
} color_e;
struct answer_t {
color_e (*color)();
char *(*getname)();
char *(*answer)(int id);
};
typedef struct answer_t answer_t, *Answer;
Answer answer_infra_red_new();
Answer answer_red_new();
Answer answer_blue_new();
Command
にあたるクラスはInterviewer
。ConcreteCommand
はレッドとブルーの市民。
Answer
対象の人に対して質問します。
Answer
の内容、クリアランスによってインタビューの内容が変わります。
#include "answer.h"
struct interviewer_t {
void (*interview)(void);
};
typedef struct interviewer_t interviewer_t, *Interviewer;
Interviewer interviewer_red_new(Answer answer);
Interviewer interviewer_blue_new(Answer answer);
最後にmain。コンピューター様がclient。コマンド実行時にget_interviewer_by_computer
を介してInterviewer
を生成します。
Invoker
がmain関数。コンピューター様にお尋ねしたらinterviewを見るだけ。
#include "interviewer.h"
#include <stdio.h>
#include <stdlib.h>
#define LENGTH(array) (sizeof(array)/sizeof(array[0]))
static Interviewer interviewer_infra_red_new() {
printf("インフラレッドに質問機会など与えられません。\n");
exit(1);
}
static Interviewer get_interviewer_by_computer(char *argv[]) {
Answer (*answer_new[])() = {answer_infra_red_new, answer_red_new, answer_blue_new};
int answer_color=atoi(argv[1]);
if(LENGTH(answer_new)<answer_color) answer_color = LENGTH(answer_new) - 1;
Interviewer (*interviewer_new[])(Answer) = {interviewer_infra_red_new, interviewer_red_new, interviewer_blue_new};
int interviewer_color=atoi(argv[2]);
if(LENGTH(interviewer_new)<interviewer_color) interviewer_color= LENGTH(interviewer_new) - 1;
/*create answer class from input type*/
Answer answer_instance = answer_new[answer_color]();
/*create interviewer command class with answer*/
return interviewer_new[interviewer_color](answer_instance);
}
int main(int argc, char *argv[]) {
if(argc<3) return -1;
/*create interviewer command class with answer*/
Interviewer interviewer_instance = get_interviewer_by_computer(argv);
interviewer_instance->interview();
return 0;
}
レッド同士の会話。インフラレッドへの扱いがひどい。
$ ./test 1 1
answer_red_new
interviewer_red_new
レッドへのインタビュー
[interview_red]市民スミス, あなたは車を持っていますか?
いいえ、持っていません。そこのインフラレッドのそばにありますね。
[interview_red]ではそれでブリ―フィングルームへ向かいましょう。
そんなレッドもInterviewerがブルー様に代わると卑屈に変わります。
$ ./test 1 2
answer_red_new
interviewer_blue_new
レッドへのインタビュー
[interview_blue]市民、幸福は義務です。あなたは幸福ですか?
はい、幸福は義務です。
[interview_blue]よろしい。さて、ミュータントは見つかりましたか?
あそこのインフラレッドがミュータントです!ZAPしましょう!
[interview_blue]そうですか、では後でZAPしておきましょう。ところで市民スミス、私 の500クレジットを見かけませんでしたか?
100クレジットしか落ちていませんでした~(以降無言で靴をなめ続ける
[interview_blue]…まあいいでしょう。さっさとミュータントを探してください。
…しょうもないものを作ってしまった。
感想
「利用者には出来るだけ簡単に操作してもらう」、コード作成に関係なく使える考えですね。完全=でなくても同じ考え方を導入したシステムは沢山ありそうです。
後はしょうもないコードでも作ってみると見えるものがあるもんですね。見える化大事。