お茶漬けびより

"あなたに教わったことを、噛んでいるのですよ" 五等分の花嫁 7巻 「最後の試験が五月の場合」より

拡張式 Factory Method

f:id:pickles-ochazuke:20170715173147j:plain

ナッツ食べてます。そこそこおいしいです。

前回、FactoryMethod を作りましたが、今回は、それを改造します。

pickles-ochazuke.hatenablog.com

基本的な FactoryMethod だと、新しいクラスを作ったときに毎度 FactoryMethod 側も新しいクラス用の処理を追加しないといけません。そこで、新しいクラスへの対応を実行中に行えるようにします。

概要

大まかに書くと、抽象基本クラスを継承した新しいクラス側に、"自身のオブジェクトを作成し、それを返す" コールバック関数を作ります。そのコールバック関数と key となる文字列を FactoryMethod に渡し、FactoryMethod 側は、その関係を覚えておきます。実際にオブジェクトが必要になったときは、key となる文字列を渡すことで、それに対応したコールバック関数が FactoryMethod 内で呼ばれ、作成されたオブジェクトが返ってくる。という感じです。

図にすると以下のような感じでしょうか。

f:id:pickles-ochazuke:20170715194958p:plain

上記は、Callback の登録のみしか書いていませんが、削除もできたほうがいいので、実装時には削除用の関数も作ります。

FactoryClass

まずシーケンス図にあった FactoryClass のヘッダは以下のとおりです。 クラス名は、前回の続きなので SlimeFactory となっています。 また、SlimeFactory で作られるオブジェクトは、すべて抽象基本クラスの ISlime を継承しています。

#pragma once
#include <string>
#include <map>
#include <functional>
#include "ISlime.h"

class SlimeFactory
{
public:
    using DoMakeCallback = std::function<ISlime*()>;

    SlimeFactory()  = delete;
    ~SlimeFactory() = delete;
    static void RegisterSlime(const std::string& type, DoMakeCallback cb);
    static void UnregisterSlime(const std::string& type);
    static ISlime* DoMakeSlime(const std::string& type);

private:
    using CallbackMap = std::map<std::string, DoMakeCallback>;
    static CallbackMap DoMakes_;

};

上から順番に見ていきます。まず、using DoMakeCallback = std::function<ISlime*()> ですが、 これは、コールバック関数の型を定義しています。using DoMakeCallback は書くのを楽にしているだけで、std::function<ISlime*()> が大事です。これが、登録するコールバック関数の型になります。登録したいクラスは、コールバック関数を作るとき、この型(ISlime*())を守る必要があります。std::function は、関数ポインタ用のクラスだと思っておけば問題ないです。

コンストラクタとデストラクタは、今回必要ないので delete しています。実際に FactoryClass を作るときは、1つしか作られないようにシングルトンにするといいでしょう。

static void RegisterSlime(const std::string& type, DoMakeCallback cb) は、コールバック関数を登録するための関数です。登録したいコールバック関数があるとき、この関数を呼び出して、引数に key となる文字列とコールバック関数を渡してやります。

static void UnregisterSlime(const std::string& type) は、先ほどとは逆で、登録済みのコールバック関数を削除する関数です。使うときは、登録したときに渡した文字列と同じ文字列を引数に渡すだけです。

static ISlime* DoMakeSlime(const std::string& type) は、オブジェクトが必要になったときに呼びます。引数に登録時の文字列を渡してやることで、生成されたオブジェクトが返ってきます。

using CallbackMap = std::map<std::string, DoMakeCallback> は、これも型を定義しているだけです。std::map<std::string, DoMakeCallback> は、コールバック関数を登録するための型になります。std::stringkey となり、DoMakeCallback が登録されたコールバック関数です。

最後に、static CallbackMap DoMakes_ ですが、これは、コールバック関数が登録される場所になります。

次に、ソースファイルを見ていきます。

#include "HealSlime.h"
#include <iostream>

// インスタンス化
SlimeFactory::CallbackMap SlimeFactory::DoMakes_;

void SlimeFactory::RegisterSlime(const std::string& type, DoMakeCallback cb)
{
    DoMakes_[type] = cb;
}

void SlimeFactory::UnregisterSlime(const std::string& type)
{
    DoMakes_.erase(type);
}

ISlime* SlimeFactory::DoMakeSlime(const std::string& type)
{
    if (type == "Slime") {
        return new Slime();
    }
    else if (type == "SheSlime") {
        return new SheSlime();
    }
    else if (type == "HealSlime") {
        return new HealSlime();
    }

    CallbackMap::iterator it = DoMakes_.find(type);
    if (it != DoMakes_.end()) {
        if (!it->second) { std::cout << "呼び出せない" << std::endl; return nullptr; }
        return (it->second)();
    }

    return nullptr;
}

上から順番に見ていきましょう。 まずは、SlimeFactory::CallbackMap SlimeFactory::DoMakes_ で static の変数をインスタンス化してやります。

RegisterSlime(const std::string& type, DoMakeCallback cb) は、登録なので、登録用の変数 DoMakes_ にコールバック関数を登録しています。

UnregisterSlime(const std::string& type) は、削除なので、type を元に DoMakes_ に登録されているコールバック関数を削除しています。

DoMakeSlime(const std::string& type) は、type を元に作成すべきオブジェクトを決め、作成し、そのオブジェクトを返します。
Slime, SheSlime, HealSlime は、前回作成した内容です。そこを抜けると type と関連するコールバック関数があるか探します(CallbackMap::iterator it = DoMakes_.find(type);)。見つかれば、そのコールバック関数が呼べるか確認(if (it != DoMakes_.end()))し、問題なければ、呼び出して、返ってきたオブジェクトをそのまま返します(return (it->second)())。

以上が拡張式の FactoryMethod になります。

登録するためのコールバック関数の中身は、以下のように書きます。

static ISlime * DoMakeMyself()
{
    return new MySlime();
}

このメソッドを RegisterSlime の第二引数に渡してやれば、DoMakes_ に登録されます。

実行

実際に使うときは、以下のように使います。

 std::function<ISlime*()> func = MySlime::DoMakeMyself;
    SlimeFactory::RegisterSlime("MySlime", func);

    ISlime* slime = SlimeFactory::DoMakeSlime("MySlime");

結果は、以下です。

ぼくが考えた最強のスライム が現れた!

続行するには何かキーを押してください . . .

以上のように、新しいクラスを作ったときは、FactoryClass 側で何も追加する必要なく、対応することが出来ました。

おわり

実際に作ってみると、することは難しそうだけど、作ってみたら簡単だった。ように感じました。コールバック関数の登録も削除も一行で済み、呼び出すときもfindで探して、呼び出すだけですので。

今回の難しいところは、拡張式の FactoryMethod の作り方よりも、関数ポインタ、std::map、を理解しているかどうかだと思います。さらに言ってしまえば、std::map は今回採用しているだけなので、何か key を渡されたときに、それに関係する関数ポインタを返せるならどう作ってもいいわけです。なので、今回一番重要なのは、関数ポインタを理解しているかどうかなのだと思いました。

最後に、コードと参考資料を載せておきます。

github.com

C++のためのAPIデザイン

C++のためのAPIデザイン