諸事情で数日はネットワークをあまり使えないので、RISC-Vに関連する調査は中止。 あまりやることが無いので例によって一人読書会をしている。 今回は「リーダブルコード」。名著と呼ばれており、私も一度さっくりと読んだ。 ただし忘れていることもあるし、体系的に学びたいので読み直す。
例によってJupyter Notebookに書いてまとめをブログに張っていくことにする。
今回は第11章「第11章 一度に1つのことを」、第12章「コードに思いを込める」までをまとめた。
まとめるといっても、本書は言いたいことを非常に簡潔に要約しており、ほぼほぼそれを写経したようなものになっているが、まあ自分で手を動かさないと分からないということで。。。
リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)
- 作者: Dustin Boswell,Trevor Foucher,須藤功平,角征典
- 出版社/メーカー: オライリージャパン
- 発売日: 2012/06/23
- メディア: 単行本(ソフトカバー)
- 購入: 68人 クリック: 1,802回
- この商品を含むブログ (138件) を見る
第11章 一度に1つのことを
鍵となる考え: コードは1つずつにタスクを行うようにしなければいけない
一度に複数のタスクを行うコードを、一度に一つのタスクを行うように再構成する。
- コードが行っている「タスク」をすべて列挙する。この「タスク」という言葉は緩く使っている。「負ぶえジェクトが妥当かどうかを確認する」のような小さなこともあれば、「ツリーのすべてのノードをイテレートする」のようにあいまいなこともある。
- タスクをできるだけ異なる関数に分割する。少なくとも異なる領域に分割する。
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つのタスクを行っている。
- old_voteとnew_voteを数値に「パース」する。
- 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つのタスク」を適用する
location_info
ディクショナリから値を抽出する。- 「都市」の優先順位を調べる。何も見つからなかったら、デフォルトで「Middle-of-Nowhere」にする。
- 「国」を取得する。無ければ「Planet Earth」にする。
place
を更新する。は、そのままでいいだろう。
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つのサブタスクがある。
- 国が「USA」ならば、別の変数を使う。
- 先ほどのコードには「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」を使うことにする。
このようにすると、非常に汚いコードになる。
- キーのデフォルト値にい「unknown」を使う。
- HttpDownloadのメンバがあるかどうかを確認する。
- 値を抽出して文字列に変換する。
counts[]
を更新する。
タスクを別々の領域に分割する。
- 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章 コードに思いを込める
- コードの動作を簡単な言葉で同僚にもわかるように説明する。
- その説明の中で使っているキーワードやフレーズに注目する。
- その説明に合わせてコードを書く。
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 まとめ
説明することデコードがより自然なものになっていく。 説明で使っている単語やフレーズをよく見れば、分割する下位問題がどこにあるかがわかる。