LoginSignup
1
1

More than 1 year has passed since last update.

リーダブルコードを読んだ

Last updated at Posted at 2023-02-19

前置き

何回か読んだけど、少なくとも実務に活かせるほど自分の中で落とし込めていないなと思ったので自分のためにまとめました。
ほんと誰得な記事ですが、ここに供養します。
自分にとって有用だと思った部分のみ抜粋しています。

1 理解しやすいコード

1.1 「優れた」コードって何?

前者と後者ではどちらが優れているだろうか

return exponent >= 0? mantissa * (1 << exponent) : mantissa / (1 << -exponent);
if (exponent >= 0) {
    return mantissa * (1 << exponent);
} else {
    return mantissa * (1 << -exponent);
}

前者のほうが簡潔だ。でも、後者のほうが安心だ。「簡潔」と「安心」はどちらが大切なのだろうか

1.2 読みやすさの基本定理

他人に自分のコードを読んでもらって理解するまでにかかる時間を最短にしよう。

1.3 小さなことは絶対にいいこと?

コードは短くした方がいい。でも、「理解するまでにかかる時間」を短くするほうが大切だ。

1.4 「理解するまでにかかる時間」は競合する?

コードを効率化する
テストしやすくする
設計をうまくする
これらは理解しやすさと競合しない。
それに理解しやすいコードは優れた設計やテストのしやすさにつながることが多い。
どうすればいいか分からなくなったときは、本書の規則や原則よりも「読みやすさの基本定理」を最優先に考えてほしい。

1.5 でもやるんだよ

他人の誰かが自分のコードを理解しやすいかなんて考えるのは大変なことだ。
でもこの目標を受け入れたら、バグの少ないコードを作り出せるようになる!

第Ⅰ部 表面上の改善

2 名前に情報を詰め込む

名前は短いコメントだと思えばいい。
短くてもいい名前をつけられれば、それだけ多くの情報を伝えることができる。
情報を詰め込んだ名前の付け方を紹介する。本章は6つのテーマで構成されている。

  • 明確な単語を選ぶ
  • 汎用的な名前を避ける(あるいは、使う状況を選ぶ)
  • 抽象的な名前よりも具体的な名前を使う
  • 接尾辞や接頭辞を使って情報を追加する
  • 名前の長さを決める
  • 名前のフォーマットで情報を伝える

2.1 明確な単語を選ぶ

例えば、「get」はあまり明確な単語ではない。

def GetPage(url):
...

「get」という単語からは何も伝わってこない。
このメソッドはどこから取ってくるのだろうか?ローカルキャッシュ?データベースから?インターネットから?
インターネットから取ってくるのなら、FetchPage()やDownloadPage()のほうが明確だ。

もっと「カラフル」な単語を探す

シソーラス(類語辞典)を使って調べよう。

単語 代替案
send deliver, dispatch, announce, distribute, route
find search, extract, locate, recover
start launch, create, begin, open
make create, set up, build, generate, compose, add, new

2.2 tmpや、retvalなどの汎用的な名前を避ける

tmp/retval/fooのような名前を付けるのは、「名前のことを考えていません」と言っているみたいなものだ。
retvalを使った関数を見てみよう。

var euclidean_norm = function (v) {
    var retval = 0.0;
    for (var i = 0; i < v.length; i += 1)
        retval += v[i] * v[i];
    return Math.sqrt(retval);
};

retvalは「戻り値」以外の情報はない。
いい名前というのは、変数の目的や値を表すものだ。
ここではvの二乗の合計を表しているわけだから、sum_squaresという名前がいいだろう。
変数名がsum_squaresだったらバグを見つけやすい。

sum_squares += v[i]; // 二乗の合計じゃない

ただし、汎用的な名前に意味がないわけではない。
うまく使った例を見ていこう。

tmp

2つの変数を入れ替える古典的な例を考えてみよう。

if (right < left) {
    tmp = right;
    right = left;
    left = tmp;
}

このような場合は、tmpという名前で全く問題ない。
この変数の目的は情報の一時的な保管だ。
「この変数は他に役割がない」という明確な情報を伝えている。

2.3 抽象的な名前よりも具体的な名前を使う

変数や関数などの構成要素の名前は、抽象的ではなく具体的なものにしよう。
もし絶対に知らせなければいけない大切な情報があれば「単語」を変数名に追加すればいい。
例えば、16進数の文字列を持つ変数について考えてみよう。

string_id; // af8ef584ecd8

idのフォーマットが大切なら、名前をhex_idにするといい。
以下余談
hexは16進数の意味のHexadecimalの略語。
6(Hexa)と10の進数(decimal)で16進数。オシャレすぎる。
https://jumbleat.com/2016/10/14/about_hex/

