第3回 プログラミングについて 『良いプログラムって何?』

『良いプログラムって何?』
『良いプログラムって何?』ということで、今回は良いプログラムを書くためのには何が大切なのかを考えてみます。
まずはエラー処理の話し。
エラー処理というと多分プログラムを作った方ならば誰でも御存知かと思います。ファイルをオープンしようとしたらファイルがなかったとか、入力された値が不適切だったりとか様々です。
プログラムと一口に言っても、そのとき一度しか使わないものもあれば、数年間あるいは10年以上に渡って使用されるものとプログラムによって違いがあります。確かにこのデータだけを今日じゅうに何とか変換できればいいといった緊急のときには、エラー処理もくそもあったものではなく、時間との勝負で作ってしまうこともありますが、おおよそのプログラムは長い間使われることになります。長い間頻繁に使われるほどエラーの処理をしっかりとしておかなければなりません。
さて、あるプログラムを実行すると次のようになってしまいました。
$ test
<Stat>
--- エラー ---
<End>
まだエラーだと教えてくれるだけましなのですが、使う方にとってはこでれはたまったものではありません。いったい何がエラーなのか分らないのでは途方にくれてしまいます。
また次の様なデータファイルがあったとします。プログラムがアルファベットの順番があっているかを調べるものだとして、
abcdefghijklmnopqrstuvwxyz
abcdefghijklmnopqrstuvwxyz
abcdefghijklmnopqrstuvwxyz
abcdefghijklmnopqrstuvwxyz
:
:
abcdefghijklmnopqrstuvwxyz
abcdefghijklmnopqrstuvwxyz
abcdefghjiklmnopqrstuvwxyz
abcdefghijklmnopqrstuvwxyz
abcdefghijklmnopqrstuvwxyz
$ test
<Start>
--- アルファベットの順番に誤りがあります ---
<End>
となっても少しは親切ですがこれではどこが間違っているのかを1つ1つ調べなければなりません。
$ test
<Start>
--- アルファベットの順番 (ji) に誤りがあります ---
<End>
だとかなり良くなってはきましたが、これでも一体どの行が悪いのかが分りません。
$ test
<Start>
--- アルファベットの順番 (ji) に誤りがあります ---
( abcdefghjiklmnopqrstuvwxyz )
ファイル data.dat の 20 行目。
<End>
となれば満足できるかと思います。カラム数までを表示するか否かは作る側の思想にも依りますが、今までの経験から言えばそこまでの必要はないみたいです。
おおよそプログラムの大半はエラー処理です。このエラー処理がどこまで行なわれているか否かでプログラムの良し悪しの殆どが決ってしまうと言っても過言ではないでしょう。エラーメッセージの例を紹介しましたが、このメッセージが悪いと使う側はエラーの対処に無駄な時間をかけることとなってしまいます。プログラムを書くのは1回ですが、使うのは1回ではありません。エラーメッセージは『どこが、どうして、どう悪いのか』をできる限り簡潔に表現した方が良いでしょう。
次は操作性。
「どうしても間違って入力してしまう」などと聞くことがあります。こういときプログラムをよく見てみるとプログラムの方に責任があることが多いようです。データを入力する人や操作する人にどうしても責任を転化してしまいがちですが、もともと入力しにくいフォーマットや間違えてもあたりまえな操作を要求するプログラムが悪いのです。
例えば、基板設計用のCADシステムではソースデータの代表選手であるネットリスト。これは部品のどことどこを接続すべきかを記述するデータファイルですが、
(Netlist "Test data"
(Signal_net
(Net "BUS1"
(
(Layer 1)
(Connect
(Reference "IC1" Pin_number 1)
(Reference "IC2" Pin_number 2)
)
)
)
)
)
という表現をしなければならないものがあったとします(フォーマットは架空のものです)。これは Test data というタイトルの接続データで、信号線には信号名が BUS1というものがあり、それは第1層で接続すべきで部品番号 IC1 の1番ピンと部品番号IC2 の2番ピンを結ぶものである。といった意味と解釈して下さい。いかにもLISP風な拡張性にも富んだ美しいフォーマットですが、このフォーマットには欠点があるのです。それは入力しずらいということです。5行や10行程度のものであればなんら不自由はないのですが、これが3000行ともなると事情が変わってきます。
同じ文字列を何度も入力しなければならず、段下げをしっかりとやっておかなければどのカッコがどのカッコに対応しているのかが分らなくなってしまいます。
入力といってもテキストデータをキーボードから打ち込むことだけではなく、CADソフトのようにマウスをクリックしながら座標の入力や編集をすることもあります。操作性が特に問題にされるのはこういった種類のプログラムです。私もこれまでに両手に余るグラフィックエディタを作ってきましたが、初期のものと最近のものとではかなり操作性に差があります。やはり最近のものの方が操作性は良いようです。
いったい何が良ければ操作性が良いのかと言えば『違和感がない』というところでしょうか。人間に変な操作を強要せず全体に統一されたインターフェイス(操作の手順など..)となっていることです。実際自分自身で何度も操作をしてみて、少しでも間違った操作をしてしまったり自分の感覚と違った要求をプログラムがしたときなどは、操作する人間が悪いのではなく、人間の感覚に合っていないプログラムの方に問題があるのです。
グラフィックエディタなどは全体が100とすると、人間とのインターフェイスのために記述する部分はエラーの処理を含め80~90ぐらいを占めていて、実際にデータを操作する部分はほんの少ししかありません。極端な例では、インターフェイスの部分が2000行でデータの処理に1行などというコマンドもいくつもあります。操作性の良いプログラムを作るには『経験がものをいう』ことが多いことは確かです。
操作の手順も重要ですが次の様なことにも大変気を使う部分の1つです。
『1つの端点が同一座標にある2本の直線(いわゆる折れ線)を任意の座標をマウスで指示することで、そのどちらかを選択する』と言う命題。座標の違っている端点はいとも簡単ですが、同一の座標の端点はどういう風にして選択したらいいのでしょうか。
マウスがぴったりとその座標を指示したときはどうしようもありませんが、人間というのはそのどちらかを選択したいという意志が微妙な座標のズレに現れます。またこれを検出して人間の意志に近い選択をできるならば意識的にズラして選択することもできます。
実行スピードもまた『良いプログラム』の要素です。同じ処理を行なうのならば速い方がいいに決っています。現在の計算機の処理能力でしたらどんな書き方をしても結構快調に実行してくれます。第1回目のときにもお話しましたが、15年程前は1MIPSの計算機でいかに速く処理するかを必死になって模索していました。その当時から比べれば現在のものは100倍も200倍も計算機の処理パワーがありますので、プログラムをつくる側にとっては負担が減っているのは確かです。しかしながら計算機の能力が増大してくると、それの伴って処理しなければならないデータも増加し、さらに複雑なことを要求されますのでイタチゴッコと言えなくもありません。
最後にプログラムの内側の『良いプログラム』を考えてみます。
基本的なことですが読み易いリスト。これが最重要な要素です。適度なスペースとインデント、簡潔で的確なコメント、直感しやすい変数名と関数名、全体を把握しやすい構造など数えればきりがありませんが、長い間保守されるプログラムであればあるほどリストの読み易さは重要になってきます。ただ単に見た目がいいというリストであれば簡単なのですが、保守を目的とした読み易さのためにはこれも長い間の積み重ねが必要になります。
こう言ってしまえばあまりにも無責任ですので、今回は少しだけ指針になることをあげてみましょう。
・変数名、関数名、定数名の名付け方
・何を関数にし、いつどこで使うか
・どのようなデータ構造とするか
・できる限り単純な表現をする
・間違い易い表現はしない
・統一した表現を行なう
などが考えられるでしょう。
変数、関数、定数などの名前の付け方には十人十色ではありますが、おおよそ言えることは、値が有効な範囲が狭い変数には短い名前を、1つの関数内程度の範囲で値が有効なものには長中間的な長さの名前を、複数の関数に対して値が有効変数には長い名前を付けます。関数名はローカルな関数には短い名前でも構いませんが、関数名を見ただけでおおよその機能が分るような名前にします。定数は意味のある値にたいしては名前を付ける。ということです。
例えば
#define I 100
int i[I];
int loop_counter;
for(loop_counter=0;loop_counter<I;loop_counter++){
i[loop_counter]=0;
}
と表現すると非常に理解しずらいものになってしまいます。単純で短いループのカウンタには短い単純な名前を配列などの変数には長い名前を、定数名にも意味の分る様なものにすべきです。そこで
#define MAX_ARRAY 100
int array[MAX_ARRAY];
int i;
for(i=0;i<MAX_ARRAY;i++){
array[i]=0;
}
とした方がわかりやすくなります。単純なループのカウンタの名称に i とか j とかの名称を使うのは、じつはFORTRANでの暗黙な変数の型の規約に変数名が i~n から始まっていれば整数型でそれ以外は実数型というものがあって、この言語での記述の慣習が他の言語にもそのまま踏襲されているものです。慣習とは暗黙の規則と言えるものですので、このような命名法はそのまま受け入れた方が良いようです。また i とかj とかの変数は値の寿命が短い変数に付けるとすれば、気軽に使い回しができるということにもなります。
定数にも意味のあるものには名前を付けた方が良いでしょう。自分でも、急いでいるときやあまり重要ではないと思った定数にはそのまま値を記述してしまって、後になってその値の意味が分らなくなることがあるのですが、
void sub(mode)
int mode;
{
if(mode==3){
:
:
}
else if(mode==5){
:
:
}
}
などと記述されていると、3と5という値が一体どういう意味を持ったものなのかが分らなくなってしまい、あちこちの関数の内容を調べる事態となってしまいます。その上3という値の意味を変えたいときなどには、どの関数の3という定数がそれなのかが分らなくなって改修に時間がかかったり、バグの原因ともなってしまいます。
#define MODE_ADD 1
#define MODE_DELETE 2
#define MODE_COPY 3
#define MODE_MOVE 4
#define MODE_DISPLAY 5
void sub(mode)
int mode;
{
if(mode==MODE_COPY){
:
:
}
else if(mode==MODE_DISPLAY){
:
:
}
}
とした方が理解しやすいでしょう。さらに定数を定義する #define 文を各関数に重複して定義する可能性があるのであればこの定義の部分を別のファイルに分けて #include 文を使って読み込んだ方が良いでしょう。
関数の使い方や関数名の付け方にも工夫が必要です。よく「何を関数にすればいいのですか」と質問されることがあります。この答えもなかなか難しいものがあります。
何度も行なう処理を関数にすることも、一度しか使わないものを関数にすることも、数行の処理しか行なわないものを関数にすることもあります。これも経験的にそうするのだということになりますが、関数にする目的はプログラムを組み立て易く、理解し易いようにするためと言えるでしょう。
main()
{
sub1();
sub2();
sub3();
}
などと関数名を命名すると一体何をしているのかが関数名だけでは分りません。
main()
{
load_data();
check_data();
display_error();
}
などとなっている方が良いでしょう。
またどういうときに関数にするのかといえば、先に述べましたがやはりケースバイケースですが、上記のサンプルのように機能単位で作成するのが一般的といえるでしょう。
また複雑な関数やループのを記述するときに、あまりにも色々な処理をし過ぎて冗長な表現になってしまうときに、たとえ一度しか呼び出すことがなくても各機能単位を関数にする方が良いこともあります。
for(loop=0;loop<MAX_DATA;loop++){
for(i=0;i<MAX_DATA/2;i++){
for(j=i+1;j<MAX_DATA/2;j++){
if(data[i]>data[j]){
k=data[i];
data[i]=data[j];
data[j]=k;
}
}
}
for(i=MAX_DATA/2;i<MAX_DATA;i++){
for(j=i+1;j<=MAX_DATA;j++){
if(data[i]>data[j]){
k=data[i];
data[i]=data[j];
data[j]=k;
}
}
}
}
と記述すると一体何をしているのかが一見して分らなくなることがあります(内容にはあまり意味はありませんが、これぐらいはまだ単純ですが..)。このようなときには
for(loop=0;loop<MAX_DATA;loop++){
sort_data(data,0 ,MAX_DATA/2);
sort_data(data,MAX_DATA/2,MAX_DATA );
}
として関数を呼び出すようにした方が分り易くなります。
C言語はかなり表現の仕方が自由ですので、
for(i=0;i<10;i++)if(i==j)for(k=0;k<10;k++)sub(k);
などという表現は、いくら自由だからといっても止めた方が良いでしょう。こういうときには、
for(i=0;i<10;i++){
if(i==j){
for(k=0;k<10;k++)sub(k);
}
}
としておかないと
for(i=0;i<10;i++)if(i==j);for(k=0;k<10;k++)sub(k);
とセミコロンを途中で入れてしまって思わぬバグの原因にもなりかねません。
式の表現も単純にした方が良く、あまり一度に色々なことをすると混乱の元になります。
例えば、
if(array[i=k++ + ++j]== ++m)s++;
これは分りにくい上に間違いを発見しずらくなります。初心者の方に多く見られますがあまりにも背伸びをしようとして、頑張らなくてもいいところまで頑張ってしまった結果、変なプログラムにしてしまうこともあります。無理をしないで記述すべきです。
定数の記述の仕方にも気をつけておかないと分りずらいプログラムになってしまうことがあります。
a=2.513247122872;
とコメントもなく突然書かれていたら、この数値の意味がさっぱりわかりません。
a=2.513247122872; /* pai-pai/5 */
とコメントするか、
a=pai-pai/5.0;
と円周率を定数名に定義しておくか、
a=3.1415926539-3.1415926539/5.0;
と記述しておくべきです。円周率のような値のときには定数名を定義して使用するのが普通ですが、一般的ではない値のときには後になっても分るようにしておくべきでしょう。
とりとめのない話しになってしまいましたが、また次回としましょう。