読みやすいソースコードの書き方

こんにちは。プログラマーの尾関です。
今回は、読みやすいプログラムのコードを書く方法を紹介します。
(新人向けにまとめたものなので、基礎的な内容となります)

■ソースコードが読みにくいと起こる弊害

読みにくいコードを書くと、様々な問題が発生します。

  • 修正コストが上がるため、ソースコードの修正がしにくくなる
    • エンバグが出やすくなる
    • 機能追加するコストが高くなる
  • ソースコードを書いた本人以外がコードを読むことができなくなる
    • 他の人がバグを修正したり、機能を追加することができない
    • 引き継ぎができない
  • ソースコードを見るだけでげんなりしてしまう
    • やる気が出なくなる

逆に、読みやすいコードを書けば、「修正が簡単」で「機能追加も楽」で、「他の人が修正できるので引き継ぎが楽」になります。
ということで、読みやすいコードを書けるようになりましょう! というのがこの記事の趣旨となります。

■読みやすいコードを書くためのポイント

今回紹介するのは、ちょっとした工夫で読みやすくするための方法です。

  1. 論理演算子の数を減らす
  2. ブロックネストの数を減らす
  3. 変数のスコープを減らす

1. 論理演算子の数を減らす

1つの条件分岐にたくさんの「&&」や「||」や「!」といった論理演算子がたくさん含まれると読みにくくなります。

if(A && B) { // これくらいだったらまだ読める
}

↓

if(A && B || C) { // これは一瞬手が止まって考えてしまう
}

if(!A && !B) { // これも考え込んでしまう
}

このような論理演算子が多い式を、シンプルにする方法を考えてみます。

▼例

例えば、「HPが30%未満」かつ「SPゲージが80より大きい」場合に必殺技を発動できるとします。

// 必殺技が発動できるかどうかをチェック
if((1.0f * hp / hpmax) < 0.3f && sp > 80)) {
  // 必殺技発動
}

そのまま書くとこのようなプログラムになると思います。ただ、このコードを即座に理解するのはかなり難しいと思います。また、このコードに新たな条件(例えば特定アイテムを保持している必殺技が使えるなど)を追加したときに、式がどんどん横に伸びていってしまいます。

この問題に対する最初のアプローチは「式の関数化」です。

void doSomething() {
  // ...

  // 必殺技が発動できるかどうかをチェック
  if(isSpecialSkill()) {
    // 必殺技発動
  }

  // ...
}

/**
 * 必殺技が発動できるかどうかを判定する関数
 */
bool isSpecialSkill() {
  return ((1.0f * hp / hpmax) < 0.3f && sp > 80);
}

必殺技発動判定の式を「isSpecialSkill」関数としました。これにより、この関数の呼び出しは「必殺技が発動するかどうかを判定している」ということが一目で分かります。そして、この関数が正常に動いていれば内部の動作を気にする必要がなく、異常な動作をしたときだけこの関数を内部処理を見るようにすればよくなります。
ただ、これだけでは判定式がそのまま関数になっただけです。判定式を「ドモルガンの法則」を使って分離を行います。

★ドモルガンの法則とは

命題A:このボールは青い
命題B:このボールは赤い

この場合、「このボールは青くない、かつ赤くもない(!A && !B) 」という集合は、
「このボールは青い、または赤い、ということはない !(A || B)」
という集合に置き換えが可能

→例えば「黄色いボール」は「青くない、かつ赤くもない」そして「青い、または赤い、ということはない」

つまり、真偽を反転し、論理和(&&)と論理積(||)を交換すると、同一の集合になるとした法則

ドモルガンの法則を使うと、「HPが30%未満 かつ SPゲージが80より大きい」という条件は「HPが30以上なら発動できない または SPゲージが80以下なら発動しない」という条件に置き換えることができます。

/**
 * 必殺技が発動できるかどうかを判定する関数
 */
bool isSpecialSkill() {
  if((1.0f * hp / hpmax) >= 0.3f) { return false; } // HPが30%以上なら発動しない
  if(sp <= 80) { return false; } // SPが80以下なら発動しない
  if(hasSpecialItem() == false) { return false; } // 特定アイテムを持っていないと使えない

  return true; // 発動する
}

これにより、条件式が分解され1行ごとにシンプルな条件が定義されるようになりました。コードが読みやすくなったのも重要ですが、新しい仕様を追加するのが容易になったというのも重要です。

/**
 * 必殺技が発動できるかどうかを判定する関数
 */
bool isSpecialSkill() {
  if(hasSpecialItem()) { return true; } // 特定アイテムを持っていれば無条件で使える
  if((1.0f * hp / hpmax) >= 0.3f) { return false; } // HPが30%以上なら発動しない
  if(sp <= 80) { return false; } // SPが80以下なら発動しない

  return true; // 発動する
}

上記コードは「特定アイテムがないと必殺技が使えない」という仕様を追加したコードとなります。

