読者です 読者をやめる 読者になる 読者になる

新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。気づけば社会人3年目に突入。

学習記録:リーダブルコード

[学習記録] リーダブルコード

第Ⅱ部:ループとロジックの単純化

7章:制御フローを読みやすくする

  • 条件やループなどの制御フローはできるだけ「自然」にする
    • コードの読み手が立ち止まったり読み返したりしないように書く。
条件式の引数の並び順
  • 引数の並びの指針
    • 左側:調査対象の式。変化する。
    • 右側:比較対象の式。あまり変化しない。
    • 「もし君が18歳以上なら〜」と言うのは自然だが、「もし18年が君の年齢以下なら〜」は不自然。
// 自然な例
if (length >= 10)
while (bytes_received < bytes_expected)

// 不自然な例
if (10 <= length)
while (bytes_expected > bytes_received)
if/elseブロックの並び順
  • if/else文の並び順の指針
    • 条件は否定形よりも肯定形。if(!debug)よりif(debug)
    • 単純な条件を先に書く。
    • 関心を引く条件や目立つ条件を先に書く。
    • これらの優劣は衝突することもあるので、そのときは自分で判断する
// URLにクエリパラメータexpand_allが含まれているかどうか判定

// 「expand_allが入っていない時」と言われても、
// 見たときにはexpand_allが関心を引いてしまう
if (!url.HasQueryParameter("expand_all")) {
    response.Render(items);
    ...
} else {
    ...
}

// 関心を引く条件(更に肯定形なので)を先に処理する
if (url.HasQueryParameter("expand_all")) {
    ...
} else {
    response.Render(items);
    ...
}
  • 否定形であっても、単純で関心を引く場合もある
    • if not file : ログを出力、else…
三項演算子
  • 基本的にはif/elseを使おう
    • 三項演算子はそれによって簡潔になるときにだけ使う
    • 行数を短くするよりも、他人が理解するのにかかる時間を短くする。
// 12時以降ならPM
if (hour >= 12) {
    time_str += "pm";
} else {
    time_str += "am";
}

// この場合は三項演算子の方がわかりやすいし簡潔
time_str += (hour >= 12) ? "pm" : "am";
do/whileループを避ける
  • コードブロックを再実行する条件が下にあるため、コードを2回読む必要がある
    • whileループにすれば読みやすくなる。ただし、コードを重複させてまでdo/whileにする必要はない
    • 条件文の書き換えでwhile文に置き換えられることが多い
関数から早く返す
  • 関数から早くreturnすることはいいことである
    • 関数の先頭で引数チェックを行うような「ガード節」を使わずに実装すると不自然な実装になる
    • 確実にクリーンアップコードを実行したいときには、別の仕組み(デストラクタなど)を使う
悪名高きgoto
  • 最も単純で害のないgotoは、関数の最下部に置いたexitに飛ぶもの
    • これが唯一許されるgoto。それ以外は他の手段で置き換えられるべきもの
ネストを浅くする
  • ネストの深いコードは理解しにくい
    • 条件を常に覚えておかなくてはいけない
    • 下記の例では、更にSUCCESSとSUCCESSの否定が交互に登場する
if (user_result == SUCCESS) {
    if (permisson_result != SUCCESS) {
        ...
        return;
    }
    reply.WriteErrors("");
} else {
    reply.WriteErrors(user_result);
}
reply.Done();
  • どうしてこのようなコードが出来上がったのか?
    • もとのコードはとても単純で、それにコードを追加した結果
    • 最も簡単にコードを挿入でき、簡潔な変更であった
    • しかし、あとで誰かがコードを見た時には、こうした文脈は全て失われてしまう
    • 変更するときにはコードを新鮮な目で見る必要がある
// 元のコード
// user_resultを見てエラー文字列を決めてから、replyを完了するだけ
if (user_result == SUCCESS) {
    reply.WriteErrors("");
} else {
    reply.WriteErrors(user_result);
}
reply.Done();

// これに以下のコードが追加された
    if (permisson_result != SUCCESS) {
        ...
        return;
    }
  • 早めに返してネストを削除する
    • 失敗ケースを早めに関数から返せば良い
if (user_result != SUCCESS) {
    reply.WriteErrors(user_result);
    reply.Done();
    return;
}

if (permission.result != SUCCESS) {
    ...
    return;
}

reply.WriteErrors("");
reply.Done();
  • ループ内のネストを削除する
    • ループ内でreturnと同じようなことをするには、continueを使う
    • 多用するとわかりにくくなるので注意
実行の流れを追う
  • できることならプログラムの全ての実行パスを簡単に追えるようになるとよい
    • main()から出発して、心のなかでコードを追っていく
    • しかし、コードを舞台裏で実行する要素がある場合、コードを追うのが難しくなる
    • 後で理解しにくくなるので、コード全体に占める割合を大きくしないことが大切
構成要素 流れがわかりにくくなる理由
スレッド どのコードがいつ実行されるのかよくわからない
割り込みハンドラ 他のコードが実行される可能性がある
例外 関数呼び出しが終了しようとする
関数ポインタと無名関数 どのコードが実行されるのかわからない

