FPGA開発日記

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

「リーダブルコード」を読む (第3章「理解しやすいコード」第4章「美しさ」)

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

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

今回は第3章「理解しやすいコード」第4章「美しさ」までをまとめた。

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


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

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

第3章 誤解されやすい名前

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

3.1 例: filter()

filter()という言葉では、選択するのか、除外するのかがわからない。

resultls = Database.all_objects.filter("year <= 2011");
  • 選択するのであれば、 select() にした方が良い。
  • 除外するのであれば、 exclude() にした方が良い。

3.2 例: Clip(text, length)

# textの最後を切り落として、「...」をつける
def Clip(text, length):
    ...

Clip()の動作は2つ考えられる。

  • 最後から length文字を削除する (remove)
  • 最大length文字までを切り詰める (truncate)

3.3 限界地を含めるときはminとmaxを使う

ショッピングカートには商品が10点までしか入らないとする。

CART_TOO_BIG_LIMIT = 10

if shopping_cart.num_items() >= CART_TOO_BIG_LIMIT:
    Error("カートにある商品数が多すぎます。");

根本的な問題は、CART_TOO_BIG_LIMITという名前があいまいなことだ。

  • アドバイス: 限界値を明確にするには、名前の前に max_min_ を付けよう。
MAX_ITEMS_IN_CART= 10

if shopping_cart.num_items() >= MAX_ITEMS_IN_CART:
    Error("カートにある商品数が多すぎます。");

3.4 範囲を指定するときはfirstとlastを使う

包含の意味を含むのであれば、範囲を指定するときはfirstlastを使用した方が良い。

set.PrintKeys(first="Bart", last="Maggie");

3.5 包含/排他的範囲には begin と end を使う

包含・排他的範囲 (最初を含むが、最後の要素は含まない)の場合は beginendを使うのが良い。

3.6 ブール値の名前

ブール地の変数やブール地を返す関数の名前を選ぶときは、truefalseの意味を明確にしなければならない。

bool read_password = true;

上記のプログラムは2つの意味に読み取ることができる。

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

need_passworduser_is_authenticatedなどが適切。

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

例: get*()

getで始まるメソッドはメンバの値を返すだけの「軽量アクセサ」の意味を持っている。以下は、getMean()が非常に重たいため良くない例。

public class StatisticsColllector {
    public void addSample(double x) { ... }
    public double getMean() {
        // すべてのサンプルをイテレートして、total/num_samplesを返す。
    }
    ...
}

例: list::size()

list.size()の計算量がO(n)のため、下記のコードは [tex:O(n2)] になっている。

void ShrinkList(list<Node>& list, int max_size) {
    while (list.size() > max_size) {
        FreeNode (list.back());
        list.pop_back();
    }
}

sizeメソッドは一定時間で終了するようにすべきだ。最新のC++標準では、size()の計算量をO(1)にすることが求められている。

3.8 例: 複数の名前を検討する

名前を決めるときは複数の候補を検討する。

experiment_id: 100
description: "フォントサイズを14ptに上げる"
traffic_fraction: 5%

同じような実験をするときは設定ファイルの大部分をコピペしなければならない。 既存の設定ファイルをほかの実験でも使えるようにしたい場合、以下のように書くことができる。

expriment_id: 101
the_other_experiment_id_I_want_to_reuse: 100
[ 以下、変更が必要な情報だけ書き換える ]
  • template
experimnet_id: 101
template: 100

「これはテンプレートなのか」「このテンプレートを使っている」のかが分かりにくい。 - reuse

experiment_id: 101
reuse: 100

「この実験は100回再利用できる可能性がある」と誤解されるのを防ぐために、"reuse_id"とした方が良い。ただし、reuse_id=100でぼ、この実験の再利用idは100だ、と誤解する人が現れるかもしれない。 - copy :

experiment_id: 101
copy:100

「この実験を100回コピーする」とか「これは100回目のコピーだ」などと思われる可能性がある。

  • inherint
experimnt_id: 101
inherint: 100

