LINEヤフー Tech Blog

LINEヤフー株式会社のサービスを支える、技術・開発文化を発信しています。

コード品質向上のテクニック:第32回 奈落からの帰還

こんにちは。コミュニケーションアプリ「LINE」のモバイルクライアントを開発している石川です。

この記事は、毎週木曜の定期連載 “Weekly Report” 共有の第 32 回です。Weekly Report については、第 1 回の記事を参照してください。

奈落からの帰還

早期リターンは、関数の主な目的を達成するケース (ハッピーパス) と達成できないケース (アンハッピーパス) を分離し、関数の流れを分かりやすくするために有効なテクニックです。ただし、早期リターンをどのように適用するかは注意深く考える必要があります。

以下のコードでは、 messageData.mainMimeType が想定外のケースと messageData.content の保存が失敗したケースで return していますが、逆に読みにくくしてしまっています。どのように改善したら良いでしょうか。

fun ...(...) {
    try {
        val messageType = if (messageData.isParsed) {
            messageData.messageType
        } else {
            when (messageData.mainMimeType) {
                "image" -> MessageType.IMAGE
                "video" -> MessageType.VIDEO
                else -> return false
            }
        }

        val saveResult = saveContent(messageData.content, messageType) // IOException を投げる事がある
        if (!saveResult.isSuccessful) {
            return false
        }
        return true
    } catch (_: IOException) {
        return false
    }
}

分かりやすい帰り道を作る

先程の関数の return が分かりにくいのには、以下の 2 つの理由があります。

  1. ネストの深い場所や、分岐の一部からの return がある (例: 最初の return false は、tryifwhen の内側にあり、かつ、そこに到達する条件がひと目ではわかりにくい)
  2. return が、ハッピーパスとアンハッピーパスの分離にあまり寄与していない (例: !saveResult.isSuccessfulreturn false には後続の動作がない)

早期リターンを使う場合、return を目立つ場所に置き、条件をわかりやすくし、ハッピーパスが目立つようにする ことを心がける必要があります。

例えば以下のようにすると、「早期リターンが起きるケースは、messageType が null になるとき」ということがわかります。また、最後の行を見ると、メッセージの保存がうまく行ったときに true が返ってくるということもわかりやすくなっています。

fun ...(...) {
    val messageType = messageData.messageType.takeIf { messageData.isParsed }
        ?: MAIN_MIME_TO_TYPE_MAP[messageData.mainMimeType]
        ?: return false

    val saveResult = try {
        saveContent(messageData.content, messageType)
    } catch (_: IOException) {
        SaveResult.Error()
    }
    return saveResult.isSuccessful
}

companion object {
    val MAIN_MIME_TO_TYPE_MAP: Map<String, MessageType> = mapOf(
        "image" to MessageType.IMAGE,
        "video" to MessageType.VIDEO
    )
}

特に、whenswitch といった多分岐を使うときに、一部の条件だけで return するコードを書いてしまうと、早期リターンそのものが見落とされかねません。例えば、以下の FooBar に変換するコードでは、INVALID のケースの return に気づかないかもしれません。

fun ...(foo: Foo) {
    val bar = when (foo) {
        Foo.FIRST -> Bar.FIRST
        Foo.SECOND -> Bar.SECOND
        Foo.THIRD -> Bar.THIRD
        Foo.INVALID -> return
    }

    ...
}

この問題は、多分岐と早期リターンを分離することで解決できます。以下の改善案では、Foo から Bar に変換するコードを関数として抽出しています。

fun ...(foo: Foo) {
    val bar = toBar(foo)
        ?: return

    ...
}

private fun toBar(foo: Foo): Bar? = when (foo) {
    Foo.FIRST -> Bar.FIRST
    Foo.SECOND -> Bar.SECOND
    Foo.THIRD -> Bar.THIRD
    Foo.INVALID -> null
}

こうすることで、「FooBar に変換できないケースで早期リターンする」ということを強調できます。

なお、早期リターンについては、読みやすいコードの書き方のプレゼンテーション (slide=39 から slide=48) や、書籍 読みやすいコードのガイドライン の 5 章でも解説しています。併せてご参照ください。

一言まとめ

早期リターンを使う場合は、その存在と条件、およびハッピーパスを目立たせる。

キーワード: early return, conditional branch, function flow

コード品質向上のテクニックの他の記事を読む

コード品質向上のテクニックの記事一覧