前回に続き、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専門のチームを編成し、キャリアに応じた専任コンサルタントがご相談を承り ます。キャリアチェンジから市場価値の可能性、ご収入などの相談から面接のア ドバイスまでお気軽にご相談ください。求人情報誌や転職情報サイトなどで一般 に公開されていないような「急募求人案件」も随時ご紹介が可能です。まずはご 登録ください!