2.4 名前に情報を追加する

値の単位

プログラミングで使う単位はたくさんある。
以下の表は単位のない仮引数と、単位を追加したより良いバージョンの仮引数を示したものだ。

関数の仮引数 単位を追加した仮引数
Start(int delay) delay -> delay_secs
CreateCache(int size) size -> size_mb
ThrottleDownload(float limit) limit -> max_kbps
Rotate(float angle) angle -> degrees_cw

その他の重要な属性を追加する

セキュリティの問題など注意を換気する情報も追加した方がいい。

状況 変数名 改善後
passwordはプレインテキストなので、処理をする前に暗号化すべきである。 password plaintext_password
ユーザーが入力したcommentは表示する前にエスケープする必要がある comment unescaped_comment
htmlの文字コードをUTF-8に変えた html html_utf8
入力されたdataをURLエンコードした。 data data_urlenc
基本的に変数の意味を理解してもらわなければ困るところに属性を追加しておこう。

2.5 名前の長さを決める

不要な単語を投げ捨てる

ConvertToString()をToString()にしても必要な情報が損なわれていない。

3 誤解されない名前

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

3.1 例:filter()

データベースの問い合わせ結果を処理するコードを書いているとしよう。

results = Database.all_object.filter("year <= 2011")

これでは、year <= 2011のオブジェクトを「選択」するのかyear <= 2011 を「除外」するオブジェクトなのか曖昧だ。
「選択する」のであればselect()
「除外する」のであればexclude()にしたほうがいい。

ex(外に) clude(閉じる) -> 外へ追い出す -> 除外する

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

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

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

3.6 ブール値の名前

ブール値の変数やブール値を返す関数の名前を選ぶときには、trueとfalseの意味を明確な名前にしなければならない。
ブール値の変数名には、頭にis/has/can/shouldなどをつけてわかりやすくすることが多い。

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

例: get()

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

データ量が大量にあったらものすごいコストになるような関数にgetを使わない。
コストが高いということを知らないと安易に使ってしまう。
コストの高さが事前にわかるように命名してあげる。

4 美しさ

優れたソースコードは「目に優しい」ものでなければならない。

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

インデントがきちんとされているコードのほうが理解しやすい。

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

コードの見た目を一貫性のあるものにするには、適切な改行を入れるようにしよう。
(それからコメントも整列させよう)
任意の速度のネットワークに接続したときにプログラムがどのように動くかを評価するコードだ。
TopConnectionSimulatorクラスのコストラクタには4つの仮引数がある。

  1. 接続速度(Kbps)
  2. 平均遅延時間(ms)
  3. 遅延「イライラ」時間(ms)
  4. パケットロス率(%)
public class PerfomanceTester {
  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 (
      45000, /* Kbps*/
      10,    /* millisecs latency*/
      0,     /* jitter */
      0      /*packet loss %*/);
  public static final TcpConnectionSimulator cell =
    new TcpConnectionSimulator (
      100, /* Kbps*/
      400, /* millisecs latency*/
      250, /* jitter */
      0    /*packet loss %*/);

}

このコードでは一貫性があり、楽に目を通せるが、縦に長くなっているし、同じコメントが3回繰り返されている。
このクラスを完結に書いたら以下のようになる。

public class PerfomanceTester {
  // 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,    0);
}

コメントを最上部に移動させて、仮引数を一行で書くようにした。
より簡潔な表組に「データ」が並ぶようになった。

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

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

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

4.7 コードを「段落」に分割する

文章と同じようにコードを「段落」に分けるべきだ。

  • 似ている考えをグループにまとめて、他の考えと分けるため
  • 視覚的な「踏み石」を提供できるから。これがないと、ページの中で自分の場所を見失う
  • 段落単位で移動できるようになるから

ひとかたまりのコードは誰も読む気がしない。

5 コメントすべきことを知る

5.5 まとめ

コメントの目的とはコードの意図を読み手に理解してもらうことである
コメントすべきではないこと

  • コードからすぐに抽出できること
  • ひどいコードを補う
  • コメントを書くのではなく、コードを修正する

6 コメントは正確で簡潔に

6.4 関数の動作を正確に記述する

// このファイルに含まれる行数を数える
int CountLines(string filename) {...}

あまり正確なコメントではない。
「行」には様々な意味があるからだ。

// このファイルに含まれる解消文字(\n)を数える
int CountLines(string filename) {...}

6.5 入出力のコーナーケースに実例を使う

