可読性向上の必勝パターンは存在するのか

ソフトウェア開発において、開発規模が大きくなるほど「読む」ことに割くコストも大きくなります。そのため、読むことの難度がそのまま開発の難度に直結するといっても過言ではありません。

可読性の向上において一番大切なことは、具体的な指標や手法そのものではないと私は考えます。1つの視点や手法にとらわれると、かえって可読性を悪化させることもあるからです。この誌面では「早期リターン」⁠return early/early return)を用いて、そのことを解説します。

強力なテクニック —⁠—早期リターン—⁠—

「早期リターン」は、関数の動作を分かりやすくするテクニックの1つです。このテクニックを用いるには、まず、関数の動作を以下の2つに分ける必要があります。

  • ハッピーパス 関数の主な目的を達成できるケース
  • アンハッピーパス エラーなどで、主な目的を達成できないケース

ハッピーパスの処理が関数中のどこにあるかが分かりやすいと、関数の動作も分かりやすくなります。これを、以下の2つのコードで比較して確認しましょう。

コード1 早期リターンを使う場合
fun someFunction() {
    if (!isNetworkAvailable()) {
        showNetworkUnavailableDialog()
        return
    }
    val queryResult = queryToServer()
    if (!queryResult.isValid) {
        showInvalidResponseDialog()
        return
    }

    ... // ハッピーパスの実装
}
コード2 早期リターンを使わない場合
fun someFunction() {
    if (isNetworkAvailable()) {
        val queryResult = queryToServer()
        if (queryResult.isValid) {
            ... // ハッピーパスの実装
        } else {
            showInvalidResponseDialog()
        }
    } else {
        showNetworkUnavailableDialog()
    }
}

まずは、ハッピーパスに着目してみましょう。コード2ではifによるネストの深い場所にハッピーパスがあるため、どの分岐がハッピーパスなのかを確認するには、しっかりとコードを読む必要が出てきます。一方でコード1は、早期リターンの部分がアンハッピーパスの処理であることが明確なため、その下がハッピーパスの処理だということが分かりやすいです。

また、アンハッピーパスに着目した場合も早期リターンは有効です。やはりコード2の方は、アンハッピーパスとそれに対応する処理の関連性が分かりにくくなっています。その理由は、アンハッピーパスの条件と、その処理を行うコードが離れていることにあります。⁠ネットワークが使えないisNetworkAvailablefalseである⁠⁠」ことに対応する処理は、長いifのボディの下にあるelseを辿ってみるまで分かりません。コード1ではアンハッピーパスと対応する処理が近くにあるため、⁠ダイアログが表示されたときに、その条件を調べる」ことは簡単です。

このように、早期リターンはコードの可読性を向上させる上で役立つ強力なテクニックです。ハッピーパスが大きなコードになる場合や、アンハッピーパスが複数ある場合は特に有効でしょう。ここまでは様々な解説によって、広く知られていることです。

アンハッピーを取り除けばハッピーか?

強力なテクニックであるはずの「早期リターン」も、使い方を誤ると可読性を下げかねません。ここでは、2つのアンチパターンを紹介します。

アンチパターン1:分かりにくい場所からのリターン

制御構造のネストの深い場所で早期リターンを行ったり、whenswitchの条件分岐の一部だけでリターンしたりすると、早期リターンしていることそのものを見落としがちです。結果として、⁠ハッピーパスを通らないケースがある」ことが分からないままその処理を更新し、バグを埋め込むこともあるでしょう。コード3では、早期リターンがwhenの一部に入ってしまっているため、条件分岐を斜め読みした時に見落とされる可能性があります。

コード3 条件分岐内に存在する早期リターン
enum class ThemeType { LIGHT, DARK, INVALID }

fun setThemeBackgroundColor(themeType: ThemeType) {
    val argbColor = when (themeType) {
        ThemeType.LIGHT -> WHITE_ARGB_COLOR
        ThemeType.DARK -> BLACK_ARGB_COLOR
        ThemeType.INVALID -> return
                    // このreturnは見落とされやすい。

    }

    someView.setBackgroundColor(argbColor)
    anotherView.setBackgroundColor(argbColor)
    yetAnotherView.setBackgroundColor(argbColor)
}

