415
0
0

リーダブルコード 3章〜4章

Published at July 21, 2019 5:45 p.m.

リーダブルコード輪読会 初回(後半)

レジュメ

  • 3章 誤解されない名前
  • 4章 美しさ

リーダブルコードの本の通りに進めていきます。
写経しているだけの部分が多いです。

3章 誤解されない名前

誤解される名前に気をつけろ

鍵となる考え
名前が「他の意味と間違えられることはないだろうか?」と何度も自問自答する

3.1 例: filter()

python
results = Database.all_objects.filter("year <= 2011")

このresulstsには何が含まれているか?

  • year <= 2011のオブジェクト
  • year <= 2011ではないオブジェクト

どっちでしょうか?
どっちかわからないのはfilterが曖昧な言葉だから。
「選択する」ならselect(), 「除外する」ならexclude()のほうが良い

3.2 Clip(text, length)

python
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点までしか入らない例を考えます

python
CART_TOO_BIG_LIMIT = 10

if shopping_cart.num_items() >= CART_TOO_BIG_LIMIT:
    Error("多すぎ")

このコードにはoff-by-oneエラーがあります。(境界条件の判定に関するエラー)
>=ではなく>とするべき。

python
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の絵を参照してください

python
PrintEventsInRange("OCT 16 12:00am", "OCT 17 12:00am")

終端に指定した値を含まずに指定する、といった要求の際にどのような名前を使うのが適切か
begin,endを使うことが多いが、endは少し曖昧。
「本の終盤(the end of the book)」といった使い方をした際にはendは包含的な意味になる。

しかし、これ以上ふさわしい単語は英語には存在しない。
begin,endが適切。

3.6 ブール値の名前

ブールの値の変数や関数の場合、trueとfalseの意味を明確にしないといけない。

以下はよくないパターン

javascript
bool read_password = true;

readという単語が曖昧で、以下のどちらなのか不明瞭

  • パスワードをこれから読み込む必要がある
  • パスワードをすでに読みとっている

need_passworduser_is_authenticatedを使ったほうが良い。
ブールの名前にはis,has,can,shouldをつけてわかりやすくするケースが多い。

たとえばSpaceLeft()という名前は数値を返すように聞こえちゃうので、HasSpaceLeft()のほうが明確。

否定をする名前を避ける
bool disable_ssl = false;
これは混乱するケース

bool use_ssl = true;
こっちのほうが読みやすい。

3.7 ユーザーの期待に合わせる

ユーザーが先入観を持っているために誤解を招いてしまうケースがある。

get*()

多くのプログラマはgetで始まるメソッドは値を返すだけの「軽量アクセサ」であるという規約に慣れ親しんでいる。

例えば以下のような例

java
public class StatisticsCollector {
    pulblic void addSample(double x) { ... }

    public double getMean() {
        // すべてのサンプルをイテレートして、total / num_samplesを返す
    }
}

getMean()は過去のデータをすべてイテレートしてその場で平均を計算する実装になっている。
データが大量にあったらすごいコスト。
そのことを知らないユーザはコストが高いと思わずにこのgetMean()を使ってしまうかも。
computeMean()とかのほうが適切。

list::size()

C++標準ライブラリの例。

以下のコードには非常にわかりにくいバグがある

cpp
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つのどの名前を使うかを検討したい。

  1. template
  2. reuse
  3. copy
  4. 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つの仮引数がある。

  1. 接続速度(Kbps)
  2. 平均遅延時間(ms)
  3. 遅延「イライラ」時間(ms)
  4. パケットロス率(%)

また、このコード
TcpConnectionSimulator
のインスタンスが3つ必要。

java
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に目が向いてしまう。

ということで、これを改善。

java
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つも繰り返されている。

さらに改善

java
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 メソッドを使った整列

人事データベースがあって、フルネームを取得するような関数がある例。

cpp
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とか、文字列がなんども登場して邪魔。

ということで改善。

cpp
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という関数を用意し、面倒な仕事はこの関数に詰め込む。

cpp
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()引数は空白を使って整列できる

cpp
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 一貫性と意味のある並び

python
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 宣言をブロックにまとめる

cpp
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();
}

特にひどいわけではないけど、もっとわかりやすく。
グループごとに並べる。

cpp
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 子度を「段落」に分割する

文章は段落に分割されている。
同様にプログラムも「段落」にわけるべき。

python
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)

このコードを読むことは難しい。

python
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 個人的な好みと一貫性

最終的には個人の好みになってしまうこともある。
例えばクラス定義のかっこの位置。

cpp
class Logger {
    ...
}

なのか

cpp
class Logger
{
    ...
}

どっちでもいいけど、これを混ぜると非常に読みにくい。

「間違った」スタイルを使っているプロジェクトに携わる場合、
このような箇所はプロジェクトの規約に従うようにしたほうが良い。

鍵となる考え
一貫性のあるスタイルは「正しい」スタイルよりも大切だ。

4章まとめ

誰もが美しいコードが好き。

  • 複数のコードブロックで同じようなことをしていたら、シルエットも同じようなものにする。
  • コードの「列」を整理すれば概要が把握しやすくなる
  • ある場所でA,B,Cの順で並んでいたものをB,C,Aのように並べてはいけない
    意味のある順序を考える
  • から行を使って大きなブロックを論理的な「段落」にわける

感想

リーダブルコードを今回初めて読みましたが、気づかされることが多かったです。
特に名前の付け方については、もっと気を配るべきだったと反省しました。
プロジェクトの状況や採用しているエディタによっても少し変わる部分もあるのかなと思ったりもしました。

ありがとうございました。