C言語講座14回目。
C言語講座としていますが、今回はどのような言語でも通じる話だと思っています。
この業界で長くシステム開発にかかわっていると、中にはすごく長いプログラムに出会うこともあります。
今まで出会った中で、一つの関数の最大長が、7000行以上あるものがありました。
再構築・作り直しのプロジェクトでであった過去のプログラムでしたが、どこで変数が宣言されているのかもわからなくなるし、とても追い切れないというもので、移行では大変な思いをしました。
そんなこともあり、今回は、関数の長さについてです。
関数の長さはどの程度が良いか
プログラミングする関数は、1画面におさめよう
1関数は、1機能がいい。さらに最近はあまり聞かなくなりましたが、一つの関数、モジュールは、1画面に収まるようにコーディングするのが、基本だと考えています。
20年くらい前はプログラミング雑誌が月に何冊も発行されていて、既にほとんどが廃刊になっていますが、その中で何度もそんな記述を見かけました。
当時の画面とすると、解像度も低いし、1画面でみれる行数は、せいぜい25行程度です。
25行に収めるのは、場合によっては多少苦しいですが、今でもこの考え方はそう間違っていないと思っています。
冒頭で書いた7000行のプログラムは極端な例だとしても、数百行のプログラムはよく見かけます。
構造化プログラミングは、順接-分岐-繰返し、といって、延々とそのパターンでコーディングされたプログラムで、一関数が500行も800行もあるものは、他人が開発したものだと追うのが大変です。
人間、そんなに多くのことを一度で理解はできないし、理解できないようなものを一生懸命作るから、バグが入り込んでしまうという悪循環に陥ります。
それでも懸命にコメントをつけて、体裁を整えるのが通常のプログラムかも知れなませんが、コメントの下に延々とプログラムが書いてあって、とても追いきれなくなって触らずにそっとしておこう、という話がときどきつぶやかれています。
プログラミングで関数を短くする6つの理由
プログラミングで1つの関数を1機能で、短くする理由をまとめてみました。
既に述べた、「わかりやすさ」というものが、全てに優先する理由ですが、短くした場合の利点はそれだけではありません。
(1)関数単位で考えた場合、単一機能のみに思考を限定できる。
(2)関数単位にバラバラにテストすることが可能である。
(3)開発を進めていくと、先につくった関数の再利用ケースがでてくる可能性がある。
(4)ある機能が、再利用部品にできる可能性がある。
(5)そのままで、再利用できない場合でも、一部の関数(機能)のみのコピー変更で対応できる場合もある。
(6)仕様変更が発生した場合の対応が関数に限定できる。
これだけ理由もあるし、関数に分けて作ると、開発効率は確実にあがります。
欠点は、(3)、(4)を突き詰めすぎるとプログラミングをした人以外が理解するのに、逆に時間がかかってしまう場合があることです。
共通化をするのであれば、きちんとした設計とドキュメントを残すことが大切です。
プログラミング:関数を分割する方法
ここからは、関数を小さくする方法、分割する考え方について書きます。
例えば、とあるロジックの中で、一度長さを計算して、その結果を使って何かの処理する関数があるとします。
仕様書では、例えばこんな記述
ABCロジックでは、以下の処理を行う。
・初期化処理(開始ログの出力...)
・表示文字($以降の文字)列の長さを計算
・$で始まる文字列を表示
まず仕様書にあわせて、そのままコーディングします。
int ABCLogic(char *pAry[]) { int iSize, i; char* p; /* いろいろな初期化 */ ~ /* $ 以降の文字列の長さを調べる */ iSize = 0; for (i = 0; pAry[i] != NULL; i++) { /* $を見つけて、見つかった場合、それ以降の文字列の長さを加算 */ p = strchr(pAry[i], '$'); if (p != NULL) { ++p; iSize += strlen(pAry[i]); } } if (iSize <= 0) { /* エラー処理とか... */ return ERROR; } /* 表示処理 */ ~複雑でとっても長い処理 ~ return NOERR; }
この程度ならまだ許せる、と言って気を抜いていると、プログラムはどんどん肥大化し、いつのまにやら、300行、400行、500行と増えていきます。
仕様書は、初期化、長さ取得、と書いてあります。
実際にプログラムにしていくと、初期化で数十行とか、長くなることもよくあります。
一方、人の理解は、初期化するという理解です。
とすれば、プログラムもそのように分割して記述してしまった方が、わかりやすくなります。
内部関数は、通常staticを付けて、モジュール外からは、参照できないようにするのは、大規模開発では実施した方がいい、テクニックです。
/* いろいろな初期化 */ static int ABC_Init(必要な引数) { /* 初期化処理 */ } /* $ 以降の文字列の長さを調べる */ static int ABC_CalcSize(char* pAry[]) { int iSize, i; char* p; iSize = 0; for (i = 0; pAry[i] != NULL; i++) { /* $を見つけて、見つかった場合、それ以降の文字列の長さを加算 */ p = strchr(pAry[i], '$'); if (p != NULL) { ++p; iSize += strlen(pAry[i]); } } } static int ABC_Disp(/* 必要な引数 */) { /* 表示処理 */ ~複雑でとっても長い処理 ~ } /* メインのABCロジック */ int ABCLogic(char *pAry[]) { /* いろいろな初期化 */ if (ABC_Init(/* 必要な引数 */) == ERROR) { /* エラー処理とか... */ return ERROR; } /* $ 以降の文字列の長さを調べる */ if ((iSize = ABC_CalcSize(pAry)) <= 0) { /* エラー処理とか... */ return ERROR; } /* 表示処理 */ if (ABC_Disp(/* 必要な引数 */) == ERROR) { /* エラー処理とか... */ return ERROR; } return NOERR; }
処理単位に分割することで、関数を短くすることができます。