コード3では、themeTypeINVALIDの場合においてのみ、早期リターンを行っています。このぐらいの複雑さならば、returnを見落とすことは少ないかもしれません。しかし、今後の仕様変更でアンハッピーパスが増えたり、制御構造のネストが加えられたりすると、returnを見落としやすいコードになってしまうでしょう。

早期リターンを行っているかどうかは、関数の処理の流れにおいて重要であるため、returnの部分を目立たせるべきです。方法はいくつか考えられますが、もしコード3ThemeTypeが変更可能なら、次のように改善できます。

ThemeTypeからINVALIDを削除する関数の引数として、INVALIDの代わりにnullで受け取るとコード4のように、ハッピーパスとアンハッピーパスが明示的に分離された関数が作れます。ここで、ThemeType?は、nullを許容するThemeTypeの型という意味です。

コード4 ハッピーパスとアンハッピーパスの分離
enum class ThemeType { LIGHT, DARK }

fun setThemeBackgroundColor(themeType: ThemeType?) {
    if (themeType == null) {
        return
    }

    val argbColor = when (themeType) {
        ThemeType.LIGHT -> WHITE_ARGB_COLOR
        ThemeType.DARK -> BLACK_ARGB_COLOR
    }

    ...
}

このように、ハッピーパスとアンハッピーパスの条件で型を分けることで、ネストの深い位置や条件分岐の一部からのreturnをうまく解消できることがあります。

アンチパターン2:不要なアンハッピーパス

もう1つのアンチパターンとして、早期リターンを適用することに囚われた結果、本来不要なアンハッピーパスを作ってしまうことが挙げられます。アンハッピーパスを「特殊な」ハッピーパスとしてみなすことで、早期リターンそのものが不要になるなら、そちらの方が好ましいでしょう。例えば、Listの各要素に対して処理をするmapforEachといった関数は、空のリストに対する呼び出しでも有効です。mapforEachを使う多くの場合、if (list.isEmpty()) returnといった早期リターンは不要でしょう。⁠空のリスト」をアンハッピーパスとして取り扱うのではなく、あくまでも「特殊な」ハッピーパスとして取り扱うことで、関数をより単純かつ読みやすくすることができます。

コード3コード4では、INVALIDはあくまでもアンハッピーパスとして扱っていました。もし、INVALIDLIGHTにフォールバックするという仕様であるならば、コード5のようにINVALIDもハッピーパスとして取り扱うことができます。

コード5 ⁠特殊な」ハッピーパスを使った早期リターンの削除
fun setThemeBackgroundColor(themeType: ThemeType) {
    val argbColor = when (themeType) {
    ThemeType.DARK -> BLACK_ARGB_COLOR
    ThemeType.LIGHT, ThemeType.INVALID ->
        WHITE_ARGB_COLOR
    }

    ...
}

今実装している機能の価値に大きく影響しないならば、仕様を変更してアンハッピーパスを「特殊な」ハッピーパスとすることで、早期リターンそのものを削除することも選択肢の1つです。

プログラミング原則やテクニックに対する考え方

「早期リターン」のように、非常に強力でよく知られたテクニックであっても、使い方を誤ると可読性を下げてしまうことがお分かりいただけたと思います。一般に「よい」とされるプログラミング原則やテクニックも、今、目の前にあるコードに適用するかについては慎重に考える必要があります。また、原則やテクニックが有効であるかは、ソフトウェアの分野・種類やプログラミングパラダイムに応じても変わるでしょう。本当に大切なのは、いかに多くのテクニックを知っているかではなく、そのテクニックがどんな場面で有効かを理解し、適切に使用できるかです。


書籍読みやすいコードのガイドライン -持続可能なソフトウェア開発のためにでは、可読性を向上させるための様々な原則やテクニックを紹介するだけでなく、それらがどういう時に・どのような理屈で有効なのかを解説しています。原則やテクニックの表面的な把握ではなく、読みやすさの本質を体系的に理解する足がかりとして、本書がお役に立てれば幸いです。

石川宗寿(いしかわむねとし)

LINE株式会社のシニアソフトウェアエンジニアとして,コミュニケーションアプリ"LINE"のAndroid版の開発に従事。"LINE"のソースコードの可読性向上のため,自らリファクタリング・コードレビューをする他,可読性にかかわる開発文化や基盤の構築,教育・採用プロセスの改善なども行う。