delhi09の勉強日記

技術トピック専用のブログです。自分用のメモ書きの投稿が多いです。あくまで「勉強日記」なので記事の内容は鵜呑みにしないでください。

数年ぶりにリーダブルコードを読んだ

概要

数年ぶりにリーダブルコードを読んだ。

www.amazon.co.jp

前から順番にガッツリ読んだ訳ではなくて、パラパラ流し読みしつつ、引っかかったトピックを重点的に読んだ。

前回読んだのは、恐らくもう5年以上前で、まだ就職する前で大学生でアルバイトでエンジニアをやっていた頃だったと記憶している。

当時はあまりピンとこなくて、正直、一般的に高評価されているほど価値のある本だと思えなかったのだけれど、今になって読み直してみたらすごく貴重なことが色々書いてある本だということに気づいた。(この辺の評価が変わった理由についての考察は最後に書いた)

まずは以下に読み直した感想を書いていきたいと思う。

本書の全体に通底する考え方(だと思ったこと)

以下の2つは本書を読む上で、常に心に留めておきたい大事な考え方だと思った。

  1. 一貫性のあるスタイルは「正しい」スタイルよりも大切
  2. 「やりすぎ」はよくない

1.は「4章 美しさ」(p53)に出てくる言葉で、2.は「10章 無関係の下位問題を抽出する」(p140)と「14章 テストと読みやすさ」(p195)に出てくる言葉である。

本書はコーディングに関する良いプラクティスをまとめた本だが、上記の2つは常に心に留めた上で読む必要があるのではないかと思った。

(『リーダブルコード』に限らず、プログラミングの良いプラクティス集的な本を読むとき及び実践するときに大事な考え方だと思う。)

「1.一貫性のあるスタイルは「正しい」スタイルよりも大切」について

この言葉は、個人的なコードの美しさの嗜好よりも、プロジェクトの規約に従う方が重要であるという文脈で出てくる。

私はこの考え方の大切さは、コーディングスタイルの問題に限らないと思う。

担当者によって色々なコーディングスタイルや設計・実装方針が入り混じってしまうよりも、コード全体が、コードの読み手にとって、メンタルモデルを構築しやすくなっていることの方が遥かに重要だと思う。(例えそれが良いプラクティスであったとしても)

(話が逸れるが、「メンタルモデル」という言葉については、ちょっと前に読んだ以下の記事がとても勉強になったので、「メンタルモデル」っていう概念いいなと思って適切かは分からないが使ってみた。)
techlife.cookpad.com

エンジニアをやっていると、「さすがにこれは直したいな」と思うこともあると思うが、時にはムズムズしても我慢することも大事なんだろうなと思う。

「「やりすぎ」はよくない」について

この言葉は、「10章 無関係の下位問題を抽出する」では、1つの関数の処理が大きくなりすぎた場合に、複数の関数に分割するのは良いことだが、分割しすぎるとかえって読みづらくなってしまうという文脈で出てくる。

私自身、関数やクラスを分割しすぎた結果、1つのロジックを読むのにファイルの上下やファイル間を行ったり来たりしなければならなくなって、かえって読みづらくしてしまうとかはよくやってしまう。
(実装している本人はその時には気づかないが、後から自分のコードを読むと読みづらいなと感じたり、人からレビューで指摘されたりする。)

やはり、関数やクラスの分割に限らず、ソースコードの規模やプロジェクトの目的によって、適切な程度というのはあるはずなので、やり過ぎればよいというものではないと思う。
(「適切な程度」を体で覚えたり、もしくは指標化したりするのが難しいのだろうが)

個人的に重要だなと思ったトピック

以下では、個人的に重要だなと思ったトピックのいくつかに関して、読みながら考えたことや感想を書いていく。

4.6「宣言をブロックにまとめる」、4.7「コードを段落に分割する」(p51)

ある時期から、コードを書く時に、

  • 行と行の間は空けるべきなのか
  • 空けるべきだとしたら、どこで空けるのがよいのか

ということで、悩むようになった。

ただ、「こういう箇所で空白行を入れましょう」というような一般的な指針はあまり存在しないので、なんとなく「ここは処理の区切り目だから空白行を入れておこうかな?」というように、モヤモヤしながらも感覚的にやっていた。

改めて本書を再読したら、「こういう場面では空白行を入れてコードをグルーピングするといいですよ」っていう指針がちゃんと書いてあることに気づいた。

7.1 条件式の並び順(p84)

このトピックでは

if (length >= 10)

if (10 <= length)

ではどちらが読みやすいかという話題が出てくる。

私も含めて、多くの人が前者の方が読みやすいと感じると思う。

これも、特に誰かに習った訳ではなくて、本のサンプルコードを読んだり、人のコードを読んだりしているうちに、自然とそういうものだと身についたのだと思う。
だから、「なんで逆だとよくないのか説明してほしい」と言われると難しい。

本書を読むとちゃんと納得できる理由が書いてある。

やや余談だが、Javaでは以下のように、ヌルポを防御するためにexpectedな値の方を先に書く場合がある。

if("OK".equals(status)) {
    // 処理
}

これはこれで合理的な理由があるので良いのだが、初めて見たときは、なんで逆にしているんだろう?と疑問に思ったのを覚えている。

この書き方に違和感を覚えるというのも、評価する値を左側に書くというのが感覚レベルで身についていたからなんだろうなと思う。

7.2 if/elseブロックの並び順(p86)

