新卒エンジニアである私がリーダブルコードを読んで学んだこと、そこから今後意識するべきだと思うことをまとめていく。
内容のまとめ
リーダブルコードでは、集団開発をする上で必要な最低限のコーディングルールがまとまっている。
私はこの中で言及されている内容で重要なものは大きく分けると3つの項目がある。
- 命名規則について
- ロジックの単純化について
- コードのまとめ方について
命名規則について
エンジニアには必ず変数や関数を命名するという作業を行う場面がある。しかしこの命名を"hoge"や"fuga"などふざけた名前にしてしまうと、コードが無法地帯となり後々の技術負債につながる。それを避けるために、命名に関してはある程度の共通ルールを設けおく必要がある。
また、わかりやすい変数名をつけることで後でコードを見返した際にバグの発見が容易になるという利点もある。
1.明確な単語を選ぶ
コードを読んでいるとよく出現する、get,putなどの汎用的な単語での命名を避けた方が無難である。
これは具体的な単語を選択することでその変数の役割を想像できる状況を作ることを目的としている。
よく使う単語の代替案
よく使う単語 | 代替案 |
---|---|
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 |
get | fetch,download |
stop | kill,pause |
2.tmpやretvalなどの汎用的な名前を避ける
コードの例文などで一時的な値を保存するときなどにtmpやretvalなどの変数を用いることがある。しかし、これはあまりよい命名ではない。tmp,retvalのようなあまり意味を持たない値を持つことはコードを読む際に混乱を招きやすく、バグを見つけにくくなってしまう。しかし、どうしてもこのような命名をしたいときは、tmp_fileなど情報を追加することで混乱を避けることが効果的である。
このtmpという値は中間結果の保持などで使われることが多いが、リーダブルコードではそもそもの中間結果の保持を推奨していない。中間結果の保持はコードを改善することで必要がなくなることが多いため、なるべく中間結果は保持しないスタンスでいる必要がある。
このことからわかるようにtmpなどの変数や中間結果の保持などは何か明確な理由があるときにのみ用いるべきである。
3.値の単位
時間やバイト数のように計測できるものがあれば、変数名に単位を入れるとわかりやすい。たとえばstart_msのようにミリセカンドを表す_msをつけることでその変数はミリ秒を扱っていることがわかりやすい。
その他にも、変数の意味を理解して欲しいときには変数に重要な属性を付与することが効果的である。
特にデータを安全にする関数を呼び出した後はそのデータの「状態」を変数名に追加することでデータの状態を伝える必要がある。たとえば、セキュリティ保護がされていないURLを扱うときにはuntrustedUrlと名付け、データを安全にする関数を呼び出した後にtrustedUrlなどに変数名を変えることでデータの状態を正しく伝えることができる
扱うもの | 変数名 |
---|---|
時間 | _ms,_secs |
サイズ | _mb,_kbps |
周波数 | _cw |
4.境界値の命名
コードを書くうえでバグが起きやすいのは、値の境界値を扱う場合である。条件分岐の場合に扱う変数が以上なのか以下なのか限界値を含むのか含まないのか意識しながらコードを読むときにも混乱を招きやすい。
しかし、比較に扱う変数にその境界値の情報を持たせておくとコードの理解スピードを早めることができる。
- 限界値を含めるときはminとmaxを使う
- 範囲を指定するときはfirstとlastを使う
- 包含・排他的範囲にはbeginとendを使う
ロジックの単純化について
ロジックが複雑だと、コードの読み手がそこで躓いて時間を奪ってしまったり、何回も読み返すような手間を取らせてしまう。そんな状況を避けるためにも、ロジックを単純化して読みやすい制御フローを意識する必要がある。
条件式
ifなどの条件式ではよく比較をする。以下のコードではどちらが読みやすいか?
if (10.24568425 <= length)
if (length >= 10.24568425)
ほどんどの場合後者の方が読みやすい。
このように、左側に変化する調査対象の式、右側にあまり変化しない比較対象の式を記載することで、コードの読み手の違和感をなくすことができる。
また、以下の式は同じものである。
// 条件式1
if (a == b) {
case1
} else {
case2
}
//条件式2
if (a != b) {
case2
} else {
case1
}
この2つの式は同じことをしているが、圧倒的に上の式の方が読みやすい。
単純で読みやすい条件式にするには優劣がある。
- 条件は否定系よりも肯定系をつかう
- 単純な条件を先に書く
- 関心を惹く条件や目立つ条件を先に書く
これらの優劣は衝突することもあるので、そのときは自分で判断して優先度を決める必要がある。
####関数から早く返す
関数で複数のreturn文を使うのに抵抗がある人がいるかもしれないが、それは大きな間違いである。
読みにくいコードにはネストが深いコードがある。このネストをいかに少なくするかでコードの読みやすさは大きく変わることが多い。関数から早めに値を返すことができれば、ネストを浅くして読みやすいコードを実現することが可能になり、可読性が向上する。
これを日頃から意識することで、コードをクリーンに保つことができる。
###コードのまとめ方について
巨大な式の分割
巨大な式というのは理解がしにくいものが多い。一度に覚えておかないといけない変数名が増えたり、何度もコードを読み直す手間もかかる。
そんな巨大なコードの塊は、分割する必要がある。
効果的な分割の方法に説明変数を導入するというものがある。説明変数は難解な式を説明する変数に置き換えるというものである。
productId = line.split((':')[0].strip()
if productId <= 1000
line.split((':')[0].strip()
が一見なにを表しているか分からないような式でも、一度productIdのような説明変数に格納することでコードの可読性が向上した。このように簡潔な名前で式を説明することで、「コードをドキュメント化」することができる。
そしてもう一つのテクニックに「ド・モルガンの法則」を用いるというものがある。
if(!( a && !b ))
という条件式がある時大抵の読み手は立ち止まって考えてしまう。
このような条件式があるときには、
notを分配して、and/orを反転する(notを括り出す)
というド・モルガンの法則を用いることで簡潔に表すことができる。つまり、
if(!( a && !b ))
→ if( !a || b )
というように表すことができるということだ。
このように難解な式や巨大な式はいかにシンプルに読みやすく書くことができるがを考える必要がある。
変数と読みやすさ
変数を何も考えないで使うとプログラムが理解しにくくなる。
- 変数が多いと変数を追跡するのが難しくなる
- 変数のスコープが大きいとスコープを把握する時間が長くなる
- 変数が頻繁に変更されると現在の値を把握するのが難しくなる
このような問題に対処するには以下のような対処が必要である。
- 邪魔な変数を削除する
- 結果をすぐに使って、中間結果を保持しないなど工夫して不必要な変数は削除する必要がある
- 変数のスコープをできるだけ小さくする
- 変数を数行のコードからしか見えない位置に移動することで一度に考えなければいけない変数を減らすことができる
- 一度だけ書き込む変数を使う
- 変数を操作する場所が増えると、現在値の判断が難しくなる。そこで、変数に一度だけ値を設定すれば、(あるいは、constやfinalなどのイミュータブルにする方法を使えば)、コードが理解しやすくなる
- どうしても変数を変更する必要がある場合は、変更する箇所をなるべく少なくすることを意識する必要がある
無関係の下位問題の抽出
無関係の下位問題を積極的に見つけて抽出するためには以下を考える必要がある。
- 関数やコードブロックを見て「このコードブロックの高レベルの目標は何か?」と自問すること
- コードの各行に対して「高レベルの目標に直接的に効果があるのか?あるいは、無関係の下位問題を解決しているのか」と自問する。
- 無関係の下位問題を解決しているコードが相当量あれば、それらを抽出して別の関数にする
ここでいう無関係の下位問題を抽出した状態というのはコードを抽出して別の関数にする際にその抽出されたコードがどのように呼び出されるのか意識しない状態であるということである。
プロジェクト固有のコードから汎用コードを抽出することでほとんどのコードは汎用化できる。一般的な問題を解決するライブラリやヘルパー関数を作っていけばプログラムに固有の小さな核だけが残る。
例えば、ディズニーランドに行った人が平均で1日何個のアトラクションに乗るのかを求めるコードを書くとする。
そうすると高レベルの目標は「ディズニーランドに行った人が平均で1日何個のアトラクションに乗るかを求める」ということになる。
そのためには
- ディズニーランドの1日の来場者の数を求める処理
- 1日でディズニーランド内のアトラクションが乗せた人の数を求める処理
- 一人当たり何個のアトラクションに乗ったかを求める処理
これらを考える必要がある。
しかし、ここで考えるべきは3つ目の一人当たり何個のアトラクションに乗ったかを求める処理であり、そのほかの2つはそれを求めるのに必要な材料でしかない。つまり、無関係の下位問題である。
どのようにデータを扱っているかによるが、ここで1日の来場者を求める処理や1日でディズニーランド内のアトラクションが乗せた人の数を求める処理が複雑になっており、その関数の可読性を下げている要因になる場合これらは抽出する必要が出てくる。
特に1日の来場者を求める処理は他の場所でも使う可能性があるため、他の場所でも使うことができる汎用的な関数になることができる。
このように無関係の下位問題を抽出することで、その関数が何をしたい関数なのかを明確に捉えることが可能になる。そして、汎用的な関数を抽出することでコードの再利用性が高まる。
####一度に一つのことを
一度に複数のことをするコードは理解しにくい。オブジェクトを生成して、データを整理して、データを加工して....と複数の「タスク」が絡み合っていると、「タスク」が個別に完結しているコードより読みにくくなる。
関数は一度に一つのことを行うべきという意見は有名かもしれないが、ここでいう一度に一つのことをというのも同じようなことである。しかし、関数に限ったことではない。一度に1つのタスクをするための手順は以下である。
- コードが行なっている「タスク」を全て列挙する。この「タスク」という言語はゆるく使っている。「オブジェクトが妥当かどうかを確認する」のような小さいことから、「ツリー全てのノードをイテレートする」のように曖昧なものもある。
- タスクをできるだけ異なる関数に分割する。
これらは、新しくコード書くときにも有効な方法だがリファクタリングに用いても効果を発揮する。複数のタスクを扱うコードを発見したときは、タスクを列挙し、異なる関数に分割できないかを考えることでコード全体がシンプルで読みやすくなる。
タスクを分割すると、それぞれのタスクに対するコードに集中できるため新たなリファクタリング方法を思いつくこともある。
まとめ
リーダブルコードでは主に変数とロジックをいかにして読みやすくするかということがまとめられており、具体的な例を用いて説明がなされていた。
その中でこれは意識するべきだと感じたものが3つある。
- ロジックの言語化
- 各ブロック、各領域でそのことだけを考えればいいようにコードを書く
- 読みやすいコードを書くことに対して妥協しない
特に3つ目の「読みやすいコードを書くことに対して妥協しない」に関しては本の中でも
理想とは程遠いインターフェースに妥協することはない
と書かれている。これは言い換えると読みやすいコードを書くことに関して妥協してはいけないということだ。読みやすいコードは自分のためにもなるが、他人がコードを理解しチーム全体の開発効率を向上させるために必要不可欠な要素だと言える。
また、これらのことを普段から「意識」することが何よりも大切である。最初は難しいかもしれないが最後には無意識下で読みやすいコードを書くことができるようになるためにも意識し、行動に移すことが重要である。