このプログラムは何をするものか分かりましたか。では、リスト12−1と12−2を両方見比 べながら調べていきましょう。
ファイルの存在の有無を調べるのに、openを使うのは賢明ではありません。openしたファイルを closeするのは面倒です。どのCにでも、ファイルのアクセス権を調べるaccess関数が存在するは ずです。これを使えば、ファイルをオープンすることなく、ファイルの状態を調べられます。もち ろん、openしていないので、closeは必要ありません。
これは、とても軽症で、そんなにとやかく言う程のものではありません。
では、リスト12−1で、複写元ファイルのopenとcloseの対応を調べましょう。openは32行に あります。closeは、165行と182行の2個所にあります。徹底的にネストにこだわるのなら、192行 の } の次にcloseを書くべきです。でも、けっして分かりやすくありません。これも、openと closeが「長距離」になっているからです。
複写元、複写先の両ファイルをオープンし、コピー用のバッファ(実は1KB)をmallocしてい ます。次にforループに入り、複写元から複写先へ1KB単位でmallocで確保したバッファを経由し ながらファイルの中身をコピーしています。
ということは、どういうことでしょう。ファイルをまるまるコピーしているだけなのです。UNIX のcpコマンドがやるのと同じことを長々と書いているだけです。ですから、ここの「行数稼ぎ」は、 system関数を使い、unixのcpコマンドを実行するようにすれば一発ですんでしまいます。もちろん、 中途半端な1KBのバッファなど確保する必要もありません。確保していないのだから、freeの必要 もありません。それに、1KB程度のバッファがどうしても欲しいのなら、mallcするより、関数内 部に局所変数として確保すべきです。
この部分と同様のことが、例の有名なCの本(参考文献12−1)に、もう少しエレガントに書か れていますが、あれはcpコマンドの解説として出ているのです。でも、何の考えもなく、あの本の 一部をコピーすれば、あの本の著者並みの良いソフトウェアになる訳ではありません。
cpコマンドを使うと、複写元ファイルのopenが不要になり、ファイル記述子fhsも不用になりま す。どんどん単純になっていきますね。
for( exp_a ; exp_b ; exp_a )今回の場合は比較的短いのですぐに理解できますが、それでも望ましくはありません。こういう場 合は、
while( exp_a, exp_b )とか、
for(;;) { exp_a; if( ! (exp_b) ) break; }など、できるだけ同一式が出てこない方法に書き換えます。リスト12−2では、永久ループの for(;;)を使った書き方にしました。これは、readとwriteが対なので「同一レベル」に並ぶように し、エラー処理も読み込み、書き込み直後に行なうようにしたためです。readが1回、writeが1 回の方がバランスが取れていると思いませんか。
でも、この部分は、もっとちゃんと修正したリスト12−3ではなくなってしまいます。
プログラムは、「準備」、「処理」、「後始末」の三段論法で、順に流れるのがベストです。だ から、「後始末」はプログラムの最後に位置しなければなりませんが、関数の途中にあるので分か りにくいのです。リスト12−2では、この後始末を関数の最後にしました。
でも、まだ分かりづらいのは、正常時にも、異常時にも通過するためです。
リスト12−1またはリスト12−2の関数notice_promptのNOTICE_MESSAGE_STRINGSに続く大 文字の名前はマクロ名で、
#define SYS_MESSAGE_FILECOPYE "複写できません。"というように文字列に定義されています。これらのマクロ定義は、1つのファイルに書かれている ので、良くまとまっていると思うのは軽率です。
それどころか、このメッセージ定義ファイルの内容を変更しようものなら、makeしたとたん、全 ファイルのコンパイルが始まります。短いプログラムなら不平もありませんが、このプログラムは 十万行を越えていて、20分程度待たされてしまいます。だから、とても気楽にメッセージの変更は できません。本当に気合いを入れて、「やるぞー」という意気込みで開始し、散歩か買いものにで も出かけるのでなければしてはいけません。
これだけの長さになると、まず一人で開発することはありません。複数人での開発ですから、全 プログラムをコンパイルしている間は、全員の作業がその間停止してしまいます。もちろん、その ようにならなくする一時のがれの手段もありますが、いやらしい逃げ道にすぎません。これでは、 もう作業になりません。
ウィンドシステム以外でも、起動時などにユーザ毎の環境設定ファイルを読み取り、ユーザ毎に 異なった動作をするようにプログラムを作るのは今や常識ですよね。ウィンドシステムでは、始め からそのような機構が存在します。ぜひ使って楽をしましょう。
オリジナルはOpenWindows上で動作するように、XViewライブラリの関数notice_promptを使って 作られていました。アプリケーションは、できることならどこのメーカーのコンピュータでも動作 することが理想です。OpenWindowsでは特定メーカーのコンピュータでしか動作しなくなり、他の メーカーがより優れたコンピュータを出しても移植が大変です。それで、特定コンピュータメーカー に拘束されないように、よりオープンなMotifへ移植しました。
リスト12−3を見てください。このリストはMotif版のものです。ここでは、XViewの関数 notice_promptの代わりに、Xウィンドのリソースデータベースの機能をちゃんと利用した関数 my_notice_promptを作成し、利用しました。この関数は、第2引数にエラー種別を文字列で指定す ることで、この文字列から表示すべき文字列などを取り出し、エラー表示をします。
リスト12−3 修正版(その2) エラー処理を統合化(Motif版) |
1 int file_copyfile( filed, files, job, page, comment ) 2 char *filed; /* 複写先ファイル名 */ 3 char *files; /* 複写元ファイル名 */ 4 char *job; /* 複写先JOB名 */ 5 char *page; /* 複写先頁番号 */ 6 char *comment; /* 複写先コメント */ 7 { 8 int ret; 9 int fhd = -1; 10 SYS_PAGE_HEAD head; 11 BASE_INFO destbase; 12 char command[1000]; 13 /* 複写先ファイルの存在確認 */ 14 if( access( filed, O_WRONLY ) == 0 ) { 15 ret = my_notice_prompt( master_menu_panel, "filecopy_check" ); 16 if ( ret != NOTICE_YES ) 17 return -2; 18 } 19 /* ファイルのコピー */ 20 sprintf( command, "cp %s %s 2> /dev/null", files, filed ); 21 if( system(command) != 0 ) { 22 my_notice_prompt( master_menu_panel, "filecopy_cp_error" ); 23 return ERROR; 24 } 25 /* 複写先ファイルのオープン */ 26 if( (fhd = open( filed, O_RDWR ) ) == -1 ) 27 goto error_finish; 28 /* 複写先ファイルの先頭へ */ 29 if( lseek( fhd, 0, SEEK_SET ) != 0 ) 30 goto error_finish; 31 /* ヘッダー部読み込み */ 32 if( read( fhd, &head, sizeof(SYS_PAGE_HEAD)) != sizeof(SYS_PAGE_HEAD) ) 33 goto error_finish; 34 /* 変更部分に位置合わせ */ 35 if ( lseek( fhd, head.base_pos, SEEK_SET ) != head.base_pos ) 36 goto error_finish; 37 /* 変更部分の読み込み */ 38 if( read( fhd, &destbase, head.base_size )!= head.base_size ) 39 goto error_finish; 40 /* 変更 */ 41 strncpy( destbase.job_name, job, JOBNAME_LEN ); 42 strncpy( destbase.page_no, page, PAGENO_LEN ); 43 strncpy( destbase.comment, comment,COMMENT_LEN); 44 /* 変更部分に位置合わせ */ 45 if ( lseek( fhd, head.base_pos, SEEK_SET ) != head.base_pos ) 46 goto error_finish; 47 /* 変更部分の書き込み */ 48 if( write( fhd, &destbase,head.base_size) != head.base_size ) 49 goto error_finish; 50 51 if ( close( fhd ) == -1 ) 52 goto error_finish; 53 /* 正常終了 */ 54 return NORMAL; 55 56 error_finish: 57 unlink( filed ); 58 my_notice_prompt( master_menu_panel, "filecopy_check_error" ); 59 60 return ERROR; 61 } |
オリジナルプログラムでは、notice_promptがプログラム全体に400個ほど散在しています。だか ら、notice_promptを同値の関数に置き換えるのではなく、頑張って便利な関数を作ることには大 きな意味がありますが、my_notice_promotの詳細はMotifやXウィンドの詳細を理解する必要があ るのでリストは省略します。
リスト12−3には、my_notice_promptは3つしかありません。それらの第2引数は、
"filecopy_check" "filecopy_cp_error" "filecopy_check_error"になっています。readとwriteに対応してエラーメッセージを書く人が大勢いますが、それはデバッ グしている人には意味があるかも知れませんが、アプリケーション利用者にはどうでも良いことで、 自分のやろうとしたことが拒否された本質的理由が分かれば良いのです。以上の3つの第2引数は エラーメッセージそのものではなく、エラーメッセージを引き出す「鍵」です。Motifでは、
ColorSystem*filecopy_check: ファイルコピー/ファイル名エラーという感じに、: の右側に自由に文字列を書けば、その文字列が直接画面上の指定した適当な個所 に出るようにすることができます。
実際のエラーメッセージは、テキストファイルとして用意されるので、ユーザーにとって一番分 かりやすいメッセージを後でじっくり推敲してもらうことができます。
でも、このマニュアルに載っているサンプルプログラムを見ると笑ってしまいます。そのコメン トには、
/* このプログラムのエラーメッセージはすべて英語であるが、 * 真の国際化プログラムではソースコードに書き込むべきではない。 * ある種のデータベースからエラーメッセージを検索すべきである。 */と書かれています
そして、その後に、英語のエラーメッセージがfprintfの引数に書かれているのがゴロゴロ出て きます。まあ、コメントで一言断わっているのはよいのですが、「ある種の」というのは、どの種 のことなのか全然わかりません。このマニュアルの他の部分を見ても、具体的にどうすればよいか は書かれていません。
こういうのを、「分かる人にだけ分かる書き方」と言うのです。アホみたいに短い例でよいから、 そのような具体例が欲しいですね。せめて、「ある種」ではなく、たった一言、「リソースファイ ルなどのある種のデータベース」と言って欲しいものです。
実はこのプログラム、日本語のエラーメッセージなどがプログラム中にベタベタと書かれていた のですが、外国から引合いが来てしまい、英語化しなければならなくなったのです。
ウィンドシステムを使い、輸出もする可能性のある場合には、絶対にプログラム中に日本語文字 列を直接書くのは止めましょう。エラーメッセージだけでなく、ボタンなどあらゆる個所にメッセー ジがありますが、それらは全部プログラム中から消し、メッセージ用のデータファイルに移しましょ う。そして、プログラムに書くのは、それらのメッセージを引く「鍵」だけにするのです。
このシステムは、残念ながら、悲鳴がでるほど莫大な量の漢字文字列がプログラム中にばらまか れていました。国際化を叫んでいる人が多いのに、正しい国際化の実現方法を例示しているものが 少ないのは嘆かわしいことです。