「リーダブルコード」を改めて読んでみて(その3)

「リーダブルコード」に関する記事の続きです。ライブ出演前の練習や会社の開発合宿などでばたついており、間が空いてしまいました。

今回の記事では第Ⅱ部(7-9章)の内容に触れていきます。第Ⅱ部は「ループとロジックの単純化」というテーマで、以下の3点にフォーカスしていました。

意識することが増えるとコードは読みづらくなるという旨の内容が言葉を変えて何度も登場しており、各章ではそれを減らすための方法について言及しています。


制御フローを読みやすくする方法

7章では、制御フローを読みやすくする方法について言及しています。重要だと感じたのは以下のフレーズです。

  • 条件やループなどの制御フローはできるだけ「自然」にする。
  • ネストが深くなると、読み手は「精神的スタック」に条件をプッシュしなければいけない。


項目数が多いので、個人的に新しい気付きであったり重要だと感じた部分を抜粋して書いていきます。

条件式の引数の並び順

条件式を"自然"にするための書き方は以下のようになります。

  • 左側:「調査対象」の値。変化する。
  • 右側:「比較対象」の値。あまり変化しない。


コードで表すと以下のようになります。

// 悪い例
// "If 10 is less than or equal to length."となる
if (10 <= length) { ... }

// いい例
// "If length is greater than or equal to 10."となる
if (length >= 10) { ... }


一般的な横書きの文章も「Z」の書き順の方向に読み進めていくので、言われてみれば至極当然のことだなと感じました。

ネストを浅くする

ネストの数だけ制御フローが存在するので、ネストが多いとその分の条件を意識しながらコードを読まなければなりません。
ネストを減らす有効な方法の1つに早期returnがあります。これは主に正常系でないケースを先に処理するために用いられます。「ガード節」と呼ばれたりもします。
早期returnによりネストが1つ浅くなり、意識すべき条件が1つ少なくなるので、コードが読みやすくなります。

コードで表すと以下のようになります。

// 早期returnなしの場合
fun doSomething() {
  if (condition) {
    // 何らかの処理
    ...
  }
}

// 早期returnありの場合
fun doSomething() {
  if (!condition) {
    return
  }
  // 何らかの処理
  ...
}


式を読みやすくする方法

8章では、式を読みやすくする方法について言及しています。重要だと感じたのは以下のフレーズです。

  • 式を簡単に分割するには、式を表す変数を使えばいい。
  • 式を説明する必要がない場合でも、式を変数に代入しておくと便利だ。
  • 最初は簡単な問題だったのに、驚くほど複雑なロジックのコードになってしまった。こういう場合には、もっと簡単な方法があるのだ。


8章に関しても、個人的に新しい気付きであったり重要だと感じた部分を抜粋して書いていきます。

説明変数を使う

式を表す変数のことを説明変数と言います。複雑な式を説明変数に代入することで読みやすくなります。
例として、userInfoという値からユーザ名を抽出し、rootユーザかどうか判定するケースを考えます。

// "id : userName"のデータ
val userInfo = "1 : root"

if (userInfo.split(":")[1].trim() == "root") { ... }


コメントにデータ形式を書くことでいくらか理解しやすくはなりますが、それでもif文の条件式を一目見て内容をすぐ理解するのは難しいと思います。

そこでuserNameという説明変数を用意し、以下のように書き換えてみます。

val userInfo = "1 : root"

val userName = userInfo.split(":")[1].trim()
if (userName == "root") { ... }


このように、説明変数を使うことでif文の条件式の内容が理解しやすくなりました。


要約変数を使う

大きなコードの塊を小さな名前に置き換えて、管理や把握を簡単にする変数のことを要約変数と言います。大きな式を要約変数に代入することで読みやすくなります。
例として、ユーザがドキュメントの所有者かどうか判定するケースを考えます。

if (request.user.id == document.ownerId) { ... }


