プログラムには流れがある

ユキ先輩。プログラムを分かりやすく書くために、気を付けると良いことってありますか?
そうねぇ……プログラムの流れを意識することかしら。
プログラムの流れ、ですか?詳しく教えてください!
じゃあ、いくつか例を挙げてみるわね。

まず、プログラム中の処理には、「正常時の処理」と「エラー(異常時の)処理」がありますね。

ソースコード上では、正常時の処理が中心にくるような書き方をするのがおすすめです。なぜなら、エラー処理は「だいたいの場合は発生しない状況」を扱う処理だからです。

正常時の処理を中心に据えておけば、そのプログラムをほかの人が見るときや、あとで自分が見たときに理解しやすいでしょう。

正常時の処理が中心にくるように、ですね。……けど、あまりイメージがわかないです。
そうね。それじゃぁ、次の「悪い例」を見てみましょう。
/* 指定されたファイルにテキストを書き込む。
 * - pText: テキスト
 * - pPathToFile: ファイルのパス
 * 正常に書き込めたらtrue、エラーが発生したらfalseを返す。
 */
bool WriteTextToFile(const char *pText, const char *pPathToFile) {
  assert(pText);
  assert(pPathToFile);

  FILE *fp = fopen(pPathToFile, "w");
  if (fp == 0) {
    return false; /* エラー:ファイルを開けなかった */
  }

  if (fputs(pText, fp) != EOF) {
    fclose(fp);
    return true; /* 正常 */
  }

  fclose(fp);
  return false; /* エラー:テキストを書き込めなかった */
}

この関数は、ファイルに文字列を書き込み、うまくいったらtrueを返すというものです。でも、正常時の流れがどこにあるのかが見つけにくくなってしまっています。

たしかに。なんか、入り組んでいる感じがしますね。
問題点は、いくつか考えられるわね。例えば……
  • エラーの判定に一貫性がありません。fopen()では失敗したかどうかを判定しているのに対し、fputs()では成功したかどうかを判定しています。
  • すべての処理に成功したとき、関数の最後までたどり着くことなくreturnしています。
  • fopen()は1カ所しかないのに、fclose()は2カ所で呼び出されています。
なるほど〜。
だけど、最大の問題は、書き方に「ポリシー」がないことよ!
書き方の「ポリシー」ですか!?

流れが分かりやすい書き方

プログラムの流れが分かりやすくなる、おすすめのポリシーを紹介するわね。

  • 正常でもエラーでも、関数の最後まで処理を到達させる。
  • if文では処理が成功したかどうかを判定し、エラー処理が必要な場合はelseに収める。

このポリシーにしたがうと、上の関数は次のようになります。

bool WriteTextToFile(const char *pText, const char *pPathToFile) {
  assert(pText);
  assert(pPathToFile);

  bool bSuccess = false;

  FILE *fp = fopen(pPathToFile, "w");
  if (fp) {
    if (fputs(pText, fp) != EOF) {
      bSuccess = true;
    }

    fclose(fp);
  }

  return bSuccess;
}

どうでしょう。正常時の流れがよく見えるようになったのではないでしょうか。正常時の流れが見えれば、その関数がどのようなアルゴリズムで書かれているのかが明確になります。

ほんとだ。処理の流れが分かりやすくなりましたね!
それに、複雑さが軽減されて、改造が必要なときも手を付けやすいソースコードになったんじゃないかしら。

上の例でも、ごく自然な流れで、fopen()fclose()の呼び出しが1度ずつになりました。さらに、エラーが発生したとき、実は「falseを返す」以外に何もしていなかったということが明らかになりました。

ポリシーにしたがって書くようにすると、いろいろなメリットがあるんですね。
そうね。でも、これですべてOKってことでもないのよ。

この書き方は、必ずしも万能というわけではありません。判定すべきことが増えてくると、ifのネストが深くなってしまうのです。処理をサブルーチンに分ければネストの問題は解決できますが、ほかの書き方をしたくなるケースも出てくるでしょう。

毎回同じポリシーにするってわけにはいかないんですね。

もう一つの書き方

もう一つ、別のポリシーを紹介するわね。

  • 正常時は、関数の最後まで処理を到達させる。
  • エラーを検出したら、即座にreturnする。

このポリシーにしたがうと、先ほどの関数は次のように書けます。

bool WriteTextToFile(const char *pText, const char *pPathToFile) {
  assert(pText);
  assert(pPathToFile);

  FILE *fp = fopen(pPathToFile, "w");
  if (fp == 0) {
    return false;
  }

  if (fputs(pText, fp) == EOF) {
    fclose(fp);
    return false;
  }

  fclose(fp);

  return true;
}

エラーケースが多数考えられる関数は、こちらのポリシーで書いたほうがシンプルになることがよくあります。そもそも、if文のネストが深くならないのでこちらのほうが好き、という人もいるかもしれません。

ただし、プログラムの「構造化」という面では、returnを何度も書くのは好ましいとはいえません。この例のfclose()ように、同じ処理を2回以上書かなければならない場面も出てきます。

それでも、一定のポリシーにしたがって書かれたプログラムは読みやすくなるものよ。
書き方に「ポリシー」がないと、プログラムが必要以上に複雑になっちゃうってことですね!
そういうこと。

基本的には、1つ目に紹介したポリシーで書くようにするのがおすすめです。それで、ちょっとやりづらいなと感じたときは、2つ目のポリシーを試してみてください。

ここがポイント!
プログラムを書くときは、処理の流れを意識しよう。一定のポリシーにしたがって書けば、読みやすくなります。