このトピックでは、 if/elseで条件分岐をするときに、どの順番から書くとよいかということが議論されている。

私は、これも誰かに習った訳ではないが、経験的に「条件にnotが入ると可読性が下がるので、できるだけ避けるべき」ということは感じていたので、なるべく肯定条件で書くようにはしていた。

ただ、ロジックによっては、

if(has_error) {
  // エラー処理
} else {
  // 正常系の処理
}

のように、elseブロックの処理の方が正常系のロジックになる場合もある。

このような場合は、ifの先頭で判定するのが否定条件になったとしても、正常系の処理が先にくるようにした方がいいんじゃないかというようなことも考えていた。

この辺の考え方についても、本書にはいくつかの指針が提示されていた。
(考え方は大体合っていた。)

結論としては、「状況によって判断基準は変わってくる」とのことである。

8.1 説明変数、8.2 要約変数(p100)

ある程度プログミングをしている内に、変数には以下の2つの役割があるんだなと、自分の中で理解するようになった。

  1. 値に名前をつける。
  2. 式の結果を保存することで、同じ処理を何度も実行しないようにする&参照しやすいようにする。

1.に関しては、この逆の悪い例がいわゆる「マジックナンバー」だと思う。
マジックナンバー (プログラム) - Wikipedia

2. に関しては、例えば、患者が花粉症の症状があるか否かで処理を分岐したい時に、以下のようにいったん判定の結果を変数に格納しておく。

boolean hasSymptoms = patient.hasHayfever() && (month == 2 || month == 3)

こうすることで、同じ判定結果がコードの中で何回か必要になった場合に、hasHayfever()を何度も実行しなくてよいし、複雑な条件式を何度も書かなくてよいので、DRYなコードになる。

今日のコンピュータの性能であれば、よっぽど重い処理じゃなければ、処理が複数回実行されることでパフォーマンスに無視できないレベルの影響が出ることはないと思うので、「コードがDRYになる」の方が主要なメリットなのではないかと思う。

前置きが長めになったが、本書の用語に照らし合わせると、1の役割にあたるのが「説明変数」、2の役割にあたるのが「要約変数」ということになる。

上に書いたように、この2つの用語を知らなくても、ある程度コードを書いていれば、変数にはこういう役割があるんだなということは、感覚的にわかるようになると思うが、用語を与えられることで、

  • 感覚的に理解していたことに名前をつけて体系的に理解できるようになる。
  • レビューなどの場で、「ここは条件式が複雑だから『リーダブルコード』に出てくる「要約変数」にした方がいいと思うよ」というように、端的な言葉でコミュニケーションできるようになる。

などのメリットがあると思う。

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

この章では全体として、ある関数の中で、処理の抽象度が周りのロジックと比べて低い部分(下位問題)を抽出して、別の関数に切り出すというプラクティスについて扱っている。

最初の例では、複数地点([経度,緯度のペア)の配列の中で、ある地点から最も近いものを探すロジックが出てくる。

この処理では、2つの地点の経度と緯度から距離を算出する部分のロジックが、とても複雑であり、周りのロジックと比べて抽象度が低い処理となっている。
この部分を別の関数に抽出することで、読みやすいコードになるということである。

これに関しても、あるメソッドの処理がfatになりすぎたら、処理をフェーズに分割して、別のclassやprivateメソッドに抽出するということは、ある程度の経験を積んだ頃には自然とやるようになっていた。

ただそれは、そうしないと可読性が下がって自分も困るからやっていただけで、一般的なリファクタリング手法であるということを意識しながらやっていた訳ではなかった。

今回、本書を読み直して、一般的なプラクティスの観点でも、間違ったことや横道にそれたことをしていた訳ではなかったんだなということを認識できたのはよかった。

また、p141の「あわせてよみたい」のコラムに、ケント・ベックの『Smalltalk Best Practice Patterns』の引用として紹介されている

メソッド内部の処理は同じ抽象度のレベルになるようにします

というアドバイスがとても勉強になった。

メソッド内部を複数のprivateメソッドに分割するということは、これまでもある程度できていたけど、分割するときの抽象度を揃えるということは意識していなかったので、今後はその辺も意識してコーディングできるようになりたいなと思った。

感想

全般的に感じたこととして、業務でエンジニアをやってきた中で、人のコードを読んだり、「ここはこうした方がいいんじゃないか」と考えたりして、自然と身についていたプラクティスに、ちゃんと名前が付けられて自分の中で整理されていく感じがとても気持ちがよかった。

数年前に読んだ時から評価が変わった理由の考察

今回、読み直して思ったのは、『リーダブルコード』以外の本にはあまり書かれていない貴重なトピックは、ある程度の品質が求められるコードをチームで書くようにならないとピンとこない、地味なトピックが多いなと思った。

学生でアルバイトでしかコードを書いたことがなかった当時は、その辺はアンテナにあまり引っかからなかったのかなと思った。

逆に『リーダブルコード』にも

  • 早期returnを使いましょう
  • 重複を除いたListが欲しい場合はSetを使いましょう

などといった、比較的テクニカルなトピックもあって、当時はそっちの方がアンテナに引っかかったのだと思う。

ただ、これらのテクニカルなトピックの方は、決して難しい内容を扱っている訳ではなく、『リーダブルコード』を読まなくても、他の本や記事にも書いてあったり、レビューで指摘されたりして自然と覚えられるようなことなので、結果的にあまり新しいことを得られなかったなという感想になったのだと思う。