8章:巨大な式を分割する

  • 巨大な式は飲み込みやすい大きさに分割する
説明変数
  • 式を簡単に分割するには、式を表す変数を使えば良い
# 何とrootを比較しているのかひと目で分からない
if line.split(':')[0].strip() == "root":

# 説明変数を使う
username = line.split(':')[0].strip()
if username == "root":
要約変数
  • 説明する必要がない場合でも、式を変数に代入しておくと便利
    • 大きなコードの塊を小さな名前に置き換える
// ユーザが文書の所有者である、という条件
// 理解するのに少し時間がかかる
if (request.user.id == document.owner_id) {
    // ユーザはこの文書を編集できる
}

// 要約変数を追加することでわかりやすくなる
final boolean user_owns_document = (request.user.id == document.owner_id);

if (user_owns_document) {
    ...
}
ドモルガンの法則を使う
  • notを分配しててand/orを反転する
    • この法則を使えば、論理式を読みやすくできる
// 「ファイルが存在し、かつ保護されていない」以外の場合
if (!(file_exists && !is_protected))

// 「ファイルが存在しないか、保護されている」場合
if (!file_exists || is_protected)
短絡評価の悪用
  • if (a || b)のaがtrueならば、bは評価されない
    • 便利だが、悪用すると複雑なロジックになる
    • 「頭のいい」コードがかけるが、後で他人が読んだ時わかりにくくなる
複雑なロジックと格闘する
  • Rangeクラスの実装
    • [begin, end)の範囲を示すクラス
    • いずれかの端が他のRangeと重なっているか確認するOverlapsWith()を実装したい
bool Range::OverlapsWith(Range other) {
    // 分かりにくい・・・
    return (begin >= other.begin && begin <= other.end) || ...
}
  • より優雅な手法を見つける
    • よりよい解決策には創造性が必要。例えば、反対から問題を解決してみる
    • 2つの範囲が重なる場合を考えるのではなく、重ならない場合を考えてみる
bool Range::OverlapsWith(Range other) {
    if (other.end <= begin) return false;
    if (other.begin >= end) return false;

    return true;
}
巨大な文を分割する
  • 同じ式を要約変数にして抽出
    • タイプミスを減らす、情報が縮まりコードが読みやすくなる、変更時に楽
  • マクロを使うことで、少しだけ違うが似たような式をまとめることが可能
    • 使いどころに注意

9章:変数と読みやすさ

  • 変数を適当に使うとプログラムが理解しにくくなる
    • 変数が多いと追跡が難しくなる
    • 変数のスコープが大きいと把握する時間が長くなる
    • 変数が頻繁に変更されると現在の値を把握するのが難しくなる
変数を削除する
  • 役に立たない一時変数は削除する
    • 以下の変数nowには意味がない
    • 複雑な式を分解していない、より明確になっていない、一度しか使っていない
now = datetime.datetime.now()
root_message.last_view_time = now
  • 中間結果を削除する
    • 中間結果を保持する変数を使うのではなく、結果をそのまま使う
// 中間結果を保持する変数を使用
int indexToRemove = -1;
for (int i = 0; i < array.length; i++) {
    if (array[i] == valueToRemove) {
        indexToRemove = i;
        break;
    }
}

if (indexToRemove >= 0) {
    array.splice(indexToRemove, 1);
}

// 結果をそのまま使った方が簡単
for (int i = 0; i < array.length; i++) {
    if (array[i] == valueToRemove) {
        array.splice(i, 1);
        return;
    }
}
  • 制御フロー変数を削除する
    • プログラムの実行を制御するためだけにあり、データを含んでいない
    • うまくプログラミングすれば、制御フロー変数は削除できる
    • ネストが深くてbreak出来ない場合は?ループ全体もしくは内部を関数に移動しよう
// 制御フロー変数doneを使用
while (/* 条件 */ && !done) {
    ...
    if (...) {
        // 単にbreakすれば良い
        done = true;
        continue;
    }
}
変数のスコープを縮める
  • 変数のことが見えるコード行数をできるだけ減らす
    • メンバ変数はクラスのなかで「ミニグローバル」になっていると言える
    • ローカル変数に格下げするか、メソッドをstaticにすることでメンバ変数と関係ないことを示せる
    • 大きなクラスを小さなクラスに分割することも有用な方法
  • 言語によるスコープ規則
    • C++:if文の条件式内で変数を宣言することでスコープをifブロックに限定
    • JavaScript:varを付けないとグローバルスコープになってしまう(for文のiでさえも!)
    • Python等:ブロックで定義された変数は関数全体にこぼれ出る
  • 定義の位置を下げる
    • もともとのC言語では、関数やブロックの先頭で変数を定義する必要があった
    • 使用する直前で定義することで理解の助けになる
変数は一度だけ書き込む
  • 変数が絶えず変更され続けると、追跡する難易度が格段に上がる
    • 変更されない変数は扱いやすい。例えば定数は多くのことを考える必要がない
    • 「変数を一度だけ書き込む」が無理だとしても、変更箇所はなるべく少なくすべき