目次
深いツリー状になっているモジュールの構成は、見通しが悪いので 良くない構成と言われます。よって、なるべくフラットにした 構成にすることが求められます。 『モジュール化による2層アーキテクチャ』は、 オブジェクト指向の目的の1つである、 モジュールを組み合わせて作成するプログラミングを明確にし、 フラットな構成にするためのフレームワークを提供します。
いくらフラットにすると言っても、どうしても水平に
並べることができないものがあります。
それは、コントローラとモジュールです。
『モジュール化による2層アーキテクチャ』は、
コントローラを上位層に、モジュール(の集まり)を下位層にします。
前者を、コントロール・レイヤ、後者をモジュール・レイヤと
呼ぶことにします。
コントロール・レイヤは1つのクラスから成っており、
から構成されています。詳細は各章を参照してください。
ツリー構造の最も末端にあるものが、コア・オブジェクトです。
コア・オブジェクトは、
数値や文字列のデータメンバから構成され、
現実世界の何かを表現してシミュレートに使われたり、
接続されている周辺機器を表現します。
コア・オブジェクトは、他のクラスを所有しません。
データ的なコア・オブジェクトの例
class Customer { /* 顧客 */ String name; /* 顧客名 */ Date firstMeetDay; /* 初回の交渉日 */ int type; /* 顧客タイプ */ }; |
ハードウェア・ラッパー的なコア・オブジェクトの例
class CDPlayer { /* CD プレイヤー */ int nSong; /* CD の曲数 */ int iSong; /* 現在の演奏曲番号 */ }; |
1つまたは複数の数値や文字列だけでなく、
1つまたは複数のオブジェクトを所有するものが
コンポジット・オブジェクトです。
所有されるオブジェクトは、コア・オブジェクトか
コンポジット・オブジェクトか
外部のソフトウェア部品のどれかで、
他のオブジェクトに共有されないで独占しています。
そのため、所有されるオブジェクトのライフサイクルは
すべて、所有するコンポジット・オブジェクトと同じになります。
class StereoCompo { String name; /* 製品名 */ CDPlayer cdPlayer; /* CD プレイヤー・コア・オブジェクト */ MDPlayer mdPlayer; /* MD プレイヤー・コア・オブジェクト */ Speaker speaker; /* スピーカー・コア・オブジェクト */ }; |
ラッパー的なコンポジット・オブジェクトの例
class Printer { String name; /* プリンタ名 */ HPrt handle; /* プリンタのハンドル、OS から提供 */ }; |
A と B が共に所有(または関連)するオブジェクト C がある場合、
A か B のどちらかが所有するか、A と B とは別に C が独立するか
のどちらかを選択しなければなりません。少なくとも片方は
コンポジット・オブジェクト(所有者)にはなれません。
このような、外部に必要なオブジェクトとリンクしなければならない
オブジェクトを、リンカブル・オブジェクトと呼びます。
オブジェクト C をコモン・オブジェクトと呼びます。
リンカブル・オブジェクトは、1つのオブジェクトでは
不完全形になっており何もできません。
必要なオブジェクトとリンクして初めて有効になります。
一般にコモン・オブジェクト C は、共有するオブジェクト A, B のどちらかが存在
しているときに必要になるので、A または B に独占すると
都合が悪いケースがおきることがあります。
そこで、C は、独立させたほうがいいでしょう。
class Stereo { /* ステレオ:リンカブル・オブジェクト */ String name; /* 製品名 */ CDPlayer cdPlayer; /* CD プレイヤー・コア・オブジェクト */ MDPlayer mdPlayer; /* MD プレイヤー・コア・オブジェクト */ Speaker* speaker; /* スピーカーへ リンク */ }; class Phone { /* 電話:リンカブル・オブジェクト */ String name; /* 製品名 */ Dialer dial; /* ダイアル・コア・オブジェクト */ Speaker* speaker; /* スピーカーへ リンク */ }; class Speaker { /* コモン・オブジェクト:コア・オブジェクト */ String name; /* 製品名 */ int volume; /* ボリューム */ int bass; /* 低音設定 */ }; |
独立したコモン・オブジェクト C は、関連するオブジェクト A とライフサイクルが異なります。 あるイベントに対して A の内部だけで済んでいたサービスのうち、 ライフサイクルに関わる部分は A と C の両方に対して 行う必要が出てくるので注意が必要になります。
コモン・オブジェクトは、コンポジット・オブジェクトが所有していた オブジェクトだけでなく、コア・オブジェクトの 一部のメンバ変数が集まってできることもあります。
ツリー構造に対応するものは、コンポジット・オブジェクトによる 垂直的なツリーだけでなく、リンカブル・オブジェクトによる水平的な ツリーも含めて実装されることになるので注意してください。
リンクするオブジェクトが、ある特定の操作関数だけに必要な場合、 クラスの内部でポインタを持つ必要はありません。 関数の引数に渡すようにします。
コントロール・レイヤは、1つのコンテキスト変数(構造体)を持ちます。
そのコンテキスト変数は、アプリケーションが持っている
すべてのオブジェクト(コア・オブジェクト、
コンポジット・オブジェクト、リンカブル・オブジェクト)を所有します。
すべてを所有するということなので、コンテキスト変数は、
アプリケーション・オブジェクトに相当する
コンポジット・オブジェクトです。
struct App { /* コンテキスト */ Customer customer; /* 顧客情報:コア・オブジェクト */ Stereo stereo; /* ステレオ:リンカブル・オブジェクト */ Phone phone; /* 電話:リンカブル・オブジェクト */ Speaker speaker; /* スピーカー(ステレオと電話で共有):コア・オブジェクト */ }; |
コンテキストに含まれるオブジェクトのほとんどは、 ユーザが理解できるものにします。そして、それらのオブジェクトに 対するシナリオをイベント関数(3-2章)に記述します。
コンテキストを初期化する場合、所有している各オブジェクトの初期化関数 (生成、ライフサイクル開始)を呼び出すようにします。 ただし、リンカブル・オブジェクトには、 呼び出す順番に依存関係がある場合があるので注意してください。
void App_init( App* c ) { Customer_init( &c->customer ); Speaker_init( &c->speaker ); /* コモン・オブジェクトは先に初期化することが多い */ Stereo_init( &c->stereo, &c->speaker ); /* 必要なリンクを指定 */ Phone_init( &c->phone, &c->speaker ); /* 必要なリンクを指定 */ }; |
ユーザの操作や割り込みなどのイベントが起きたときに
実行する関数をイベント関数と呼びます。
イベントによってプログラミングすることを
イベント関数は、アプリケーション・オブジェクトの 操作関数に位置し、イベントの種類と現在の状態に応じて シナリオを記述します。
void App_onMouseMove( App* c, Msg* msg ) /* マウスの割り込み関数 */ { if ( c->soundEnable ) { /* 状態を判定 */ Speaker_setSwitch( SPEAKER_ON ); /* スピーカーのスイッチを入れる */ PCM_startSound( msg->beepType ); /* 音を鳴らし始める */ PCM_waitTillFinish(); /* 音が鳴り終わるまで待つ */ Speaker_setSwitch( SPEAKER_OFF ); /* スピーカーのスイッチを切る */ } } |
イベント関数が大きくならないためにも、 詳細なプロセスはモジュール・レイヤの 各種オブジェクトの操作関数(サービス関数)の内部に 入れるようにします。 その際、コンテキストを操作関数の引数に渡さないようにしたほうが 良いでしょう。なぜなら、 オブジェクト(コア・オブジェクト、コンポジット・オブジェクト、 リンカブル・オブジェクト)がコンテキストの構造に依存してしまう 点と、リンクするオブジェクトがメンバ変数名に依存してしまうためです。 操作関数の所属するクラスのオブジェクトを引数に渡すようにします。
ポーリング(入力があるまで何もしないループを繰り返す)したり、 イベントがあるまでスリープ(ウェイト)する場合、 関数は、イベントが発生して呼び出されるものではなくなり、 ある特定のモードを形成する関数になります。
このモード関数は、アプリケーション・オブジェクトの 操作関数に位置し、内部にメッセージ・ループを持ちます。 メッセージ・ループの内部では、メッセージのタイプを switch 文などで判定しなければなりません。
void App_firstMode( App* c ) /* あるモード */ { Msg msg; for (;;) { /* メッセージ・ループ */ msg = App_getMsg(c); switch ( msg.type ) { case MOUSE_MOVE: if ( c->soundEnable ) { /* 状態を判定 */ Speaker_setSwitch( SPEAKER_ON ); /* スピーカーのスイッチを入れる */ PCM_startSound( PCM_BEEP ); /* 音を鳴らし始める */ PCM_waitTillFinish(); /* 音が鳴り終わるまで待つ */ Speaker_setSwitch( SPEAKER_OFF ); /* スピーカーのスイッチを切る */ } break; case ... } } } |
モード関数が大きくなりすぎないためにも、 イベントのタイプに対応したイベント関数に分割したり、 詳細なプロセスはモジュール・レイヤの 各種オブジェクトの操作関数(サービス関数)の内部に 入れるようにします。