モジュール化による2層アーキテクチャ

目次


1.モジュール化による2層アーキテクチャ

深いツリー状になっているモジュールの構成は、見通しが悪いので 良くない構成と言われます。よって、なるべくフラットにした 構成にすることが求められます。 『モジュール化による2層アーキテクチャ』は、 オブジェクト指向の目的の1つである、 モジュールを組み合わせて作成するプログラミングを明確にし、 フラットな構成にするためのフレームワークを提供します。

いくらフラットにすると言っても、どうしても水平に 並べることができないものがあります。 それは、コントローラとモジュールです。 『モジュール化による2層アーキテクチャ』は、 コントローラを上位層に、モジュール(の集まり)を下位層にします。 前者を、コントロール・レイヤ、後者をモジュール・レイヤと 呼ぶことにします。

コントロール・レイヤは、 『モジュール化による2層アーキテクチャ』の上位に位置し、 アプリケーションに必ず1つ存在しています (そのため、アプリケーション・オブジェクトと呼ばれます)。 コントロール・レイヤのメンバ関数は、 ユーザからのイベントごとに作成して、 各種シナリオを記述します。シナリオは、 関数(呼び出し)ツリーにおいて最も上位に位置するもので、 ユーザはその内容を知っています。逆に、知っているから ユーザはそのイベントを発生させるのです。

コントロール・レイヤは1つのクラスから成っており、

から構成されています。詳細は各章を参照してください。

モジュール・レイヤは、 『モジュール化による2層アーキテクチャ』の下位に位置し、 モジュール・レイヤの各モジュールは、オブジェクト指向で設計された インスタンスに相当します。つまり、同じクラスインスタンスが 複数あることがあるということです。 一般に言われるツリー構造は、すべてモジュール・レイヤに収めます。 ただし、ライフサイクルの管理生成/破壊、または有効/無効の操作)は、 ツリーの上位からツリーの下位へ階層的に行われるのではなく、 主にコントロール・レイヤが直接管理することになります。 たとえば、複数のモジュールで共有するモジュールは、 コントロール・レイヤの直下に配置し、共有モジュールライフサイクルの管理は、コントロール・レイヤが行います。

モジュール・レイヤの各モジュールは、その種類によって

に分けられます。詳細は各章を参照してください。


2-1.コア・オブジェクト

ツリー構造の最も末端にあるものが、コア・オブジェクトです。 コア・オブジェクトは、 数値や文字列のデータメンバから構成され、 現実世界の何かを表現してシミュレートに使われたり、 接続されている周辺機器を表現します。 コア・オブジェクトは、他のクラスを所有しません

データ的なコア・オブジェクトの例
class  Customer {       /* 顧客 */
  String  name;           /* 顧客名 */
  Date    firstMeetDay;   /* 初回の交渉日 */
  int     type;           /* 顧客タイプ */
};

ハードウェア・ラッパー的なコア・オブジェクトの例
class  CDPlayer {  /* CD プレイヤー */
  int     nSong;     /* CD の曲数 */
  int     iSong;     /* 現在の演奏曲番号 */
};


2-2.コンポジット・オブジェクト

1つまたは複数の数値や文字列だけでなく、 1つまたは複数のオブジェクトを所有するものが コンポジット・オブジェクトです。

所有されるオブジェクトは、コア・オブジェクトコンポジット・オブジェクトか 外部のソフトウェア部品のどれかで、 他のオブジェクトに共有されないで独占しています。 そのため、所有されるオブジェクトライフサイクルは すべて、所有するコンポジット・オブジェクトと同じになります。

コンポジット・オブジェクトの例
class  StereoCompo {
  String    name;         /* 製品名 */
  CDPlayer  cdPlayer;     /* CD プレイヤー・コア・オブジェクト */
  MDPlayer  mdPlayer;     /* MD プレイヤー・コア・オブジェクト */
  Speaker   speaker;      /* スピーカー・コア・オブジェクト */
};

