リーダブルコードを読んだ話
「リーダブルコード」を読んだので、紹介しつつ感想を書く。
https://www.amazon.co.jp/dp/4873115655
背景
普段の仕事でプログラムを書いていて、コードレビューにおいて「リーダブルコード的に言うと…」という指摘を受けることがあった。
自分で自分のコードの読みやすさに関して課題を感じていた(数ヶ月前に書いたコードが何してるのかよく分からない)こともあり、書籍を読んでみることにした。
概要と感想
書籍の概要は以下の通り。
コードは理解しやすくなければならない。本書はこの原則を日々のコーディングの様々な場面に当てはめる方法を紹介する。名前の付け方、コメントの書き方など表面上の改善について。コードを動かすための制御フロー、論理式、変数などループとロジックについて。またコードを再構成するための方法。さらにテストの書き方などについて、楽しいイラストと共に説明する。日本語版では Ruby や groonga のコミッタとしても著名な須藤功平氏による解説を収録。
まさにこの通りの内容だった。大まかに以下の 3 部に分かれている。
- 表面上の改善
- ループとロジックの単純化
- コードの再構成
簡単に紹介する。ここで紹介しているのはほんの一部の実践なので、気になる方はぜひ買って読んでみてください。
1. 表面上の改善
変数や関数の命名、コードのレイアウトやフォーマット、良いコメントのつけ方について。
変数や関数の命名は明確で具体的にその振る舞いを示す非汎用的名前をつけるのが良い。
例えば GetPage
という名前の関数があったとして「ページを取ってくるのかな?」という挙動は予測できるが、それをどこからどうやって取ってくるのかは見当がつかない。インターネット経由で取ってくるなら FetchPage
とか DownloadPage
のような名前をつけるべきだろう。
また、大きさを表すのに Size
という汎用的な名前よりは Height
とか Bytes
のように中身がより明確に想像できる方が良い。Get
や Size
のような汎用的な命名であっても、生存期間が短ければ(= 定義してすぐ使われ他の場所では使われないなど、影響範囲が狭ければ)問題ない。
命名する際はユーザー(その関数や変数を使う側)の視点に立って期待に沿うような命名をする。誤解を防ぐためには、より明確な語彙を使う。例えば文字列を一部削除して整形するような関数なら clip
よりは remove
とか truncate
のように、より具体的な振る舞いを示す単語を使うのが良い。
コードのレイアウトについても基本的な思想は同じで、読み手に分かりやすいよう整える。読み手が慣れているパターンに沿って関連するコードをブロックにまとめ、似ているコードを同じ場所に集める。プログラムは書く時間より読む時間が長い。
例えばテストコードであれば、「状況 A で入力 B から振る舞い X を経て出力 Y を期待する」のような動作をコードとして書き、実際にその状況をセットアップしたり入力値を用意する処理はヘルパーメソッドとして分けておくのが良い。
コメントは書き手の意図を読み手に知らせるための道具であって、コードからすぐに分かることはコメントすべきではない。なぜなら、その分だけコードを読む時間が減るからだ。 例えば下記のコメントは全く役に立たない。
// address で指定されたメールアドレス宛にメールを送る
sendMail(address, subject, body) {
/* */
}
コメントするのであれば、「このコードを読む/使う時にミスしがちなことはなんだろう?」のように読み手使い手の立場に立って役に立つコメントを書く。例えば上記の関数であれば、下記のようなコメントだったら役に立つだろう。
// メールを送信する外部サービス hogehoge を呼び出す(1分でタイムアウト)
sendMail(address, subject, body) {
/* */
}
関数や変数の中身や振る舞いについてはコメントではなく良い名前をつけることで読みやすくすべきだが、大きなクラスやファイルなどで先頭にその責任や挙動の全体像をコメントすることは理に適っている(その分コードを読む時間が少なくて済む)。
2. ループとロジックの単純化
制御フローや式の分割について。
条件やループはできるだけ自然に記述し、読み手が立ち止まって読み返す必要がないようにする。例えば変数を特定の値と比較した結果に応じて処理を分岐する場合を考える。
// 調査対象 -> 比較対象
if (length <= 10) {
/* */
}
// 比較対象 -> 調査対象
if (10 >= length) {
/* */
}
結果はどちらも同じだが、英語の自然な文法に沿っている上の方が読みやすいだろう。条件やループを自然にする方法としては、関心を引く条件や目立つ条件を先に書くのが良い。また、ネストは浅くして関数からはできるだけ早く返すと読みやすいコードになる。
条件やループを処理するコードが長くなってきたら、一部の処理の結果を説明したり要約する変数を用意するのが良い。
if line.split(':')[0].strip() == "root":
# ...
上記のコードより下記のコードの方が読みやすい。
username = line.split(':')[0].strip()
if username == "root":
# ...
複雑なものをより単純、明確にしていくことでコードが読みやすくなる。
3. コードの再構成
コードを大きく変更したり完全に削除することについて。
場当たり的にコードを追加していくと、ついつい大きな読みづらい関数やクラスが誕生してしまう。
関数やコードブロックを見て、「このコードの高レベルの目標は何か」を抽出し、各行に対してその目標に直接的に効果があるのか「無関係の下位問題」を解決しているのかを自問することで、読みやすい単位に分割していくことができる。
無関係の下位問題は何と「無関係」なのかというと、コードが解決しようとしている課題の前提となる問題を解決することで、例えば以下のような問題が挙げられる。
- 高レベルの目標が「与えられた緯度経度に最も近い要素を配列から選ぶ」だとすると、「球面三角法の余弦定理」は無関係の下位問題にあたる
- 高レベルの目標が「ファイルの中身をすべて読み込む」だとすると、「ファイルをバッファに読み込む」は無関係の下位問題にあたる
- 高レベルの目標が「サーバを Ajax で呼び出してレスポンスを処理する」だとすると、「レスポンスを整形して綺麗な形で出力する」は無関係の下位問題にあたる
このように大きな問題を明確な境界線を持つ小さな問題に分離していくことで、一度に考えることを減らすことができ、効率よくコードを読み進められるようになる。
このような「無関係の下位問題」は誰かがライブラリとして開発していたり言語標準ライブラリに組み込まれていたりするので、それを使うのが良い。
特に言語標準ライブラリの裏には膨大な設計、デバッグ、修正、文書、最適化の努力、テストがあり、時の試練を生き延びてきているので大きな価値がある。定期的に標準ライブラリのドキュメントやコードを読んでどんなことができそうか考えることで、自分のコードを単純に小さいまま保つことができるようになる。
印象的だった箇所
第 13 章「短いコードを書く」の冒頭にこんな言葉がある。
最も読みやすいコードは、何も書かれていないコードだ。
ここでは例として「店舗検索システム」が挙げられている。任意のユーザーの緯度経度を基に近い店舗を検索するというシステムを作ろうとすると、日付変更線を跨いだときや北極南極近辺の処理、地球の曲率など様々な要素を考慮しなければならない。
しかし、その対象店舗がアメリカ合衆国のテキサス州に 30 店舗しか無いとしたらどうだろうか。単純に総当たりでユーザーと 30 店舗との距離を計算して最も近い距離の店を返せば十分だろう。計算量も大したことはなさそうだ。
これと似た事象はプログラマに限らず割とどこでもあって、事前に完璧にやろうとすると無限に時間がかかるものの、課題の 80% くらいを解決する単純な方法はすぐ作れたりする。最初からいろいろ考えて準備しても、使われなかったり無駄に複雑にしてしまい説明や理解が難しくなる。
限られた時間で多くのことをするには、とりあえず小さく単純な形で始めてみて必要になったら追加していく精神が重要そうだ。
コードを一切書かず人の手も一切動かさずに問題を解決できるなら、それが一番いい。
単純さ
本書の感想ではないが、最近読んだ文章で似たような話がありとても面白かった。
note: 「悪い方が良い」原則と僕の体験談 Rui Ueyama
実装の単純さが良いデザインに優先することもある。
最後に
10 年前に出版された本だが、中身には全く古さを感じなかった。
あえて言うと「JavaScript では var を使おう(var をつけずに変数を使うとグローバルスコープになるため)」という記述があるが、現代の JavaScript で var を使うことはほぼ無く原則は const 例外的に let を使うだろう。
これは本書が誤っているのではなく、本書の指針(変数への代入は一度だけ..)に沿った機能が出版後に JavaScript へ取り込まれたからベストプラクティスが変わったのであって、本書の内容自体は読みやすく使いやすいコードを書く上で非常に役立つと思った。