継承するという意味は適切に思える。 したがって、最善の名前はcopy_experimnetinherit_from_experiment_idということになった。

3.9 まとめ

英語の単語は、 filter, length, limitのように、プログラミングに使うには意味があいまいなものが多い。 名前を決める前に、誤解される可能性について考慮する。

第4章「美しさ」

優れたソースコードは「目に優しい」ものでなければならない。本章では、コードを読みやすくするための余白・配置・順序について説明する。

  • 読み手が慣れているパターンと一貫性のあるレイアウトを使う。
  • 似ているコードは似ているように見せる。
  • 関連するコードをまとめてブロックにする。

なぜ美しさが大切なのか?

見た目が美しいコードの方が読みやすいのは明らかだ さっと流し読みができれば、だれにとっても使いやすいコードだといえるだろう。

4.1 一貫性のある簡潔な改行位置

適切な改行を入れることにより、「似ているコードは似ているように見える」という原則を守る。

public class PerformanceTester {
    public static final TcpConnectionSimulator wifi = new TcpConnectionSimulator (
        500, /* Kbps */
        80, /* millisecs latency */
        200, /* jitter */
        1 /* packet loss % */);

ソースコードを整形する。

public class PerformanceTester {
    public static final TcpConnectionSimulator tb_fiber = 
        new TcpConnectionSimulator (
            500,   /* Kbps */
            80,    /* millisecs latency */
            200,   /* jitter */
            1      /* packet loss % */);

より簡潔にするように、コメントを上位に書いて下位の引数を並べるようにする。

public class PerformanceTester {
   // TcpConnectionSimulator (throughtput, 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, 5);
}

4.3 メソッドを使った整列

見た目が美しくないコードや、同じことを何度も書いているコードは、ヘルパーメソッドを使って改善する。

CheckFullName("Doug Adams", "Mr. Douglas Adams", "");
CheckFullName(" Jake Brown,", "Mr. Jake Bron III", "");
CheckFullName("No Such Guy", "", "no match found");
CheckFullName("John", "", "more than one result");
  • 重複を排除することデコードが簡潔になった。
  • テストケースの大切な部分 (名前やエラー文字列)が見やすくなった。
  • テストの追加が簡単になった。

4.4 縦の線をまっすぐにする

縦の線をまっすぐにし、列を「整列」させれば、コードが読みやすくなることがある。

CheckFullName("Doug Adams"  , "Mr. Douglas Adams", "");
CheckFullName(" Jake Brown,", "Mr. Jake Bron III", "");
CheckFullName("No Such Guy" , ""                 , "no match found");
CheckFullName("John"        , ""                 , "more than one result");

wgetのコードでは、利用可能なコマンドラインオプション(100個以上ある)が、以下のように並べられている。

commands[] = {
    ...
    { "timeout",    NULL,              cmd_spec_timeout },
    { "timestamp",  &opt.timestamping, cmd_boolean },
    { "tries",      &opt.ntry,         cmd_number_inf },
    { "useproxy",   &opt.use_proxy,    cmd_boolean },
    { "useragent",  NULL,              cmd_spec_useragent },
    ...
};

整列すべきなのか?

整列することにより、1行変更するだけですべての行を変更しなければならない、差分が増えるという意見があるが、実際にはそこまで手間はかからないはずだ。

4.5 一貫性と意味のある並び

ランダムに並べるのではなく、意味のある順番に並べるのが良い。例えば、

  • 対応するHTMLフォームの\<input>フィールドと同じ並び順にする。
  • 「最重要」なものから重要度順に並べる。
  • アルファベット順に並べる。

4.6 宣言をブロックにまとめる

コードの概要を素早く把握するには、単位を作成する。

メソッドを1つの大きなグループにまとめるのではなく、論理的なグループに分けてあげるのが適切だと思われる。

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)

4.8 個人的な好みと一貫性

クラスと定義のカッコの位置など、個人的な好みはどちらでもよい。ただし、それを混ぜてしまうと、すごく読みに組み物になってしまう。

プロジェクトの規約に従い、一貫性があった方がプログラムとしては読みやすいものになる。