6.9 まとめ

  • 複数のものを指す可能性がある「それ」などの代名詞は避ける。
  • 関数の動作はできるだけ正確に説明する。
  • コメントに含める入出力の実例を身長に選ぶ。
  • コードの意図は、詳細レベルではなく、高レベルで記述する。
  • よくわからない引数にはインラインコメントを使う。
  • 多くの意味が詰め込まられた言葉や表現を使って、コメントを簡潔に保つ。

第Ⅱ部 ループとロジックの単純化

8 巨大な式を分割する

8.5 より優雅な手法を見つける

驚くほど複雑なロジックのコードには、それと「反対」の視点で問題を解決してみる
とにかくいつもと反対のことをやってみる

8.8 まとめ

巨大な式を分割して、読み手が1つずつ読み込めるようにする方法
最も簡単な方法は「説明変数」を導入すること。大きな式の値を保持する説明変数には、3つの利点がある。

  1. 巨大な式を分割できる
  2. 簡潔な名前で式を説明することで、コードを文書化できる。
  3. コードの主要な「概念」を読み手が認識しやすくなる

その他にはド・モルガンの法則を使って論理式をきれいに書き直すことができる

9 変数と読みやすさ

変数を適当に使うとプログラムが理解しにくくなる。

  1. 変数が多いと変数を追跡するのが難しくなる
  2. 変数のスコープが大きいとスコープを把握する時間が長くなる
  3. 変数が頻繁に変更されると現在の値を把握するのが難しくなる

9.1 中間結果を削除する

中間結果を保持せず、結果をそのまま使えば、コードはスッキリする

var remove_one = function (array, value_to_remove) {
	var index_to_remove = null;
	for (var i = 0; i < array.length; i +=1) {
		if (array[i] === value_to_remove) {
			index_to_remove = i;
			break;
		}
	}
	if (index_to_remove !== null) {
		array.splice(index_to_remove, i);
	}
}

index_to_removeは中間結果を保持するためだけに使っている。 ので、それらを削除する

var remove_one = function (array, value_to_remove) {
	for (var i = 0; i < array.length; i +=1) {
		if (array[i] === value_to_remove) {
			array.splice(i, 1);
			break;
		}
	}
}

関数から早く返すことで、index_to_removeをすべて削除できた。
タスクはできるだけ早く完了する方がいい

9.2 変数のスコープを縮める

クラスのメンバ変数というのは、クラスの中で「ミニグローバル」になっているとも言える。
どのメソッドが変数を変更しているのか追跡するのが難しいので、ミニグローバルはできるだけ削除した方がいい。
クラスのメンバへのアクセスを制限する別の方法は、メソッドをできるだけ staticにすることだ。
staticメソッドを使えば「メンバ変数とは関係ない」事がよくわかる。
また、大きなクラスを小さなクラスに分ける。 ただし、お互いに独立して参照し合わないクラス限定。

submitted = false; //  var をつけていないのでグローバル変数

var submit_form = function(form_name) {
	if (submitted) {
		return; // 二重投稿禁止
	}
	...
	submitted = true;
};

これをクロージャで包む


var submit_form = (function() {
	var submitted = false; // 以下の関数からしかアクセスされない
	return function (form_name) {
		if (submitted) {
			return; // 二重投稿禁止
		}
		...
		submitted = true;
	};
}());

最終行にある括弧に注目すると、外側の無名関数がすぐに実行されて内側の関数を返している。
jsのクロージャでグローバル変数を包んであげることで、プライベートスコープを作る効果がある。
これで、読みてはsubmittedがいつ使われるのか他の箇所で気にしなくて済む。
jsのベストプラクティスは変数を定義するときは常に varキーワードをつける。こうすれば、変数のスコープをその変数が定義された(最も内側の)関数に制限してくれる。

9.3 変数は一度だけ書き込む

9.5 まとめ

  1. 邪魔な変数を削除する。 中間結果を保持する変数を削除し、結果をすぐにreturnする
  2. 変数のスコープをできるだけ小さくする。
  3. 一度だけ書き込む変数を使う。 const など

第Ⅲ部 コードの再構成

10 無関係の下位問題を抽出する

  1. 関数や、コードブロックを見て「このコードの高レベルの目標はなにか?」と自問する
  2. コードの各行にたいして「高レベルの目標に直接的に効果があるのか?あるいは、無関係の下位問題を解決しているのか?」を自問する
  3. 無関係の下位問題亜を解決しているコードが相当量あれば、それらを抽出して別の問題にする

これらは、無関係の下位問題を積極的に探すことで、コードを大幅に改善できる

10.9 まとめ

プロジェクト固有のコードから汎用的なコードを分離すること。ほとんどのコードは汎用化できる。