ラッパー的なコンポジット・オブジェクトの例
class  Printer {
  String  name;      /* プリンタ名 */
  HPrt    handle;    /* プリンタのハンドル、OS から提供 */
};


2-3.リンカブル・オブジェクトコモン・オブジェクト

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;         /* 低音設定 */
};
上記は、2-2章の StereoCompo のスピーカーを独立させて Stereo に 変更することで、Speaker を Phone と共有することができた例です。

独立したコモン・オブジェクト C は、関連するオブジェクト A とライフサイクルが異なります。 あるイベントに対して A の内部だけで済んでいたサービスのうち、 ライフサイクルに関わる部分は A と C の両方に対して 行う必要が出てくるので注意が必要になります。

コモン・オブジェクトは、コンポジット・オブジェクトが所有していた オブジェクトだけでなく、コア・オブジェクトの 一部のメンバ変数が集まってできることもあります。

ツリー構造に対応するものは、コンポジット・オブジェクトによる 垂直的なツリーだけでなく、リンカブル・オブジェクトによる水平的な ツリーも含めて実装されることになるので注意してください。

リンクするオブジェクトが、ある特定の操作関数だけに必要な場合、 クラスの内部でポインタを持つ必要はありません。 関数の引数に渡すようにします。


3-1.コンテキスト変数

コントロール・レイヤは、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 );    /* 必要なリンクを指定 */
};
後始末関数(破壊、ライフサイクル終了)も同様にします。


3-2.イベント関数

ユーザの操作や割り込みなどのイベントが起きたときに 実行する関数をイベント関数と呼びます。 イベントによってプログラミングすることを イベント・ドリブンと呼びます。

イベント関数は、アプリケーション・オブジェクト操作関数に位置し、イベントの種類と現在の状態に応じて シナリオを記述します。
void  App_onMouseMove( App* c, Msg* msg )  /* マウスの割り込み関数 */
{
  if ( c->soundEnable ) {            /* 状態を判定 */
    Speaker_setSwitch( SPEAKER_ON );   /* スピーカーのスイッチを入れる */
    PCM_startSound( msg->beepType );   /* 音を鳴らし始める */
    PCM_waitTillFinish();              /* 音が鳴り終わるまで待つ */
    Speaker_setSwitch( SPEAKER_OFF );  /* スピーカーのスイッチを切る */
  }
}
シナリオは、ユーザが理解できる可読性の高いもので、 コンテキストに含まれるオブジェクト操作関数(サービス関数)を 呼び出すようにします。 イベント関数は、コンテキストメッセージイベントの内容)を 引数に取るといいのですが、割り込みの場合はそれらに相当する グローバル変数や I/O で渡されることが多いです。

イベント関数が大きくならないためにも、 詳細なプロセスはモジュール・レイヤの 各種オブジェクト操作関数サービス関数)の内部に 入れるようにします。 その際、コンテキスト操作関数の引数に渡さないようにしたほうが 良いでしょう。なぜなら、 オブジェクトコア・オブジェクトコンポジット・オブジェクトリンカブル・オブジェクト)がコンテキストの構造に依存してしまう 点と、リンクするオブジェクトがメンバ変数名に依存してしまうためです。 操作関数の所属するクラスオブジェクトを引数に渡すようにします。


3-3.モード関数

ポーリング(入力があるまで何もしないループを繰り返す)したり、 イベントがあるまでスリープ(ウェイト)する場合、 関数は、イベントが発生して呼び出されるものではなくなり、 ある特定のモードを形成する関数になります。

このモード関数は、アプリケーション・オブジェクト操作関数に位置し、内部にメッセージ・ループを持ちます。 メッセージ・ループの内部では、メッセージのタイプを 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 ...
    }
  }
}
メッセージは、コンテキストではなく、ローカル変数にすると 区別がついて分かりやすいでしょう。

モード関数が大きくなりすぎないためにも、 イベントのタイプに対応したイベント関数に分割したり、 詳細なプロセスはモジュール・レイヤの 各種オブジェクト操作関数(サービス関数)の内部に 入れるようにします。


written by Masanori Toda from May.24.1999