目次
深いツリー状になっているモジュールの構成は、見通しが悪いので 良くない構成と言われます。よって、なるべくフラットにした 構成にすることが求められます。 『モジュール化による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 Mode { /* コンテキスト */ Customer customer; /* 顧客情報:コア・オブジェクト */ Stereo stereo; /* ステレオ:リンカブル・オブジェクト */ Phone phone; /* 電話:リンカブル・オブジェクト */ Speaker speaker; /* スピーカー(ステレオと電話で共有):コア・オブジェクト */ }; |
モード・オブジェクトは、初期化関数、後始末関数と、各モードに対応したモード関数と、 各種イベントに対応したイベントハンドラ(関数)を持ちます。
/* 初期化・後始末関数 */ void Mode_init( Mode* c ); void Mode_finish( Mode* c ); /* モード関数 */ void Mode_run( Mode* c ); /* イベントハンドラ */ void Mode_onMouseMoved( Mode* c ); void Mode_onPhoneCalled( Mode* c ); |
main 関数や、上位のモード関数は、次のようにモード・オブジェクトを使用します。
void main() { Mode mode; Mode_init( &mode ); Mode_run( &mode ); Mode_finish( &mode ); }; |
モード・オブジェクトの初期化関数では、所有している各オブジェクトの初期化関数
を呼び出すようにします。
ただし、リンカブル・オブジェクトには、
呼び出す順番に依存関係がある場合があるので注意してください。
void Mode_init( Mode* c ) { Customer_init( &c->customer ); Speaker_init( &c->speaker ); /* コモン・オブジェクトは先に初期化することが多い */ Stereo_init( &c->stereo, &c->speaker ); /* 必要なリンクを指定 */ Phone_init( &c->phone, &c->speaker ); /* 必要なリンクを指定 */ }; |
モード・オブジェクトの後始末関数も同様に行います。
void Mode_finish( Mode* this ) { Customer_finish( &c->customer ); Speaker_finish( &c->speaker ); Stereo_finish( &c->stereo ); Phone_finish( &c->phone ); } |
モードに所属するオブジェクトをそれぞれの開発者が担当する場合、
モード・オブジェクトは、特に協調作業が必要となります。
下手をすると協調作業が指数的増えてしまいます。
そうならないために、主な処理をモードの所属するオブジェクトの操作関数にさせ、
モード・オブジェクトはその協調関係のみ扱ってなるべく大きくならないようにします。
モード・オブジェクトは、モードの所属するそれぞれのオブジェクトの
単体テストにも必要になるので、(モードに直接所属するオブジェクトの)
開発者全員が共有することになります。
しかし、モード・オブジェクトがそのままでは、
すべての開発者のオブジェクトを集めなければなりません。
それを回避するためには、何も内容が無い、または定数を用いて最低限の出力を備えた
スタブ・オブジェクトを用意します。
スタブ・オブジェクトは、モードに所属するそれぞれのオブジェクトの開発者が
開発中のオブジェクトを元にインターフェイスを決め、
モード・オブジェクトの開発者が作成します。
そして、スタブ・オブジェクトも全員が共有するようにします。
アプリケーションのある特定のモードするときに呼び出し、
モードが終了したら関数から抜ける関数をモード関数と呼びます。
モード関数は、モード・オブジェクトの操作関数になります。
モード関数の最初や最後では、モードの開始、終了のイベントハンドラを呼び出し、 主に次のような内容を実行します。
モード内に
void Mode_run( Mode* this ) /* モード関数 */ { Mouse_Msg mouseMsg; /* メッセージの格納場所をローカルにとる */ Speaker_Msg speakerMsg; bool bIdle; Mode_onStart(); /* モード開始イベントハンドラ */ for (;;) { /* メッセージ・ループ */ /* 使用オブジェクトからメッセージを受け取る */ Mouse_getMsg( &this->mouse, &mouseMsg ); Speaker_getMsg( &this->speaker, &speakerMsg ); /* 論理的入力装置のイベントハンドラを起動してメッセージを変換する */ Menu_onMouse( this, &mouseMsg, &menuMsg ); /* メッセージをイベント・ハンドラに対応付けする */ bIdle = false; { switch ( menuMsg.type ) { case Menu_Click: Mode_onMenuClick( this ); break; case ... : default: bIdle = true; } } if ( bIdle ) { bIdle = false; switch ( speakerMsg.type ) { case Speaker_onTime: Mode_onSpeakerTime( this ); break; } } if ( bIdle ) { bIdle = false; switch ( mouse.type ) { case Mouse_Move: Mode_onMouseMove( this, mouse.x, mouse.y ); break; } } if ( bIdle ) Mode_onIdle(); } Mode_onEnd(); /* モード終了イベントハンドラ */ } void Mode_onStart() { Speaker_start(); /* スレッドの起動 */ ComPort_enable(); /* 割り込みを受け付ける */ } void Mode_onEnd() { ComPort_disable(); /* 割り込みの禁止 */ Speaker_end(); /* スレッドの終了 */ } void Mode_onMouseMove( Mode*, int x, int y ) /* イベントハンドラ */ { if ( c->soundEnable ) { /* 状態を判定 */ Speaker_setSwitch( SPEAKER_ON ); /* スピーカーのスイッチを入れる */ PCM_startSound( PCM_BEEP ); /* 音を鳴らし始める */ PCM_waitTillFinish(); /* 音が鳴り終わるまで待つ */ Speaker_setSwitch( SPEAKER_OFF ); /* スピーカーのスイッチを切る */ } } |
Windows のメニューやボタンなど、論理的な入力装置(物理的な入力装置を 使って間接的に操作する入力装置)があるときは、その入力装置オブジェクトの イベントハンドラを起動させてメッセージを変換し、イベントハンドラと対応付けます。
モード関数が大きくなりすぎないためにも、
イベントのタイプに対応したイベントハンドラに分割したり、
詳細なプロセスはモジュール・レイヤの
各種オブジェクトの操作関数(サービス関数)の内部に
入れるようにします。
モードと連動しないスレッドは、イベントハンドラ内で呼び出されます。
ポーリングする入力装置がないモードや、ポーリングをタイマーイベントで行う場合、
モード関数の内部にメッセージループを持ちません。
割り込み駆動のマルチスレッド・システムでは、
基本的にメッセージループを持ちません。
ただし、モードを形成している間は関数から抜けないようにします。
struct Mode { : int funcAState; /* 0=休止中、1=稼動中、2=中断処理中 */ }; void Mode_init( Mode* ); void Mode_run( Mode* ); /* モード関数中のメッセージループの直前のみ行いタイマーを起動する */ void Mode_onTimer( Mode* ); /* モード関数中のメッセージループの内部に相当、リターン */ void Mode_finish( Mode* ); void Mode_run( Mode* this ) /* 最も優先順位が高いスレッドで行う */ { Mode_start(); /* メッセージループの代わり */ sleep_task(); /* 自分のスレッド(メインスレッド)をスリープ状態にして全スレッドを起動 */ /* イベントハンドラ内でメインスレッドを起こしてここに戻ります */ Mode_end(); } void Mode_start() { Speaker_start(); /* スレッドの起動 */ Timer_set( &Timer_context, Mode_onTimer ); /* タイマーハンドラの登録とタイマーの起動 */ } void Mode_end() { Speaker_end(); /* スレッドの終了 */ } void Mode_onTimer( Mode* ) /* タイマーハンドラ:メッセージループの内部に相当 */ { Mouse_Msg mouseMsg; /* メッセージの格納場所をローカルにとる */ Speaker_Msg speakerMsg; bool bModeExit = false; /* 使用オブジェクトからメッセージを受け取る */ Mouse_getMsg( &this->mouse、&mouseMsg ); Speaker_getMsg( &this->speaker, &speakerMsg ); /* メッセージをイベントハンドラに送る */ switch ( mouseMsg.type ) { case MOUSE_MOVE: Mode_onMouseMove( this ); break; case ... : case FINISH_COMMAND: bModeExit = true; break; } if ( mouseMsg.type == IDLE && speakerMsg.type == IDLE ) sleep(); if ( bModeExit ) { /* モード終了操作 */ wakeup_task( &MainTask ); /* メインスレッドを起動する、本関数終了時に稼動 */ } } |
(作成中)
ユーザの操作や割り込みなどのイベントが起きたときに
実行する関数をイベントハンドラと呼びます。
イベントによってプログラミングすることを
イベントハンドラは次のきっかけで呼び出されます。
イベントハンドラは、アプリケーション・オブジェクトの 操作関数に位置し、イベントの種類と現在の状態に応じて シナリオを記述します。
void Mode_onMouseMove( Mode* c, Msg* msg ) /* マウスの割り込み関数 */ { if ( c->soundEnable ) { /* 状態を判定 */ Speaker_setSwitch( SPEAKER_ON ); /* スピーカーのスイッチを入れる */ PCM_startSound( msg->beepType ); /* 音を鳴らし始める */ PCM_waitTillFinish(); /* 音が鳴り終わるまで待つ */ Speaker_setSwitch( SPEAKER_OFF ); /* スピーカーのスイッチを切る */ } } |
イベントハンドラが大きくならないためにも、 詳細なプロセスはモジュール・レイヤの 各種オブジェクトの操作関数(サービス関数)の内部に 入れるようにします。 その際、コンテキストを操作関数の引数に渡さないようにしたほうが 良いでしょう。なぜなら、 オブジェクト(コア・オブジェクト、コンポジット・オブジェクト、 リンカブル・オブジェクト)がコンテキストの構造に依存してしまう 点と、リンクするオブジェクトがメンバ変数名に依存してしまうためです。 操作関数の所属するクラスのオブジェクトを引数に渡すようにします。
マルチスレッド環境では、全体から見て最もハードウェア入力に近いオブジェクトを モード・オブジェクトよりも優先順位を高くします。 このオブジェクトを割り込み発生オブジェクトと呼びます。 割り込み発生オブジェクトやモード・オブジェクト以外の、 その他の一般的な処理を行うオブジェクトをワーク・オブジェクトと呼び、 モードオブジェクトよりも優先順位を低く設定します。
たとえば、シリアルマウスを使うモードでは、 シリアル・コントローラを割り込み発生オブジェクトとし、 マウス・オブジェクトは、ワーク・オブジェクトの中で最も優先順位が高く 実行されるシリアル・イベントハンドラを持ちます。 マウス・オブジェクトのシリアル・イベントハンドラが呼び出されるまでのステップは 次のとおりです。
一般に、マルチスレッドを使うのは、反応よくするためという、
優先順位の高いオブジェクトの都合によるものなので、
優先順位の高いオブジェクトがタスクやキューの管理を行い、
優先順位の低いオブジェクトがマルチスレッドに依存しないで済むようにします。
そのためには、非同期コールの仕組みを優先順位の高いオブジェクトの操作関数にします。
非同期コールは、通常の関数呼び出しに似ていますが、
優先順位の低い別のスレッドが実行するために呼び出してすぐ処理を行わないことと、
返り値をすぐに受け取れないところが異なります。
返り値は、モード・オブジェクトのイベントハンドラにより取得できます。
非同期コールの例
int Draw_func( Draw* this, float n, int opt );
void Mode_onAny( Mode* this ) /* イベントハンドラ */ { Mode_call_Draw_func( this, &this->draw, 1.2, MODE_OPT ); } void Mode_call_Draw_func( Mode* this, Draw* self, float n, int opt ) /* 非同期コール */ { FuncCall* call = &this->funcCaller; FuncArgs* args; Semaph_start( &call->sema ); args = RingBuf_push( &call->buf ); Semaph_end( &call->sema ); args->self = self; args->n = n; args->opt = opt; wakeup_task( &call->task, call->priority, Mode_calling_Draw_func, this ); } void Mode_calling_Draw_func( Mode* this ) /* 非同期コール・別スレッドの実行部分 */ { FuncMsgs* call = &this->funcCaller; FuncArgs* args = RingBuf_peek( &call->buf ); Resource_startInterrupt( args->self->resource ); /* 割り込み型・クリティカル・セクション */ call->ret = Draw_func( args->self, args->n, args->opt ); Resource_endInterrupt( args->self->resource ); Semaph_start( &call->sema ); RingBuf_next( &call->buf ); Semaph_end( &call->sema ); priority_task( this->priority - 1 ); /* 自スレッドを this より1つ優先に */ wakeup_task( &this->task, this->priority, Mode_onCalled_Draw_func, this ); } void Mode_onCalled_Draw_func( Mode* this ) /* 非同期コール・リターンイベント */ { int ret = this->funcCaller.ret; : } |
非同期コールは、処理を中断することができます。 処理関数のループの中など、適当なところでメンバ変数をチェックします。
モードを終了するときは、非同期コールによるスレッドに中断メッセージを送り、 すべてのスレッドを終了させます。
すばやい反応をするために行うスタック的な割り込みによる非同期コールと異なり、
長時間に同時に複数の処理を交互に実行するときは、排他制御の必要なリソースに対する