読者です 読者をやめる 読者になる 読者になる

モジュール指向プログラミングのすすめ

 C言語は「スコープを管理する言語機能が貧弱」「オブジェクト指向言語として設計されていない」といった要因から、C++Javaといったオブジェクト指向言語と比べ、大規模開発に向いていないとよく言われている。

 相対的に見れば実際そうなのだけれども、そういった欠点は、伝統的なモジュール指向の構造化技法をコーディング規約で支えることによって、ある程度改善することもできる。今回はその一例として、学生時代の頃辺りから使ってきた「モジュール指向」とそれを支えるコーディング規約のセットを紹介したいと思う。
 なお今回はCを対象にしているものの、他の手続き型言語でも同じアプローチの導入は十分可能だと考える。

 以下、モジュール指向プログラミングの方針とその規約を示す。


※モジュール指向という用語は他所でもらほら見られる言葉だが、大体今回述べる内容のものと似た意味合いで使われることが多い。今回の定義も特に問題のないものと一応考えている。

※今回の内容は手続き型言語で昔から伝統的に言われてきたテクニックなので、全く高度なものでも、目新しいものでもない。ただ簡単かつ単純ながら、規約として運用していくメリットはとても大きいと感じる。

パラダイムの目的

 このパラダイムは、以下のメリットを実現することにより、プログラムの設計・開発・保守作業を効率化することを目的とする。

  1. 参照構造の明示化
  2. スコープの積極的な制限
  3. 定義・宣言の構成の厳密化
  4. プログラム構造の抽象化

概要

 このパラダイムの概要は以下のようになる。

  1. ヘッダファイルとソースファイルの組でモジュールを形成する。
  2. モジュールでソフトウェアを構成する。
  3. ファイルインクルードでモジュールのスコープを管理する。
  4. モジュールの内部データや内部関数はカプセル化する。

パラダイムを支える規約

 使用する基本的なコーディング規約を以下にまとめる。強制力に関しては、「(推奨)」のないルールはすべて必須ルール(不可能でない限り例外を認めないルール)に該当する。

●ヘッダファイルとソースファイルを1対多の構成でモジュールとしてまとめる。そしてモジュールとヘッダファイルによってプログラムを構成する(推奨)

 具体的に言うと、宣言を行うヘッダファイルと、その定義を行うソースファイルの組をモジュールとする。通常はヘッダファイル:ソースファイルが1対1の対となるが、定義が複雑な場合はソースファイルを分割し1対多の構成を導入してもよい。
 なお例外として、以下の記述のみからなるヘッダファイルに限り、モジュールの形式を取らなくてもよい。

 ちなみにconst型変数に関してもヘッダファイルのみで管理されることがあるが、今回の規約では基本それを認めない。const型変数に関しては、extern宣言を使うことで通常のモジュールの形に合わせることが望ましい。

 この規約は、変数や関数のスコープをモジュールという構造で管理することにより、凝集度・結合度の改善や、変数・関数の参照構造の明確化を促進することを目的とする。

●モジュールごとに固有の識別子をつけ、ヘッダファイルにそれを明記する

 具体的に言うと、モジュールや、モジュールに属しないヘッダファイルに、重複しない識別子をつける。そしてコメントなどの形で、ヘッダファイルにその識別子を明記する。

 この規約は、ファイルや関数・変数がどのモジュールに属しているか明確化するために存在する。主に他の規約を支えるためのルールである。

●モジュールを構成するヘッダファイル・ソースファイルの名前には、モジュール識別子を含める

 例えば「foo」という識別子をつけたモジュールを構成するファイルには、「foo.h」「foo.c」「foo_io.c」などといった、モジュールの識別子を含む名前をつける。

 この規約は、ファイルがどのモジュールに所属しているか明確化することを目的とする。

●モジュール外から参照される変数・関数の名前には、モジュール識別子をプレフィックスとして追記する

 例えば「foo」という識別子をつけたモジュールのextern変数や外部スコープ関数には、以下のようにモジュール識別子をプレフィックスとした名前をつける。

/* 外部スコープ関数 */
void foo_init(void);
void foo_bar(void);

/* 広域変数 */
extern int foo_baz;

 この規約は、外部公開されている変数や関数がどのモジュールに属しているか明確化することを目的とする。この規約の運用により、関数や変数がどこで定義されているか把握するのが容易になる。

●ヘッダファイルではインクルードガードを記述する

 具体的には、ヘッダファイルの記述内容を、以下のようなプリプロセッサ構文で囲むようにする。

#ifndef FOO_H_
#define FOO_H_

/*ヘッダファイルの内容をここに書く*/

#endif

 なお「#pragma once」のように処理系が別途インクルードガード構文を用意しているならば、それで代用してもよい(その場合コードの移植性が低下するが、本規約セットの目的から外れるため今回は無視する)。

 この規約は、ファイルインクルード(モジュールのインクルード)を柔軟に記述できるようにすることを目的とする。もしこの規約を運用しない場合は、多重宣言・多重定義の防止のためインクルード構造を厳密に管理しなければならず、モジュールのインクルードを適切に明示できなくなる恐れがある。

●ヘッダファイルに関数定義、変数定義を記述しない

 関数や変数に関しては、ヘッダファイルには宣言のみ記述し、その定義はソースファイルに記述する。

 この規約も、ファイルインクルード(モジュールのインクルード)を柔軟に記述できるようにすることを目的とする。

●モジュール外から参照されない変数・関数はstatic宣言を行う

 具体的には、内部スコープの変数・関数はstatic宣言を行い、他のソースファイルから隠蔽する。

 この規約は、モジュールのインターフェース管理の厳密化により、コードの保守性やテスタビリティを改善することを目的とする。

記述例

具体例として、fooモジュールを規約に沿って記述したものを示す。

foo.h
/* fooモジュール */
/*! ↑モジュール識別子の明示 */

/*! ↓インクルードガードの記述 */
#ifndef FOO_H_
#define FOO_H_


/* 外部スコープ関数 */
void foo_init(void);
void foo_bar(void);

/* 広域変数 */
extern int foo_baz;

/*! ↑外部公開する関数・変数にはモジュール識別子をつける */

#endif
foo.c
#include "foo.h"

int foo_baz;

/* プロトタイプ宣言 */
static void foobar(void);
static void barbaz(void);

/* 内部スコープ変数 */
static int mode;

/*! ↑内部スコープの関数・変数にはモジュール識別子をつける必要はない */
/*! ↑内部スコープの関数・変数はstatic宣言する */


void foo_init(void)
{
    ...
}

void foo_bar(void)
{
    ...
}


/*! ↓内部スコープの関数はstatic宣言する */
static void foobar(void)
{
	...
}

static void barbaz(void)
{
    ....
}