FPGA開発日記

カテゴリ別記事インデックス https://msyksphinz.github.io/github_pages , English Version https://fpgadevdiary.hatenadiary.com/

「リーダブルコード」を読む (第11章「一度に1つのことを」、第12章「コードに思いを込める」)

諸事情で数日はネットワークをあまり使えないので、RISC-Vに関連する調査は中止。 あまりやることが無いので例によって一人読書会をしている。 今回は「リーダブルコード」。名著と呼ばれており、私も一度さっくりと読んだ。 ただし忘れていることもあるし、体系的に学びたいので読み直す。

例によってJupyter Notebookに書いてまとめをブログに張っていくことにする。

今回は第11章「第11章 一度に1つのことを」、第12章「コードに思いを込める」までをまとめた。

まとめるといっても、本書は言いたいことを非常に簡潔に要約しており、ほぼほぼそれを写経したようなものになっているが、まあ自分で手を動かさないと分からないということで。。。

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)


第11章 一度に1つのことを

鍵となる考え: コードは1つずつにタスクを行うようにしなければいけない

一度に複数のタスクを行うコードを、一度に一つのタスクを行うように再構成する。

  1. コードが行っている「タスク」をすべて列挙する。この「タスク」という言葉は緩く使っている。「負ぶえジェクトが妥当かどうかを確認する」のような小さなこともあれば、「ツリーのすべてのノードをイテレートする」のようにあいまいなこともある。
  2. タスクをできるだけ異なる関数に分割する。少なくとも異なる領域に分割する。

11.1 タスクは小さくできる。

ブログに設置する投票用のウィジットがあるとする。ユーザは「アップ(賛成)」と「ダウン(反対)」を投票できる。

var vote_changed = function (old_vote, new_vote) {
    var score = get_score();
    
    if (new_vate !== old_vote) {
        if (new_vote === 'Up') {
            score += (old_vote === 'Down' ? 2 : 1);
        } else if (new_vote === 'Down') {
            score -= (old_vote === 'Up' ? 2 : 1);
        } else if (new_vote === '') {
            score += (old_vote === 'Up' ? -1 : 1);
        }
    }
    set_score (score);
}

このタスクは一度に2つのタスクを行っている。

  1. old_voteとnew_voteを数値に「パース」する。
  2. scoreを更新する。

以下のコードは、最初のタスク(投票を数値にパースする)を解決したものである。

var vote_value = function (vote) {
    if (vote === 'Up') {
        return +1;
    }
    if (vote === 'Down') {
        return -1; 
    }
    return 0;
}

残りのコードは、2つ目のタスク(スコアの更新)を解決している。

var vote_changed = function (old_vote, new_vote) {
    var score = get_score ();
    score -= vote_value(old_vote);
    score += vote_value(new_vote);
}

11.2 オブジェクトから値を抽出する

「一度に1つのタスク」を適用する

  1. location_infoディクショナリから値を抽出する。
  2. 「都市」の優先順位を調べる。何も見つからなかったら、デフォルトで「Middle-of-Nowhere」にする。
  3. 「国」を取得する。無ければ「Planet Earth」にする。
  4. placeを更新する。

  5. は、そのままでいいだろう。

var town    = location_info["LocalityName"];
var city    = location_info["SubAdministrativeAreaName"];
var state   = location_info["AdministrativeAreaName"];
var country = location_info["CountryName"];

次に、残りの値の「後半」を見つけなければならない。

// 先にデフォルト値を設定して、値が見つからなかったら書き換える。
var second_half = "Planet Earth";
if (country) {
    second_half = country;
}
if (state && country = "USA") {
    second_half = state;
}

同じようにして「前半」を見つけることができる。

var first_half = "Middle-of-Nowhere";
if (state && country !== "USA") {
    first_half = state;
}
if (city) {
    first_half= city;
}
if (town) {
    first_half = town;
}

最後に情報をつなぎ合わせる。

return first_half + ", " + second_half;

その他の手法

タスクを分割すると、コードのことを考えやすくなる。 例えば、if文が連続していると、すべての場合分けを注意深く読まなければいけなくなる。 このコードには、2つのサブタスクがある。

  1. 国が「USA」ならば、別の変数を使う。
  2. 先ほどのコードには「if USA」ロジックがほかのロジックと混ざっていた。「USA」と「非USA」は別々に処理できる。
var first_half, second_half;
if (country === "USA") {
    first_half = town || city || "Middle-of-Nowhere";
    second_half = state || "USA";
} else {
    first_half = town || city || state || "Middle-of-Nowhere";
    second_half = country || "Planet Earth";
}
return first_half + ", " + second_half;

11.3 もっと大きな例

ウェブクローリングシステムにおいて、UpdateCounts()はさまざまな統計値を更新する関数である。ウェブページをダウンロードした後に毎回呼ばれることになっている。

void UpdateCounts(HttpDownload hd) {
    counts["Exit State"   ][hd.exit_state()]++;
    counts["Http Response"][hd.http_response()]++;
    counts["Content-Type" ][hd.content_type()]++;
}