さほど大きな式とは言えませんが、if文の条件式の内容を理解するには少しばかり考える時間が必要になると思います。

そこでisOwnerという要約変数を用意し、以下のように書き換えてみます。

val isOwner = request.user.id == document.ownerId
if (isOwner) { ... }


この場合も、要約変数を使うことでif文の条件式の内容が理解しやすくなりました。


複雑な条件をシンプルにする

ロジックがあまりにも複雑になっている場合、シンプルにできることが多いです。
例として、2つの数直線が重なっているかどうか判定するケースを考えます。

数直線上の範囲を表現するデータクラスは以下のようになります。
overlapsWith()はこの数直線が別の数直線と重なっているかどうか判定する関数です。

// 数直線上で[begin, end)の範囲を表すクラス
data class Range(val begin: Int, val end: Int) {
  fun overlapsWith(other: Range): Boolean { ... }
}


素直に考えると、2つの数直線が重なる条件は以下のようになります。
(めんどくさくて時間がなくて図は用意していないです…)

  1. 一方の始点が他方の数直線の範囲に含まれる
  2. 一方の終点が他方の数直線の範囲に含まれる
  3. 一方の始点・終点がともに他方の数直線の範囲に含まれる


したがって、overlapsWith()の中身は以下のように書けます。

fun overlapsWith(other: Range): Boolean {
  return (this.begin >= other.begin && this.begin < other.end) 
      || (this.end > other.begin && this.end <= other.end)
      || (this.begin <= other.begin && this.end >= other.end)
}


これでも問題があるわけではないですが、条件式が非常に複雑で、不具合が混入しやすい状態であることは確かです。
今回の場合、2つの数直線が重ならない条件を考えることで、それ以外の場合は重なっていると判定できます。

2つの数直線が重ならない条件は以下のようになります。

  1. 一方の始点が他方の数直線の終点以降にある
  2. 一方の終点が他方の数直線の始点以前にある


したがって、overlapsWith()の中身は以下のように書き換えることができます。

fun overlapsWith(other: Range): Boolean {
  if (this.begin >= other.end) return false
  if (this.end <= other.begin) return false
  return true
}


最初の条件式と比較してだいぶ読みやすくなりました。


変数を読みやすくする方法

9章では、変数を読みやすくする方法について言及しています。重要だと感じたのは以下のフレーズです。

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


9章に関しても、個人的に新しい気付きであったり重要だと感じた部分を抜粋して書いていきます。


不要な変数を削除する

ここでいう"不要な"変数は、コードが読みやすくならない変数のことを指します。
具体的には、式の意味を説明できていない一時変数や中間結果だけを保持している変数などのことです。

例として、リストの中から特定の値を先頭に近いものから1つ削除する関数を考えます。
この関数の中のremoveIndexは中間結果を保持するためだけに使われています。

val removeOne = fun(list: MutableList<Int>, removeValue: Int) {

  var removeIndex: Int? = null

  for ((index, value) in list.withIndex()) {
    if (value == removeValue) {
      removeIndex = index
      break
    }
  }
  if (removeIndex != null) {
    list.removeAt(removeIndex)
  }
}


これを以下のように書き換えることで、removeIndexを削除できます。

val removeOne = fun(list: MutableList<Int>, removeValue: Int) {
  for ((index, value) in list.withIndex()) {
    if (value == removeValue) {
      list.removeAt(index)
      break
    }
  }
}


不要な変数が削除された分、コードが読みやすくなりました。行数も削減できました。



次回の記事では第Ⅲ部(10-13章)の内容に触れていきます。第Ⅲ部は「コードの再構成」というテーマです。
ここまでの数行単位ではなく、コードのリファクタリングの話へと進んでいきます。


一連の記事はこちらです。
tkhs0604.hatenablog.com tkhs0604.hatenablog.com tkhs0604.hatenablog.com tkhs0604.hatenablog.com tkhs0604.hatenablog.com tkhs0604.hatenablog.com