11 一度に1つのことを

コードは1つずつタスクを行うようにしなければならない。
「一度に1つのタスクをする」ための手順

  1. コードが行っている「タスク」をすべて列挙する。ex) オブジェクトが妥当かどうかを確認する
  2. タスクをできるだけ異なる関数に分割する。少なくとも異なる領域に分割する。

11.1 タスクは小さくできる

void UpdateCount(HttpDownload hd) {
	// タスク: 抽出したい値にデフォルト地を設定する
	string exit_state = "unknown";
	string http_response = "unknown";
	string content_type = "unknown";

	// タスク: HttpDownloadから値を1つずつ抽出する
	if (hd.has_event_log() && hd.event_log().has_exit_state()) {
        http_response = StringPrintf("%d", hd.http_headers().response_code());
    }

    if (...) {
        ...
    }

    if (...) {
        ...
    }

    // タスク: counts[]を更新する
    counts["Exit State"][exit_state]++;
    counts["Http Response"][http_response]++;
    counts["Content-Type"][content_type]++;
}

このコードではタスクが4->3つになっているが、きっかけにすぎないので臨機応変に領域を分割していく。
領域に分けておくといいのは、それぞれが分離されているからだ。ある領域にいるときは他の領域のことは考えなくて済む。

11.4 まとめ

読みにくいコードがあれば、そこで行われているタスクをすべて列挙する。
そこには別の関数やクラスに分割できるだろう。
タスクをどのように分割するよりも、分割すること自体が大切

12 コードに思いを込める

自分よりも知識がない人が理解できるような「簡単な言葉」で説明する能力が大切。
これは、相手に理解してもらうだけではなく、自分の考えをより明確にすることになる。
ソースコードも簡単な言葉で書くべきだ
コードをより明確にする手順

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

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

以下はPHPで書かれている。
セキュアコードを扱っていて、ユーザーにページを閲覧する権限があるかどうかを確認して、なければ権限がないことをユーザーに知らせるページに戻す。

$is_admin = is_admin_request();
if ($document) {
    if (!$is_admin && ($document['username'] != $_SESSION['username'])) {
        return not_authorized();
    } else {
        if (!$is_admin) {
            return not_authorized();
        }
    }
}
...
ページのレンダリングが続くコード

このロジックは単純化できる。簡単な言葉でロジックを説明していみよう。
権限があるのは以下の2つ。

  1. 管理者
  2. 文書の所有者(文書がある場合)
    その他は権限がない。
    ここから、解決案が以下。
if ($is_admin_request()) {
    // 権限あり
} elseif ($document && ($document['username'] == $_SESSION['username']) {
    // 権限あり
} else {
    return not_authorized();
}

改修前と比べると変数が一つなくなった($is_admin)。
否定形の!が全部なくなった。
if の中身が空になったが、単純になった。

12.2 ライブラリを知る

簡潔なコードを書くのに欠かせないのは、ライブラリが何を提供しているかを知ることだ。

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

12.4 まとめ

説明で使っている単語やフレーズをよく見れば、分割する下位問題がどこにあるかが分かる。
ラバーダッキングも有効。
問題や設計をうまく言葉で説明できないのであれば、何かを見落としているか、詳細が明確になっていないということだ。
プログラム(あるいは自分の考え)を言葉にすることで明確な形になるのだ。

13 短いコードを書く

最も読みやすいコードは何も書かれていないコードだ。

13.1 その機能の実装について悩まないで、きっと必要ないから

プログラマというのは、実装にかかる労力を過小評価するものである。

13.2 質問と要求の分割

すべてのプログラムが高速で、100%正しくて、あらゆる入力をうまく処理する必要はない。
要求を詳しく調べれば、問題をもっと簡単にできることもある。
そうすれば、必要なコードも少なくて済む。

13.3 コードを小さく保つ

  • 汎用的な「ユーティリティ」コードを作って、重複コードを削除する
  • 未使用のコードや無用の機能を削除する
  • プロジェクトをサブプロジェクトに分割する
  • コードの「重量」を意識する。軽量で機敏にしておく

13.4 身近なライブラリに親しむ

たまには標準ライブラリのすべての関数・モジュール・型の名前を15分かけて読んでみよう

第Ⅳ部 選抜テーマ

14 テストと読みやすさ

14.3 テストを読みやすくする

一般的な設計原則として「重要じゃない詳細はユーザーから隠し、大切な詳細は目立たさせる」

15 「分/時間カウンタ」を設計・実装する

コードを理解できなかった 。。

余談

VSCodeの日本語入力が遅くてぶちギレそうだったので結局Neovimで編集しました

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1