ホーム < ゲームつくろー! < デザインパターン習得編
Mediator
〜オブジェクト全員の振る舞いを決定する仲介人
@ 多対多の複雑さ
RPGのメニューで、アイテムを選ぶと右上にアイテムの絵が出て、下にその説明書きが出る仕様を考えたとします。
メニュー、絵、ヘルプなどが同時並行に変化する、ちょっと複雑な仕様です。メニュー内で別のアイテムを選択すると、絵とヘルプが変わったり、ここでは使えないときに「使う」が薄くなって選択できなくするという親切設計にするのもありです。
このメニュー画面の各部品間のつながりを模式図で示してみます。
個々のオブジェクトがお互いの変化を知るということは、お互いを参照する必要があるということです。上の図では、矢印の根元のオブジェクトが矢印の先のオブジェクトへの参照を持つことになります。例えばLsitオブジェクトが2つありますが、上と下のリストでは知っているオブジェクトが違うので、派生させてそれを区別する必要があります。また、変更を加え矢印の矛先が変わってしまう場合、クラスを書き換えるかまた派生させて置き換える必要があります。これは、わずらわしいなんてもんじゃありません。
Mediatorパターンは、このような「多対多」の複雑な関係にあるオブジェクトの振る舞いを1つのMediatorクラスで制御し、たくさんの部品のサブクラスを作成する煩わしさを軽減させると共に、中央管理型にすることで、制御の理解度を高め、部品オブジェクトの結合度をぐっと低める働きを成します。
Mediatorパターンを導入すると、上の関係図は次のようになってしまいます。
各部品はMedatorのみを知っています(矢印の矛先)。Mediatorはこれら部品のつながりをすべて管理し、正しい振る舞いになるよう仕込みます。これにより、各部品同士の結合度は非常に低くなります。Mediatorはそれぞれの部品を派生クラスレベルで細かく知っています。よって、部品を隅々まで使いこなすことが可能になります。ただし、専門性が高くなるのでMediatorは使い捨てになります。
A Mediatorパターンの実装
では、上の例を実装してみましょう。パーツクラスのポイントは2つです。
・ 各パーツがMediatorを知っているので、各パーツ共通のメンバ変数m_Mediatorを設ける
・ Mediatorに対して「変化したよ〜」っと知らせる。
class Parts
{
private:
Mediator* m_Mediator;
public:
Parts(Mediator*); // コンストラクタでMediatorを登録
virtual void Changed() // 変化をMediatorに通知
{
m_Mediator->PartsChanged(this); // 自分自身を渡す
}
}
Mediatorクラスは変化したパーツを受け取るPartsChanged関数を持ち、この関数の中でパーツの振る舞いをすべて一元管理します。
class Mediator
{
protected:
Mediator(); // コンストラクタをprotected宣言(生成の禁止)
virtual void CreateParts() = 0; // パーツを生成
public:
virtual ~Mediator();
virtual void Update(); // 画面更新
// 変化したパーツに対する振る舞いを定義
virtual void PartsChanged(Parts*) = 0;
}
このクラスにはメンバ変数が1つもありません。これは派生クラスでパーツの細かな派生オブジェクトを保持することを想定しているためです。CreateParts関数はパーツを生成しますが、そのパーツは派生クラスで定義されたメンバ変数に格納されることになります。
例としてMenuListクラスとPictureViewerクラスを作成します。
class MenuList : public Parts
{
protected:
vector<string> m_StrList;
int m_Cursor; // 選択されている要素
public:
MenuList(Mediator* med): Parts(med) {};
void Next(){ // 次のリストを指す
// ... リスト移動
// 変化をMediatorに知らせる
Changed();
}
void Prev(){ // 前のリストを指す
// ... リスト移動
// 変化をMediatorに知らせる
Changed();
}
// 文字列を取得
string Get(){ return m_StrList[m_Cursor];}
// 描画
Draw();
}
class PictureViewer : public Parts
{
protected:
vector<Pictur*> m_PictList;
int m_CurPict; // 現在の絵の要素
public:
PictureViewer(Mediator* med): Parts(med) {};
// 絵を設定
void SetPicture(string pictname){
// ...指定の絵をセットする
// 変化をMediatorに知らせる
Changed();
}
// 絵を表示
Draw();
}
MenuListは文字列のリストを持ち、外部からの指示があるとカーソルを変化させます。変化が起こるたびに登録されているMediatorに「変化したよ〜」っと知らせているのがポイントです。PictureViewerクラスも同様です。
Mediatorクラスの派生クラスMenuMediatorクラスにおいて、これらの振る舞いを管理します。PartsChanged関数は次のようになります。
void Mediator::PartsChanged(Parts* parts)
{
// ...引数のオブジェクトを判断(検索)
// ここはif〜elseでポインタからオブジェクトを識別します
if( m_MenuList == parts ){
// リストから現在指されているアイテムを
// 取得してPictureViewerに伝える
m_PictViewer->SetPict( m_MenuList->Get() );
// ヘルプを表示する
m_HelpList->SetStr( m_MenuList->Get() );
}
else if( m_PictViewer == parts ){
// 実は絵が変化しても何もしません(^^;
}
// 描画
Update();
}
この関数の中は、正直非常に複雑なことになります。各パーツの振る舞いを別の関数に移せば見苦しさは多少軽減します。これは、あまり複雑な振る舞いをMediatorに任せると、その保守管理が大変になるというMediatorパターンの欠点を露呈しています。
Mediatorパターンは、結局のところ関連がそれほど複雑ではなくて、パッケージとしてうまくまとめられているパーツを汚したくない時に使用するのが最良だと思われます。少なくとも、多対多を一対多にすることによる分かりやすさの利点は捨てがたいものです。