2. ブロックネストの数を減らす

ブロックネストとは、if文やfor文などのブロックを伴う制御構文によって、{}の階層を持つことです。

if(...) {
  for(...) {
    for(...) {
      if(...) {
      }
      else(...) {
        if((...) {
          switch(...) {
          }
        }
      }
    }
  }
}

このようにネストが深くなればなるほど、コードは入り組んで読みにくくなります。
ネストを減らすアプローチとしては「ガード節を使う」「制御構造をデータにする」です。

▼ガード節を使う

ネストを少なくする一番シンプルな方法は、一番外側のif文などを反転させ、break / continue / return などの制御文を入れることです。

for(int i = 0; i < OBJ_MAX; i++) {
  if(objs[i].Exists()) {
    DoSomething1();
    if(...) {
      DoSomething2();
    }
    DoSomething3();
  }
}

ここでは continue を使ってネストの数を1つ減らしてみます。

for(int i = 0; i < OBJ_MAX; i++) {
  if(objs[i].Exists() == false) { continue; } // ガード節
  DoSomething1();
  if(...) {
    DoSomething2();
  }
  DoSomething3();
}

ガード節を入れることのメリットとしてはネストが減る、ということ以外にも、ifブロックの終端を気にする必要がなくなるということがあります。

▼制御構造をデータ構造に変換する

例えば、switch文は関数テーブルや、Strategyパターンで置き換えが可能です。

switch(ix->Step) {
  case eStep_Init: _Init(ix); break;
  case eStep_Load: _Load(ix); break;
  case eStep_Main: _Main(ix); break;
  case eStep_Release: _Release(ix); break;
}

このようにswitch文は、関数ポインタのテーブルを作ることで除去でき、結果としてネストを減らすことができます。

3. 変数のスコープを短く保つ

変数のスコープとは、あるところで宣言された変数名が有効な範囲のことです。

  • 大域スコープ(グローバル変数) / ファイルスコープ / 局所スコープ(ローカル変数)
  • インスタンススコープ(インスタンスのメンバ変数) / クラススコープ(クラス変数)

スコープを短くすることで、その変数の役割が明確になり、サブルーチンへの切り出しなどが容易となります。
短くするアプローチとしては、「グローバル変数(static変数)をファイルスコープにする」「メンバ変数を関数内のローカル変数にする」「関数内変数を、ブロック内に閉じ込める」というものが考えられます。

void XXX(){
  int a,b,c;
  int x,y,z;
 
  :
 
  int d,e,f;
 
  :
  :
 
}

この関数では、変数が関数の様々なところで定義されています。このような記述をされていると、例えば変数「x」「y」「z」がどこで使われているかを確認するために関数の最後までチェックする手間が発生してしまいます。

void XXX(){
  int a,b,c;

  {
    int x,y,z;
    // x,y,zの寿命はこのブロック内
    :
  }
 
  {
    int d,e,f = ...,...,...;
    // d,e,fの寿命はこのブロック内 
    :
    :
  }
}

そこで関数内でのローカル変数の寿命を限定的にしました。理想としてはこれらのブロックを関数化した方が良いですが、ひとまずはこのアプローチをすることで見通しの良いコードとなります。

最後に

最後に、これまでの具体的なテクニックではなく、読みやすくするコードを書くための考え方を紹介します。

第一に、「楽にコーディングできる方法を考える」ことです。ただこれは楽をすれば良いというわけではありません。方針としては「短期的に楽をするのではなく、長期的に楽できる方法を考える」ことです。短期的に楽をするためグローバル変数やpublic変数を多用すると、バグの温床となり後で痛い目をみます。面倒でもprivateなどで隠蔽した方が長期的には楽できます。

第二に、「スコープが広い場合は長い変数名をつけて、スコープが狭い変数は短い名前にする」ことです。例えば、弊社ランカースが「東京都新宿区」にあることを説明する場合、都内の人には「新宿区」でたいてい通じます。ですが他の県に住んでいる人には「東京都新宿区」と説明する必要があるかもしれません。さらに外国の人に説明するには「日本国東京都新宿区」となります。つまりスコープ(範囲)が広がるほど長い名前が必要となり、狭いほど短い名前で済むのです。

第三に、「他人のソースコードをたくさん読む」ことです。良いコードだけではなく悪いコードも読みます。良いコードは新たな視点と指針を与え、悪いコードは教訓を得ることができます。

最後に、「様々なプログラム言語を使ってみることで技法の幅を広げる」ことです。C言語のような静的プログラム言語しか知らないのであれば、PythonやRubyのようなスクリプト言語やhaskellなどの関数型プログラミング言語は、プログラムに対する新しい視点を得ることができます。

参考

新人プログラマに知ってもらいたいメソッドを読みやすく維持するいくつかの原則
リファクタリング―プログラムの体質改善テクニック

Follow me!