実際には上記のようなメソッドは無いので、ネストしたクラスを自分で探す必要がある。 また、値がどこかに消えてしまっていることもあり、そのような場合はデフォルト値に「Unkwnon」を使うことにする。

このようにすると、非常に汚いコードになる。

  1. キーのデフォルト値にい「unknown」を使う。
  2. HttpDownloadのメンバがあるかどうかを確認する。
  3. 値を抽出して文字列に変換する。
  4. counts[]を更新する。

タスクを別々の領域に分割する。

  1. 3つのキーのデフォルト値を定義する。
  2. 可能であれば、キーの値を抽出して文字列に変換する。
  3. キーのcounts[]を更新する。

さらなる改善

ヘルパー関数を導入して、より見やすいものとする。

string ExitState(HttpDownload hd) {
    if (hd.has_event_log() && hd.event_long().has_exit_state()) {
        return ExitStateTypeName(hd.event_log().exit_state());
    } else {
        return "unkwnown";
    }
}

11.4 まとめ

本章では、コードを構成する簡単な技法として「一度に1つのタスクを行う」を主お買いした。

読みにくいコードがあれば、そこで行われているタスクのすべてを列挙し、必要ならば分割していくことが求められる。

第12章 コードに思いを込める

  1. コードの動作を簡単な言葉で同僚にもわかるように説明する。
  2. その説明の中で使っているキーワードやフレーズに注目する。
  3. その説明に合わせてコードを書く。

12.1 ロジックを明確に説明する

ユーザにページを閲覧する権限があるかどうかを確認して、もし権限がなければ、権限がないことをユーザに知らせるページに戻す。

$is_admin = is_admin_request();
if ($document) {
    if (!$is_admin && ($document['username'] != $_SESSION['username'])) {
        return not_authrized();
    }
} else {
   if ($is_admin) {
       return not_authrized();
   }
}

権限があるのは、以下の2つ。 1. 管理者 2. 文書の所有者(文書がある場合) 従って、以下のようになる。

if (is_admin_requset()) {
    // 権限あり
} elseif ($documnet && ($documnet['username'] == $_SESSION['username'])) {
    // 権限あり
} else {
    return not_authorized();
}

if文の中身が2つも空になっている。しかし、ロジックも単純になったし、否定形もなくなった。こちらの方が理解しやすい。

12.2 ライブラリを知る

「ヒント」を表示するウェブサイトを持っているものとする。

<div id="tip-1" class="tip">ヒント: 過去のクエリを見るにはログインしてください。</div>
<div id="tip-2" class="tip">ヒント: 拡大するには画像をクリックしてください。</div>
...

「その他のヒントを見る!」をクリックすると、次のヒントの番になる。jOueryというJavaScriptライブラリで実装したコードは以下のようになる。

var show_next_tip = function() {
    var num_tips = $('.tip').size();
    var show_tip = $(.tip:visible');
    
    var shown_tip_num = Number(shown_tip.attr('id').slice(4));
    if (shown_tip_num === num_tips) {
        $('#tip-1').show();
    } else {
        $('#tip-' + (shown_tip_num + 1)).show();
    }
    shown_tip.hide();
}

もっとよくできる。

今見えているヒントを見つけて隠す。
次のヒントを見つけて表示する。
ヒントがなくなったら、最初のヒントに戻る。

この説明をもとに作った解決策がこちらだ。

var show_next_tip = function() {
    var cur_tip = $('.tip:visible.').hide();
    var next_tip = cur_tip.next('.tip');
    if (next_tip.size() === 0) {
        next_tip = $('.tip:first');
    }
    next_tip.show();
}

12.3 この手法を大きな問題に適用する

株式の購入を記録するシステムがあり、time(正確な購入日時)、ticker_symbol(銘柄、例:GOOG)、price(価格、例:$600), number_of_shares (株式数、例: 100)を管理する。この情報は3つのテーブルに散らばっている。

3つのテーブルを結合するプログラムを書かなければならない。それぞれのテーブルはtimeでインデックスを取られている。

Pythonで最初に書いたコードは、一致しない行をスキップするのに多くの行を費やしてしまう。 行を抜かしていないか、イテレータを読み込み過ぎていないだろうか?などの問題を抱えている可能性がある。

解決策を言葉で説明する

3つの行のイテレータを一度に読み込む。
行のtimeが一致していなければ、一致する行まで進める。
一致した行を印字して、行を進める。
一致する行がなくなるまでこれを繰り返す。

一致する行まで進めるという機能を新しい関数AdvanceToMatchingTime()に抽出する。

手法を再帰的に適用する

AdvanceToMatchingTime()を実装する場合、 1. 現在の行のtimeを見る。一致していなければ終了する。 2. 一致していなければ「遅れている行」を進める。 3. 行が一致するまで(あるいはイテレーションのいずれかが終了するまで)これを繰り返す。

12.4 まとめ

説明することデコードがより自然なものになっていく。 説明で使っている単語やフレーズをよく見れば、分割する下位問題がどこにあるかがわかる。