前回に続き、Cocoa環境でのCommandパターン探索として、ターゲット・アクション・パラダイムの話を続けよう。
前回は、C++でターゲット・アクションと似た仕組みを実現する例として、GoF本で例として挙げられているSimpleCommandを紹介した。その際、「C++のテンプレートを使うものでは制約がある」という書き方をした。
記事の掲載後に、読者より「C++のテンプレートの説明に誤りがある」という指摘をいただいた。今回は、その誤りを訂正するとともに、C++のテンプレートとターゲット・アクションの違いについて、さらに突っ込んで議論をしてみよう。
前回の訂正:SimpleCommandは任意のクラスに適用可能
まずは、GoF本で紹介されている、SimpleCommandをもう一度紹介する。前回もソースコードを書いたが、確認の意味で再掲しよう。
基本となるのはCommandクラスである。Commandクラスで重要なのは、Executeという関数を定義していることだ。これを呼び出すことで、コマンドが発動する。Commandクラスには、Executeの実装はない。サブクラスで実装することになる。
List 1.
class Command {
public:
virtual ~Command() {};
virtual void Execute() = 0;
protected:
Command() {};
};
Commandのサブクラスとなるのが、SimpleCommandクラスだ。このクラスで、テンプレートを使う。
コンストラクタの引数として渡すのが、コマンドの受け手となるReceiverと、コマンドを実行するActionだ。Actionは、Receiverのメンバ関数として定義される。
Execute関数では、コンストラクタで指定されたReceiverのActionを呼び出すことになる。ここだけ見れば、正にCocoaのターゲット・アクションと同じ考え方だ。
List 2.
template <class Receiver>
class SimpleCommand : public Command {
public:
typedef void (Receiver::* Action)();
SimpleCommand(Receiver* r, Action a) :
_receiver(r), _action(a) {}
virtual void Execute();
private:
Action _action;
Receiver* _receiver;
};
template <class Receiver>
void SimpleCommand<Receiver>::Execute() {
(_receiver->*action)();
}
前回の記事では、「Receiverとなるクラスは、共通のクラスから継承しなくてはいけない、またすべてのActionはそのベースとなるクラスで定義しておかないといけない」という旨を書いた。だが、SimpleCommandを使うだけならば、この記述は誤りだった。実際は、どんなクラスのどんなメンバ関数もSimpleCommandに渡すことができる。
例を示そう。次のように、ReceiverAというクラスと、メンバ関数Actionを定義する。
List 3.
class ReceiverA {
public:
void Action();
};
void ReceiverA::Action() {
cout << "Invoked ReceiverA::Action()" << endl;
}
このクラスは、どのクラスも継承していない。Actionというメンバ関数にも、何ら制約はない。
このReceiverAクラスとともに、SimpleCommandを次のように使うことができる。
List 4.
ReceiverA* receiver;
receiver = new ReceiverA;
Command* command;
command = new SimpleCommand<ReceiverA>(receiver, &ReceiverA::Action);
command->Execute();
これで、CommandクラスのExceute関数を呼び出せば、ReceiverAのAction関数が呼び出されることになる。なるほど。確かに、Commandパターンの要件は満たしている。
SimpleCommandでReceiverだけを変更することはできるか?
だがこの連載の目的は、Objective-Cの、Cocoaの特性を追求することだ。Cocoaでの、ターゲット・アクション的な使い方になれているならば、次のように考えるだろう。「確かに、ReceiverAクラスのAction関数クラスを呼び出すことはできた。ならば、別のクラスのAction関数は呼び出せるだろうか?」
そこで、次のようなReceiverBクラスを考えてみよう。
List 5.
class ReceiverB {
public:
void Action();
};
void ReceiverB::Action() {
cout << "Invoked ReceiverB::Action()" << endl;
}
ReceiverAクラスと、ほぼ同じだ。Actionという同名のメンバ関数を持つ。単に、クラス名が違っているだけだ。
このクラスのActionを、Commandを使って呼び出したい。クラスは違うもののメンバ関数は同じなのだから、Receiverだけを変更すればいいのではないか、と期待する。だが、そうはいかない。結局、Actionも変える必要がある。
List 6.
// ReceiverBクラスのインスタンスを作る。
ReceiverB* receiver;
receiver = new ReceiverB;
Command* command;
// Receiverを変えただけではNG。コンパイルすら通らない。
command = new SimpleCommand<ReceiverA>(receiver, &ReceiverA::Action);
// Actionも変える必要がある。
command = new SimpleCommand<ReceiverB>(receiver, &ReceiverB::Action);
command->Execute();
List 4.では、ReceiverとしてReceiverAのインスタンス、ActionとしてReceiverA::Actionを指定していた。Actionには、ReceiverAのメンバ関数を指定しているところに注目してほしい。このActionでは、当然のことながら、ReceiverBに対して呼び出すことができない。ReceiverをReceiverBのインスタンスに変更したら、ActionもReceiverB::Actionに変更せざるを得ないのだ。
Execute関数の中でActionを呼び出す
ならば、次の手を考えるだろう。Execute関数の中で、直接Actionを呼び出すようにしてやればいい。そうすれば、Receiverのクラスがなんであろうとも、Action関数を呼び出すはずだ。つまり、C++的なduck typingを活用してやるのだ。
これを実現する、ActionCommandというクラスを作ってみよう。
List 7.
template <class Receiver>
class ActionCommand : public Command {
public:
ActionCommand(Receiver* r) :
_receiver(r) {}
virtual void Execute();
private:
Receiver* _receiver;
};
template <class Receiver>
void ActionCommand<Receiver>::Execute() {
_receiver->Action();
}
コンストラクタには、Receiverのインスタンスのみを渡す。そして、Execute関数では、Receiverが持っているAction関数を呼び出すようにするのだ。
これならば、次のように使える。
List 8.
ReceiverA* receiver;
receiver = new ReceiverA;
ReceiverB* receiver;
receiver = new ReceiverB;
Command* command;
command = new ActionCommand<ReceiverA>(receiver);
command->Execute();
command = new ActionCommand<ReceiverB>(receiver);
command->Execute();
ActionCommandのコンストラクタに渡す引数を、ReceiverAのインスタンスにするか、ReceiverBのインスタンスにするかだけで、それぞれのAction関数が呼び出されるのだ。
だがここで、初めの動機に立ち戻ってみよう。Cocoaのターゲット・アクションが目的とするものは、「任意のオブジェクト」の「任意のメソッド」を呼び出すというものだ。ActionCommandクラスの方法では、Action関数にしか対応することができない。他の関数を呼び出そうと思ったら、その度にサブクラスを作らなくてはいけない。
また、前回の最後に紹介したものだが、Cocoaでは「存在しないアクション」を呼び出すということが、ユーザインタフェースの制御で大きな役割を占める。C++のテンプレートによる方法では、SimpleCommandにしてもActionCommandにしても、存在しないActionを与える方法がない。そのようなものを作ろうとした時点で、コンパイルエラーが発生するだろう。
ターゲット・アクションに必要なもの
結局のところ、Cocoa流のターゲット・アクションが求めるものは、次のようなCommandクラスになる。
List 9.
Command* command;
command = new Command(receiver, "Action");
コンストラクタには、ReceiverとActionを渡す。Receiverは任意のクラスのインスタンスで、Actionはメソッド(またはメンバ関数)を表す「何か」だ。上のソースでは文字列で指定しているが、それには限らない。ただし、メンバ関数のポインタは駄目だ。それは特定のクラスに隷属することになるからだ。クラスの定義からは独立した形でメソッドを指定できる、「何か」が欲しいのだ。Objective-Cでは、それはセレクタになる。
煎じ詰めて言えば、動的な言語と静的な言語の差異は、ここに表れるのだろうと考えている。もし、読者の方でC++に精通しており、この要件をクリアする書き方を提案できるのであれば、是非とも教えてほしい。
次回も、Cocoaのターゲット・アクションの話を続けよう。
提供:毎日キャリアバン ク
毎日キャリアバンクではITエンジニア出身のキャリアコンサルタントで形成する IT専門のチームを編成し、キャリアに応じた専任コンサルタントがご相談を承り ます。キャリアチェンジから市場価値の可能性、ご収入などの相談から面接のア ドバイスまでお気軽にご相談ください。求人情報誌や転職情報サイトなどで一般 に公開されていないような「急募求人案件」も随時ご紹介が可能です。まずはご 登録ください!