●プログラミングスタイル CERT β版

目次


1.プログラミングスタイル CERT

深いツリー状になっているモジュールの構成は、見通しが悪いので 良くない構成と言われます。よって、なるべくフラットにした 構成にすることが求められます。 『モジュール化による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  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 );
}

モードに所属するオブジェクトをそれぞれの開発者が担当する場合、 モード・オブジェクトは、特に協調作業が必要となります。 下手をすると協調作業が指数的増えてしまいます。 そうならないために、主な処理をモードの所属するオブジェクトの操作関数にさせ、 モード・オブジェクトはその協調関係のみ扱ってなるべく大きくならないようにします。

モード・オブジェクトは、モードの所属するそれぞれのオブジェクトの 単体テストにも必要になるので、(モードに直接所属するオブジェクトの) 開発者全員が共有することになります。 しかし、モード・オブジェクトがそのままでは、 すべての開発者のオブジェクトを集めなければなりません。
それを回避するためには、何も内容が無い、または定数を用いて最低限の出力を備えた スタブ・オブジェクトを用意します。 スタブ・オブジェクトは、モードに所属するそれぞれのオブジェクトの開発者が 開発中のオブジェクトを元にインターフェイスを決め、 モード・オブジェクトの開発者が作成します。 そして、スタブ・オブジェクトも全員が共有するようにします。


3-2.モード関数

アプリケーションのある特定のモードするときに呼び出し、 モードが終了したら関数から抜ける関数をモード関数と呼びます。
モード関数は、モード・オブジェクトの操作関数になります。

モード関数の最初や最後では、モードの開始、終了のイベントハンドラを呼び出し、 主に次のような内容を実行します。

モード内にポーリング(入力があるかどうか定期的に確認すること)する 入力装置がある場合は、モード関数内にメッセージループを形成します。 メッセージループの内部では、メッセージイベントハンドラに対応付けます。

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 );  /* メインスレッドを起動する、本関数終了時に稼動 */
  }
}
マルチスレッド環境では、一般にメインスレッドや長時間スレッドは優先順位を低く設定します。 (メインスレッドはモード・オブジェクトを実行するスレッドです。)


3-3.ディスパッチャ

(作成中)


3-4.イベントハンドラ

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

イベントハンドラは次のきっかけで呼び出されます。

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

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


4-1.非同期コール

マルチスレッド環境では、全体から見て最もハードウェア入力に近いオブジェクトを モード・オブジェクトよりも優先順位を高くします。 このオブジェクトを割り込み発生オブジェクトと呼びます。 割り込み発生オブジェクトやモード・オブジェクト以外の、 その他の一般的な処理を行うオブジェクトをワーク・オブジェクトと呼び、 モードオブジェクトよりも優先順位を低く設定します。

たとえば、シリアルマウスを使うモードでは、 シリアル・コントローラを割り込み発生オブジェクトとし、 マウス・オブジェクトは、ワーク・オブジェクトの中で最も優先順位が高く 実行されるシリアル・イベントハンドラを持ちます。 マウス・オブジェクトのシリアル・イベントハンドラが呼び出されるまでのステップは 次のとおりです。

  1. 割り込み発生オブジェクト(シリアル)の割り込みハンドラが モード・オブジェクトのシリアル・イベントハンドラを起床
  2. モード・オブジェクトが、マウスのシリアル・イベントハンドラを起床。

一般に、マルチスレッドを使うのは、反応よくするためという、 優先順位の高いオブジェクトの都合によるものなので、 優先順位の高いオブジェクトがタスクやキューの管理を行い、 優先順位の低いオブジェクトがマルチスレッドに依存しないで済むようにします。
そのためには、非同期コールの仕組みを優先順位の高いオブジェクトの操作関数にします。 非同期コールは、通常の関数呼び出しに似ていますが、 優先順位の低い別のスレッドが実行するために呼び出してすぐ処理を行わないことと、 返り値をすぐに受け取れないところが異なります。 返り値は、モード・オブジェクトのイベントハンドラにより取得できます。

非同期コールの例
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;

   :
}

非同期コールは、処理を中断することができます。 処理関数のループの中など、適当なところでメンバ変数をチェックします。

モードを終了するときは、非同期コールによるスレッドに中断メッセージを送り、 すべてのスレッドを終了させます。


4-2.マルチスレッド処理

すばやい反応をするために行うスタック的な割り込みによる非同期コールと異なり、 長時間に同時に複数の処理を交互に実行するときは、排他制御の必要なリソースに対する クリティカル・セクションを処理関数内に形成します。 タイマー割り込みが使えるのであれば、タイムシェアリングを行い、複数の処理を切り替える間に 排他制御を行うことができるので処理関数を修正する必要はありません。


written by Masanori Toda from May.24.1999