オブジェクト指向言語にはデザインパターンと言うものがありまして、そいつはオブジェクト指向言語の設計で適用箇所が多いものを定式化したものになります。すでに使われているから問題なく動作することは保証されてて、さらに色んな所で使える上に再利用もできるっていう設計パターンのことをデザインパターンと呼ぶわけです。成功した設計事例の再利用とも言えます。囲碁とか将棋の定石みたいなもんですな。
僕のよく使っているC++言語も、一応オブジェクト指向言語ということになってるので、デザインパターンが使えるようになっています。こいつを一通りマスター出来たら設計の幅が広がって、作れるソフトの幅も広がるだろうなーと思いまして、今回はデザインパターンの一つ、Compositeパターンというものを紹介します。
Compositeパターンの概要を説明してから、構成(クラス図)の説明、ソースコードと言う形で進めていきます。
後ろの方にC++で実装したコードを載せています。コメントでどんな動作をしているのかを説明しているので、ソースコードと動作を見ながら勉強したいという方はそちらからどうぞ。
ちなみに、デザインパターンに興味のある方はこの本なんかをどうぞ。僕の参考になった(している)本です。
各デザインパターンが生まれた背景、そのデザインパターンが実現できる機能、さらにはそのデザインパターンが持つ問題点、ちょっとではあるけどデザインパターンの適用事例なんかが載ってます。C++を想定して書かれてるってところもグッドポイント。
Compositeパターンは親クラスの関数を呼び出すだけで子クラスも変更するときのパターン
まずはCompositeパターンって何なのよ?ってところから説明していきます。
Compositeパターンは、複数の構成要素から成るより大きな構成要素を簡単に扱いたいっていう要求から生み出されたパターンになります。説明していきますね。
見かけ上は一つの要素なんだけど、実際には色んな要素に分解できるものってありますよね?実生活で目にするもので言えば、例えばPCとかタンスなんかが挙げられます。まぁ、ほとんどの物は複数の構成要素に分解できますが。
そういうものを使う場合、ユーザーは構成要素のことは考えたり制御したりしなくても問題なく使える場合がほとんどだと思います。例えばPCにaという文字を入力したい場合、単にAキーを押すだけで済みます。Aキーの下にスイッチが付けられてるから、そのスイッチも押して…みたいなことは考えなくても問題なくaは入力できます。
そういったものは、本来は大量の構成要素から出来てて複雑なものなんだけど、うまく組み合わせられてるおかげでユーザーは構成要素のことを気にしなくても扱えるようになっています。それをクラスで上手く表現したものが今回紹介するCompositeパターンになります。
例えば、プレゼン用の資料で複数の図形から成る図を動かすって機能を実装する場合のことを考えてみます。マウスのドラッグ操作で図全体を動かすときに、ユーザーとしては、その図全体を一体として動かしたいわけです。そんなときに使えるのがCompositeパターンってわけですな。
また後で詳しく書きますが、より具体的なプログラムの話をしとくと、Compositeパターンは、図全体を動かすという関数を一回呼び出すだけで図の構成要素すべてが動かされるっていうプログラムを書きたいときに使うデザインパターンってことになります。
Compositeパターンの構成
前の節で、関数を一つ呼び出すだけで構成要素のすべてに対しても同じ変更を加えるっていう構造が必要な場合、Compositeパターンが有効だよーって話をしました。
次に、じゃあそのCompositeパターンはどうやって作ればいいの?って話をしていきます。
まずは、Compositeパターンがどのような構成になっているかから説明していきます。構成は下のクラス図のようになって、その説明が図の下に書いてあります。
クラスが4つあって、Client、Component、Composite、Leafという4つになります(場合によっては、Clientがクラスでない可能性もありますが)。ここで大事なのは、Clientを除いた3つのクラスです。ClientはComponentクラスに実装されたメンバ関数だけを呼び出すことになります。最終目的は、ClientがComponentクラスの関数を呼び出すことで、その子クラスに設定したすべてのクラスの同じ関数が呼び出されるようにするってことになります。
要するに、ClientはCompositeパターンのユーザー(呼び出し元)ってわけですな。
Client以外の3つを見ていきます。LeafもCompositeもComponentから派生しているのですが、違いが3つあります。集約の有無と関係の有無、それとメンバ関数です。まずは集約と関係について説明していきます。
CompositeクラスはComponentクラスを集約していて、必要があればComponentクラスにあるデータを参照したり、データを渡したりもします。集約するとは、Compositeクラスがメンバ変数としてComponent型のポインタを持つということです。
そのポインタには子にしたいオブジェクトのアドレスを格納していきます。Componentの派生クラスなので、CompositeにしてもLeafにしてもComponentクラス型のポインタに格納できるようになっております。
要するに、CompositeクラスにComponent型ポインタを作って、そこにLeafなりCompositeなり、子オブジェクトにしたいオブジェクトのアドレスを格納していくというわけですな。
で、method()をそのComponent型ポインタから呼び出して、子オブジェクトのmethods()をすべて実行するって動作を実現することになります。ポリモーフィズムってやつですな。この辺のことは、次の節のソースコードを見ていただければ分かるかと思います。
次に、メンバ関数の話です。add(Component)は親子関係を作りたいときに使う関数になります。上で書いた「Component型ポインタに子オブジェクトのアドレスを格納していく」って動作を実現する関数になります。子を持たない(持つ必要がない)Leafクラスには不要な関数なので、ComponentとCompositeにしか定義されていません。
remove(Component)は親子関係を解消したいときに使う関数になります(場合によっては子オブジェクトをdeleteします)。こいつも子オブジェクトのアドレスに関する操作になるんでLeafクラスには定義しません。
ソースコード
ソースコードについては、この記事を参考にさせていただきました。
本当は上の節で挙げたクラス図を忠実に再現した方が良いんでしょうが、面倒になっちゃって下図のようなクラス図に変えて実装しました(参考にしたサイトの物をパクったのでこうなったんです笑)。
基本的には上の節で説明したことをプログラムで表現しただけです。
////////////////////////////////
// compositeパターン
//
// オブジェクトに入れ子構造を作ることができるパターン
// 利点:親オブジェクトの関数を呼び出すだけで、その下のオブジェクトの同じ関数がすべて呼び出される。
//
// このプログラムは、
// 識別番号(ID)の違うオブジェクトを作って、それらのオブジェクト間に親子関係を定義し、
// 親の関数を呼び出すだけで子の関数がすべて呼び出されるという動作を確認するためのもの
//
// 親子関係は下のようにする ※(親オブジェクト)->(子オブジェクト)という形で表記する
// 1 -> 11
// 1 -> 12
// 12 -> 121
// 12 -> 122
//
// ディレクトリのように表せば、
// 1
// |---11
// '---12
// |---121
// '---122
// となる。
////////////////////////////////
#include <iostream>
#include <vector>
using namespace std;
// componentクラス
// ユーザーに公開する関数だけを宣言するクラス
// このクラスの関数を呼び出すと、
// 紐づけられているすべてのオブジェクトの
// 同じ関数が一斉に呼び出される
class Interface
{
public:
virtual void Output() = 0; // コンポーネントクラスでは純粋仮想関数を定義しておき、継承先で定義する。
};
// compositeクラス
// 箱のような役割を果たすクラス
// 実際には、より下位のポインタを紐づけていくことで処理をまとめている。
// このクラス内に新たなオブジェクトを追加する関数を定義している。
class Composite : public Interface
{
private:
int Composite_ID; // 分かりやすくするために、識別番号のようなものを用意しておく
vector<Interface*> Parts_List; // この可変長配列に、紐づける子オブジェクトのポインタを格納していく。この配列を利用して、再帰的な関数呼び出しを実現する
public:
Composite(int i) { Composite_ID = i; } // コンストラクタ(インスタンス化されるときに、IDを代入するようにしておく)
void Add(Interface*); // 上のParts_Listに新しくポインタを格納するための関数
void Output() override; // componentクラスの関数Output()をオーバーライド(再定義)
};
// ここから、Compositeクラスの関数の定義
// オブジェクトのポインタを格納する関数
// compositionを複数インスタンス化したら、
// そのすべてのオブジェクトがParts_Listを持つことになる
// Parts_Listはあるオブジェクトに対する子のポインタだけを格納する(子の子のポインタは格納しない)
void Composite::Add(Interface* New_Parts)
{
this -> Parts_List.emplace_back(New_Parts); // thisはなくても問題なく動作するが、後ろの方で色んなポインタが出てきて分かりにくくなるので明示しておく
}
// 出力関数 各オブジェクトのIDを表示する
void Composite::Output()
{
vector<Interface*>::iterator now, start, end; // nowはアドレスリストを順番に見ていくときに使い、今見ているアドレスを示す
start = Parts_List.begin(); // 子オブジェクトのアドレスリストの最初のアドレスをstartに代入
end = Parts_List.end(); // 子オブジェクトのアドレスリストの最後のアドレスをendに代入
// まずはCompositeクラス自身のIDを表示
cout << Composite_ID << endl;
// 次に、子オブジェクトのIDを順に表示
for(now = start ; now != end ; now++) // nowが子オブジェクトリストの最初のアドレスから最後のアドレスになるまでループ
{
(*now)->Output(); // 子オブジェクトのアドレスから出力関数を呼び出す。つまり、子オブジェクトの出力関数を呼び出している。
}
}
// 子オブジェクトを持たないleafクラス
// compositeクラス(箱)のように、子オブジェクトを持つことができない(持つ必要がない)クラス
// このクラスは、親クラス(ここではInterfaceクラス)の純粋仮想関数を再定義するだけでいい
class Leaf : public Interface
{
private:
int Leaf_ID;
public:
Leaf(int i) { Leaf_ID = i; } // コンストラクタcompositeクラス同様、インスタンス化と同時にIDを入力するようにしておく
void Output() override; // Interfaceクラスの出力関数をオーバーライド
};
// ここから、Leafクラスの関数の定義
// 出力関数
// compositeクラスとは違い、
// leafクラスは子オブジェクトを持つことがないと保証されているので、
// 自身のIDを表示する処理だけでいい
void Leaf::Output()
{
cout << this -> Leaf_ID << endl;
}
/* main関数 */
int main()
{
// Compositeクラスをインスタンス化
Composite com1(1);
Composite com12(12);
// Leafクラスをインスタンス化
Leaf leaf11(11);
Leaf leaf121(121);
Leaf leaf122(122);
// leafオブジェクトをcompositeに追加
com1.Add(&leaf11);
com12.Add(&leaf121);
com12.Add(&leaf122);
// compositeにcompositeを追加
com1.Add(&com12);
// 最上位のオブジェクトの出力関数だけを呼び出す
com1.Output();
return 0;
}
実行するとこうなります。
1
11
12
121
122
Composite::Output()関数の中で、子クラスの数だけループを回しながら、子クラスのアドレスからOutuput()関数を実行するという処理を書いています。この処理のおかげで、全体を一つのオブジェクトとして動かすって操作ができるようになってるんですな。
P.S. ゲーム制作にも使えそうな感じで、作れるプログラムの幅が一気に広がった感じがしてなかなか良いですなぁ。うん、これからはデザインパターンをもっと勉強していこう。きっともっといろんなものが作れるようになるぞ。とりあえず、ソフト作りがグンと楽になって、高性能にもできそうな予感。