リーダブルコード 3章〜4章
リーダブルコード輪読会 初回(後半)
レジュメ
- 3章 誤解されない名前
- 4章 美しさ
リーダブルコードの本の通りに進めていきます。
写経しているだけの部分が多いです。
3章 誤解されない名前
誤解される名前に気をつけろ
鍵となる考え
名前が「他の意味と間違えられることはないだろうか?」と何度も自問自答する
3.1 例: filter()
results = Database.all_objects.filter("year <= 2011")
このresulstsには何が含まれているか?
-
year <= 2011
のオブジェクト -
year <= 2011
ではないオブジェクト
どっちでしょうか?
どっちかわからないのはfilterが曖昧な言葉だから。
「選択する」ならselect()
, 「除外する」ならexclude()
のほうが良い
3.2 Clip(text, length)
def Clip(text, length):
...
Clip()の動作は2つが考えられる
- 最後からlength文字を削除する(remove)
- 最大length文字まで切り詰める(truncate)
仮に後者だとすればTruncate(text, length)
のほうが良い。
また、length
ではなくmax_length
のほうが明確(なんの長さなのかわからないので)
さらに
- バイト数
- 文字数
- 単語数
どの長さなのか名前に単位をつけるほうが良い
今回、文字数なのでmax_length
ではなくmax_chars
が良い。
3.3 限界値を含めるときにはminとmaxを使う
ショッピングカートに商品10点までしか入らない例を考えます
CART_TOO_BIG_LIMIT = 10
if shopping_cart.num_items() >= CART_TOO_BIG_LIMIT:
Error("多すぎ")
このコードにはoff-by-one
エラーがあります。(境界条件の判定に関するエラー)
>=
ではなく>
とするべき。
if shopping_cart.num_items() > CART_TOO_BIG_LIMIT:
プログラムの修正は簡単だが、ここでの問題はその修正ではなく、名前CART_TOO_BIG_LIMIT
がよくないという点。
アドバイス
限界値を明確にするには、名前の前にmax_やmin_をつけよう
MAX_ITEMS_IN_CART
という名前が適切
3.4 範囲をしていするときにはfirstとlastを使う
P.32の絵を確認してください
print integer_range(start=2, stop=4)
これが印字するのは[2,3]? [2,3,4]?
startというのは適切。stopは複数の解釈がある。
包含的な範囲を意味するのであればfirst,lastのほうが明確。
set.PrintKeys(first = "Bart", last="Maggie")
stopではなく、lastを使えば包含していることが明確に。
first,last以外にmin,maxを使うのも包含的範囲を表すことができる
3.5 包含・排他的範囲にはbeginとendを使う
P.33の絵を参照してください
PrintEventsInRange("OCT 16 12:00am", "OCT 17 12:00am")
終端に指定した値を含まずに指定する、といった要求の際にどのような名前を使うのが適切か
begin,endを使うことが多いが、endは少し曖昧。
「本の終盤(the end of the book)」といった使い方をした際にはendは包含的な意味になる。
しかし、これ以上ふさわしい単語は英語には存在しない。
begin,endが適切。
3.6 ブール値の名前
ブールの値の変数や関数の場合、trueとfalseの意味を明確にしないといけない。
以下はよくないパターン
bool read_password = true;
read
という単語が曖昧で、以下のどちらなのか不明瞭
- パスワードをこれから読み込む必要がある
- パスワードをすでに読みとっている
need_password
やuser_is_authenticated
を使ったほうが良い。
ブールの名前にはis
,has
,can
,should
をつけてわかりやすくするケースが多い。
たとえばSpaceLeft()
という名前は数値を返すように聞こえちゃうので、HasSpaceLeft()
のほうが明確。
否定をする名前を避ける
bool disable_ssl = false;
これは混乱するケース
bool use_ssl = true;
こっちのほうが読みやすい。
3.7 ユーザーの期待に合わせる
ユーザーが先入観を持っているために誤解を招いてしまうケースがある。
get*()
多くのプログラマはgetで始まるメソッドは値を返すだけの「軽量アクセサ」であるという規約に慣れ親しんでいる。
例えば以下のような例
public class StatisticsCollector {
pulblic void addSample(double x) { ... }
public double getMean() {
// すべてのサンプルをイテレートして、total / num_samplesを返す
}
}
getMean()は過去のデータをすべてイテレートしてその場で平均を計算する実装になっている。
データが大量にあったらすごいコスト。
そのことを知らないユーザはコストが高いと思わずにこのgetMean()を使ってしまうかも。
computeMean()
とかのほうが適切。
list::size()
C++標準ライブラリの例。
以下のコードには非常にわかりにくいバグがある
void ShrinkList(list<Node>& list, int max_size) {
while (list.size() > max_size) {
FreeNode(list.back());
list.pop_back();
}
}
バグの原因はlist.size()
の計算量が*O(n)であることを知らなかったから。
結果として計算量はO(n^2)*になっている。
「ドキュメントを読むべきだ」というのは簡単だけど、この場合はlist.size
が時間がかかりすぎることが問題。
最新のC++の標準ではsize()
の計算量を*O(1)*にすることが定められている。
3.8 複数の名前を検討する
名前を決めるときには複数の候補を検討すると思うが、以下の例はこの絵sんこう過程について示したもの。
ウェブサイトを使って「実験」をすることが多い(改善したかどうか、など)
以下の例は実験用の設定ファイルの例。
experiment_id: 100
description: "フォントサイズを14ptにあげる"
traffic_fraction: 5%
...
この設定ファイルには属性と値のペアが15個ほど定義されているとして。
同じような実験をするときには設定ファイルの大部分をコピペする必要がある。
experiment_id: 101
description: "フォントサイズを13ptにあげる"
[以下、experiment_id 100と同じ]
...
既存の設定ファイルを他の実験でも使えるようにしたい。
(これは「プロトタイプ継承」パターンと呼ばれる手法)
以下のようにかける
experiment_id: 101
the_other_experiment_id_I_want_to_reuse: 100
[以下、変更が必要な箇所だけ変更する]
基本的にはID100のものを使って、変更する部分だけを記載するという方針。
この場合、the_other_experiment_id_I_want_to_reuse
この名前をどのようにするか、を考える。
以下の4つのどの名前を使うかを検討したい。
- template
- reuse
- copy
- inherit
この機能を知らない人が見たときにどう思うかを考える。
1. templateの場合
experiment_id: 101
template: 100
templateの問題
- 「これはテンプレートだ」なのか「このテンプレートを使っている」なのかがわかりにくい
- テンプレートに使う実験のことを「本物」の実験ではない抽象的なものと考える人もいるかもしれない
ということで、templateはこのケースではNGな気がする
2. reuseの場合
experiment_id: 101
reuse: 100
reuseの問題
- この実験は100回再利用できる、と誤解される可能性もある
- 名前はreuse_idに変えたほうがいいだろう
- reuse_id: 100のことを「この実験の再利用idは100だ」と誤解するかもしれない
ということでこのケースでreuseはNG
3. copyの場合
experiment_id: 101
copy: 100
copyの問題
- この実験を100回コピーするのか、100回目のコピーなのかわからない。
- 他の実験を参照していることを表すなら
copy_experiment
という名前に帰ると良い
今までの中で一番いい気がする。
4. inheritの場合
experiment_id: 101
inherit: 100
inheritはプログラマになじみにがある。継承。
でも、他の実験から継承しているというのを明確にしておいたほうがいい。
inherit_from
,inherit_from_experiment_id
のほうがなお良いだろう。
以上のことから。
inherit_from
,inherit_from_experiment_id
というのがベストという結論に。
3章まとめ
- 最前の名前とは、誤解されない名前
- 英語の単語は
filter
,length
,limit
のように曖昧な単語があることに気をつける - 上下の限界値の名前はmax_やmin_を前につけるとよい
- 包含的範囲ならfirst,lastを使うといい
- 包含・排他的範囲であればbegin,endがイディオムなのでそれがベスト
- ブールの名前は
is
とかhas
とか。 -
disable_ssl
のような否定的な名前はさける - 単語に対するユーザーの期待にも注意する get(), size()などは軽量な動作を期待されがち
4章 美しさ
優れたソースコードは「目に優しい」ものでなければいけない。
本性ではコードを読みやすくするための余白、配置、順序について説明する。
具体的に3つの原則について
- 読み手が慣れているパターンと一貫性のあるレイアウトを使う
- 似ているコードは似ているように見せる
- 関連するコードをまとめてブロックにする
P.43の上段のソースコード。
とても読みにくい。
(さすがにこんなソースコードはないだろう・・・と思いますが)
P43の下段のソースコードは読みやすい。
汚いソースコードでググったら出てきました。
https://togetter.com/li/1341452
「分かりやすく美しいソースコードは業務の現場ではあまり求められません」
リーダブルコード的には美しいソースコードは常に求めるべきだ、ということでこの考えとは明らかに違いそうです。
4.2 一貫性のある簡潔な改行位置
Javaのソースコードを考える。
任意のネットワークに接続したときにプログラムがどのように動くかを評価するコード。
このコードではTcpConnectionSimulator
クラスを使っている。
TcpConnectionSimulator
クラスのコンストラクタには4つの仮引数がある。
- 接続速度(Kbps)
- 平均遅延時間(ms)
- 遅延「イライラ」時間(ms)
- パケットロス率(%)
また、このコード
TcpConnectionSimulator
のインスタンスが3つ必要。
public class PerformanceTester {
public static final TcpConnectionSimulator wifi = new TcpConnectionSimulator(
500, /* Kbps */
80, /* millisecs latency */
200, /* jitter */
1 /* packet loss % */);
public static final TcpConnectionSimulator t3_fiber =
new TcpConnectionSimulator(
4500, /* Kbps */
10, /* millisecs latency */
0, /* jitter */
0 /* packet loss % */);
public static final TcpConnectionSimulator cell = new TcpConnectionSimulator(
100, /* Kbps */
400, /* millisecs latency */
250, /* jitter */
5 /* packet loss % */);
}
横幅80文字にあわせるために(会社のコーディング標準)余計な改行が入っている。
t3_fiberの見た目が他と違っていて残念・・・
コードの「シルエット」が変なので自然とt3_fiber
に目が向いてしまう。
ということで、これを改善。
public class PerformanceTester {
public static final TcpConnectionSimulator wifi =
new TcpConnectionSimulator(
500, /* Kbps */
80, /* millisecs latency */
200, /* jitter */
1 /* packet loss % */);
public static final TcpConnectionSimulator t3_fiber =
new TcpConnectionSimulator(
4500, /* Kbps */
10, /* millisecs latency */
0, /* jitter */
0 /* packet loss % */);
public static final TcpConnectionSimulator cell =
new TcpConnectionSimulator(
100, /* Kbps */
400, /* millisecs latency */
250, /* jitter */
5 /* packet loss % */);
}
ソースコードに一貫性があり、楽に目を通すことができる。
でも、縦に長くてコメントが3つも繰り返されている。
さらに改善
public class PerformanceTester {
// TcpConnectionSimulator(throughput, latency, jitter, packet_loss)
// [kbps] [ms] [ms] [percent]
public static final TcpConnectionSimulator wifi =
new TcpConnectionSimulator(500, 80, 200, 1);
public static final TcpConnectionSimulator t3_fiber =
new TcpConnectionSimulator(45000, 10, 0, 0);
public static final TcpConnectionSimulator cell =
new TcpConnectionSimulator(100, 400, 250, 1);
}
コメントを上に集約して、仮引数を一行で書くように。
縦のラインもあわせた。
*でも、縦のラインを合わせるのってかなり難しくないですか?
エディタの自動整形で勝手にスペースとか消えちゃいますし(消えないようにも設定できるんでしょうけど)
みなさんどうしています?
4.3 メソッドを使った整列
人事データベースがあって、フルネームを取得するような関数がある例。
DatabaseConnection database_connection;
string error;
assert(ExpandFullName(database_connection, "Doug Adams", &error)
== "Mr. Douglas Adams");
assert(error == "");
assert(ExpandFullName(database_connection, "Jake Brown", &error)
== "Mr. Jacob Brown III");
assert(error == "");
assert(ExpandFullName(database_connection, "No Such Guy", &error) == "");
assert(error == "no match found");
assert(ExpandFullName(database_connection, "John", &error) == "");
assert(error == "more than one result");
見た目が美しくない。
長すぎて折り返されているところも。
改行の位置を変えなければいけないけど、assert(ExpandFullName....
とかerror
とか、文字列がなんども登場して邪魔。
ということで改善。
CheckFullName("Doug Adams", "Mr. Douglas Adams", "");
CheckFullName(" Jake Brown ", "Mr. Jacob Brown III", "");
CheckFullName("No Such Guy", "", "no match found");
CheckFullName("John", "", "more than one result");
引数の異なる4つのテストがあることがよくわかる。
CheckFullName
という関数を用意し、面倒な仕事はこの関数に詰め込む。
void CheckFullName(string partial_name,
string expected_full_name,
string expected_error) {
string error;
string full_name = ExpandFullName(database_connection, partial_name, &error);
assert(error == expected_error);
assert(full_name == expected_full_name);
}
見た目を美しくすることが目的であった。
しかし、別のうれしい副作用が。
- 重複を排除したことでコードが簡潔になった
- テストケースで大事な部分が見やすくなった。
- テストの追加が簡単になった。
4.4 縦の線をまっすぐにする
前節のCheckFullName()
引数は空白を使って整列できる
CheckFullName("Doug Adams" , "Mr. Douglas Adams" , "");
CheckFullName(" Jake Brown " , "Mr. Jacob Brown III" , "");
CheckFullName("No Such Guy" , "" , "no match found");
CheckFullName("John" , "" , "more than one result");
ここまでやると大変ですね・・・
繰り返しになりますが、このようなときに問題なのは、エディタによる自動整形かなと。
このような例ではタイプミスがあった際に気付きやすいといううれしさもあります。
P.48の例でもそれを述べています。
整列すべきなのか?
縦の線が「視覚的な手すり」いなれば、流し読みが楽にできるようになる。
整列なその維持に手間がかかるという理由で嫌いなプログラマーもいる。
でも、試しにやってみてはどうだろう
著者の経験ではプログラマが心配するほど手間にはならない。
4.5 一貫性と意味のある並び
details = request.POST.get('details')
location = request.POST.get('location')
phone = request.POST.get('phone')
email = request.POST.get('email')
url = request.POST.get('url')
このような形なら、ランダムに並べるのではなく、意味のある順にならべると良い。
例えば
- 対応するHTMLフォームの
<input>
のフィールドと同じ並び順にする - 「最重要」なものから並べる
- アルファベット順
4.6 宣言をブロックにまとめる
class FrontendServer {
public:
FrontendServer();
void ViewProfile(HttpRequest* request);
void OpenDatabase(string location, string user);
void SaveProfile(HttpRequest* request);
string ExtractQueryParam(HttpRequest* request, string param);
void ReplyOk(HttpRequest* request, string html);
void FindFriends(HttpRequest* request);
void ReplyNotFound(HttpRequest* request, string error);
void CloseDatabse(string location);
~FrontendServer();
}
特にひどいわけではないけど、もっとわかりやすく。
グループごとに並べる。
class FrontendServer {
public:
FrontendServer();
~FrontendServer();
// ハンドラ
void ViewProfile(HttpRequest* request);
void SaveProfile(HttpRequest* request);
void FindFriends(HttpRequest* request);
// リクエストとリプライのユーティリティ
string ExtractQueryParam(HttpRequest* request, string param);
void ReplyOk(HttpRequest* request, string html);
void ReplyNotFound(HttpRequest* request, string error);
// データベースのヘルパー
void OpenDatabase(string location, string user);
void CloseDatabse(string location);
}
これで概要が把握しやすく。
コードは増えたけど、ずっと読みやすい。
4.7 子度を「段落」に分割する
文章は段落に分割されている。
同様にプログラムも「段落」にわけるべき。
def suggest_new_friends (user, email_password):
friends = user.friends()
friend_emals = set(f.email for f in friends)
contacts = import_contacts(user.email, email_password);
contact_emals = set(c.email for c in contacts)
non_friend_emails = contact_emals - friend_emails
suggested_friends = USer.objects.select(email__in=non_friend_emails)
display['user'] = user
display['friends'] = friends
display['suggested_friends'] = suggested_friends
return render("suggested_friends.html", display)
このコードを読むことは難しい。
def suggest_new_friends (user, email_password):
# ユーザの友達のメールアドレスを取得する。
friends = user.friends()
friend_emals = set(f.email for f in friends)
# ユーザのメールアカウントからすべてのメールアドレスをインポートする。
contacts = import_contacts(user.email, email_password);
contact_emals = set(c.email for c in contacts)
# まだ友達になっていないユーザを探す。
non_friend_emails = contact_emals - friend_emails
suggested_friends = USer.objects.select(email__in=non_friend_emails)
# それをページに表示する
display['user'] = user
display['friends'] = friends
display['suggested_friends'] = suggested_friends
return render("suggested_friends.html", display)
改善バージョン。
段落ごとにコメントを追加。
コードを読むことの助けになる。
(コメントについては5章で詳細を)
4.8 個人的な好みと一貫性
最終的には個人の好みになってしまうこともある。
例えばクラス定義のかっこの位置。
class Logger {
...
}
なのか
class Logger
{
...
}
どっちでもいいけど、これを混ぜると非常に読みにくい。
「間違った」スタイルを使っているプロジェクトに携わる場合、
このような箇所はプロジェクトの規約に従うようにしたほうが良い。
鍵となる考え
一貫性のあるスタイルは「正しい」スタイルよりも大切だ。
4章まとめ
誰もが美しいコードが好き。
- 複数のコードブロックで同じようなことをしていたら、シルエットも同じようなものにする。
- コードの「列」を整理すれば概要が把握しやすくなる
- ある場所でA,B,Cの順で並んでいたものをB,C,Aのように並べてはいけない
意味のある順序を考える - から行を使って大きなブロックを論理的な「段落」にわける
感想
リーダブルコードを今回初めて読みましたが、気づかされることが多かったです。
特に名前の付け方については、もっと気を配るべきだったと反省しました。
プロジェクトの状況や採用しているエディタによっても少し変わる部分もあるのかなと思ったりもしました。