compositeパターン

compositeパターンは複数のものを単数扱いして複雑な構造への処理を容易に行なえるようにするものだ。この適用範囲は思いのほか広い。これには、『複数のものを単数扱い』というより『単数の出力を複数の出力』とみなすとした方が分かりやすいかもしれない。


compositeパターンでは、単数のものを複数として扱うよう構造な構造をつくり、そこから全体(複数のものの集合)を操作する。

例えば、何らかの整数を返すクラスがあったとしよう。

class IntGetter{
public:
    int get(){ return /*何か整数*/;}
};

上記の配列があったとして、その配列中のオブジェクトから値をとってくる処理はいかのようになるだろう。

//クライアントコード
std::vector<IntGetter*> array_IntGetter;
for(int i = 0;i<NUMINTPUTTER;i++){//生成
    array_IntGetter.push_back(new IntGetter);
}
//

std::vector<int> vRet;//結果を取得する配列
for(int i = 0;i<NUMINTPUTTER;i++){
    vRet.push_back(array_IntGetter[i]->get());//取ってくる
}

上記にデータと処理をまとめるオブジェクト指向の道理にのっとり、かつcompositeパターンを適用すると、以下のようになる。

class CompIntGetter{
public:
    CompIntGetter(){}

    void add(IntGetter * ig){//生成されたインスタンスを登録
        m_IntGetter.push_back(ig);
    }

    ~CompIntGetter(){
        for(int i = 0;i<m_IntGetter.size();i++){//破棄
            delete m_IntGetter[i];
        }
    }

    void get(std::vector<int>& vRet){//複数かえすので参照の引数で
        for(int i = 0;i<m_IntGetter.size();i++){
            vRet.push_back(m_IntGetter[i]->get());
        }   
    }    
private:
    std::vector<IntGetter*> m_IntGetter;
};

複数の値をとってくるので引数を参照のvectorにした。これで値をとってくる作業は

//クライアントコード
CompIntGetter compip;

//...
//IntGetterクラスの派生を
//CompIntGetter::add 等を使って中身を登録
//...

std::vector<int> vRet;
compip.get(vRet);

とすればよくなった。

さて、何らかの整数を返すクラスは実は"IntGetter"の1つだけではなく、何種類もあったとしよう。すると、それぞれの何らかの整数を返すクラスは共通のインターフェースを持つクラスの派生にすればいい。

class IIntGetter{//インターフェースクラス
public:
    virtual int get() = 0;//純粋仮想関数
    virtual ~IIntgetter(){}//インターフェースクラスは空のvirtual デストラクタを定義するのを忘れずに。
};

class IntGetterA:public IIntGetter{
public:
    int get(){return /*何か整数*/;}
}; 

class IntGetterB:public IIntGetter{
public:
    int get(){return /*何か整数*/;}
}; 

class CompIntGetter{
public:
    //省略
    void add(IIntGetter * ig){//引数の型をIntGetter→IIntGetterに変更
        //省略
    }
private:
    IIntGetter* m_IntGetter[NUMINTPUTTER];//IntGetter→IIntGetterに変更
};

一方でクライアントコードはIIntGetter派生クラスのインスタンスの配列をまとめることが出来るようになった。

//クライアントコード
CompIntGetter compip;

//...
//条件に応じてIntGetterAまたはIntGetterBなどのIIntGetter派生クラスのインスタンスを
//CompIntGetter::add 等を使って中身を登録
//...

std::vector<int> vRet;
compip.get(vRet);

次にIIntGetterが複数ある構造は配列でなく、『木構造』にするとしよう。
このときIIntGetter派生クラスのインスタンスは『葉』になる(直接整数を返す)。
一方、内部でIIntGetter派生クラスのインスタンスを複数もち、それらに整数を返すようお願いする木構造の節となるクラスのインスタンスは『枝』となる。

このとき構造をまとめる"CompIntGetter"のインスタンス木構造の『枝』になり、IIntGetter派生クラスのインスタンスは『枝』となる。
『枝』はさらに大きい木構造の子でしかなく、『葉』と同様に木構造の『節』(ノード)でしかなくなる。
つまり、複数(『枝』)も単数(『葉』)も同一視するのだ。
さてこの考え方を実装するとき、気づくであろう点は「インターフェースは複数の方に合わせる」ということだ。これまでのコードで言えば、IIntGetter派生クラスは返り値として整数を返すメンバ関数を持ちいて整数を得ることが出来た。

int IIntGetter::get();

一方CompIntGetterでは整数を得るために引数としてvectorの参照を渡していた。

void CompIntGetter::get(std::vector<int>& vRet);

『葉』と『枝』でインターフェースが違うのは不都合だ。
こういうときは複数(『枝』)にあわせてしまうのが好都合だ。
『葉』と『枝』を共通のクラスから派生し共通のインターフェースを持つようにしてしまおう。

    class IIntGetter{//インターフェースクラス 『葉』と『枝』どちらもこのクラスを派生する
    public:
        virtual void get(std::vector<int>& vRet) = 0;//純粋仮想関数 //複数の値を返すよう変更
        virtual ~IIntgetter(){}//インターフェースクラスは空のvirtual デストラクタを定義するのを忘れずに。
    };
    
    class CompIntGetter:public IIntGetter{//共通のインターフェースクラスから派生
        //省略
    };
    
    class IntGetterA:public IIntGetter{
    public:
        void get(std::vector<int>& vRet){ /*何か整数*/}//インターフェース変更 単数でも複数の値を返せるよう
    }; 

    class IntGetterB:public IIntGetter{
    public:
        void get(std::vector<int>& vRet){/*何か整数*/}//インターフェース変更 単数でも複数の値を返せるよう
    }; 

これで木構造ですら表せるようになったはずだ。注目すべきなのはインターフェースを『枝』の方に合わせたが、『葉』のほうはインターフェースを変えたことによってデメリットが生じているわけではないということだ。

クライアント側では非常に簡単に操作できる。

//クライアントコード
CompIntGetter ipTree;

//木構造等生成
//....

std::vector<int> vRet;

ipTree.get(vRet);//これだけ!

compositeパターンは設計で構造をしっかりモデル化できれば自然とコーディングまで持っていくことが出来るだろう。
逆に言えばしっかりモデル化ができていなければ、うまくコーディングすることはかなわないだろう。