LINEヤフー Tech Blog

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

コード品質向上のテクニック:第35回 引数の持ち腐れ

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

この記事は、毎週木曜の定期連載 "Weekly Report" 共有の第 35 回です。 LINEヤフー社内には、高い開発生産性を維持するための Review Committee という活動があります。ここで集まった知見を、Weekly Report と称して毎週社内に共有しており、その一部を本ブログ上でも公開しています。(Weekly Report の詳細については、過去の記事一覧を参照してください)

引数の持ち腐れ

ある UI 要素の背景を動的に変更することを想定しましょう。以下の setShowcaseBackground 関数は、背景の色や画像を示す backgroundType を引数に取ります。BackgroundType.IMAGE が与えられた場合、もう一つの引数 imageUrl で指定された画像を背景として設定します。

enum class BackgroundType { MIDNIGHT, SKY_BLUE, MINT, PEARL, IMAGE }

class ShowcaseLayout(...) {
    private val showcase: Showcase = ...

    fun setShowcaseBackground(backgroundType: BackgroundType, imageUrl: Url?) {
        val colorInt = backgroundType.colorInt
        showcase.backgroundImage = colorInt?.let(Image::ofColor)
            ?: imageUrl?.let(::fetchImage)
            ?: ERROR_IMAGE
    }

    private fun fetchImage(imageUrl: Url): Image? { ... }

    companion object {
        /** An image displayed when image fetching fails.  */
        private val ERROR_IMAGE: Image = ...

        private val BackgroundType.colorInt: Int?
            get() = when (this) {
                BackgroundType.MIDNIGHT -> ...
                BackgroundType.SKY_BLUE -> ...
                BackgroundType.MINT -> ...
                BackgroundType.PEARL -> ...
                BackgroundType.IMAGE -> null
            }
    }
}

このコードに問題点はなにかありますか?

すべての引数を使い切る

このコードの問題は、一部の引数が、条件によっては使われない ことです。imageUrl の値は backgroundTypeIMAGE のときだけ有効で、それ以外では意味を成しません。これは、以下のような誤解を招くコードの原因になります。

setShowcaseBackground(BackgroundType.MIDNIGHT, iconImageUrl)

このコードから予想される動作は、「iconImageUrlMIDNIGHT のカラーフィルタをかけて表示する」や「MIDNIGHT の単色背景の上に iconImageUrl を表示する」というものです。しかし実際には iconImageUrl が無視されるという、コードを読んだ開発者の予想外の動作になってしまいます。

更に、ERROR_IMAGE のドキュメンテーションが実際の挙動と異なるのも問題です。ERROR_IMAGEfetchImage が失敗したときに表示される画像と説明されているのですが、実際には実引数の組み合わせが不正な (backgroundTypeIMAGEimageUrlnull) ときも表示されます。

この問題を解決する方法としては、以下の 2 つが挙げられます。

  1. 関数を分割する
  2. 引数の型を再構成する

Option 1: 関数を分割する

条件ごとに関数を分けることで、不要な引数を削除できます。以下のコードでは、色と画像で関数を分けています。

enum class BackgroundColorType { MIDNIGHT, SKY_BLUE, MINT, PEARL }

    ...
    fun setShowcaseBackgroundColor(colorType: BackgroundColorType) {
        showcase.backgroundImage = Image.ofColor(colorType.colorInt)
    }
    
    fun setShowcaseBackgroundImage(imageUrl: Url) {
        showcase.backgroundImage = fetchImage(imageUrl) ?: ERROR_IMAGE
    }

関数を分けた結果、共通するロジックがコピーされてしまった場合は、以下のような改善できます。

  • 共通部分を private 関数などで抽出する
  • 一方の関数がもう一方を呼び出すように変更する

この「関数を分割する」方法は、特に「どちらの条件になるかが、呼び出し元によって静的に決まる」場合に有効です。逆を言えば、条件が動的に決まる状況で関数を分割すると、その条件分岐のコードが関数の呼び出し元側でコピーされてしまう可能性があります。その場合は、次の「引数の型を再構成する」手段を試みてください。

Option 2: 引数の型を再構成する

条件によって持ちうる値が異なる場合、条件ごとに型を分け、その直和型を引数として取る方法もあります。以下のコードでは、色と画像で型を分け、その直和 BackgroundType を引数として与えています。こうすることで、色を指定しつつ imageUrl を渡すということは不可能になります。

sealed interface BackgroundType {
    class SingleColor(val colorType: ColorType): BackgroundType
    class Image(val imageUrl: Url): BackgroundType

    enum class ColorType { MIDNIGHT, SKY_BLUE, MINT, PEARL }
}

    ...
    fun setShowcaseBackground(backgroundType: BackgroundType)

ただし、直和型は時として強力すぎる場合もある点に注意してください。呼び出し先で型を区別する必要がない場合は、1つの型にまとめられるケースもあります。BackgroundType の例では、画像の取得を呼び出し元で実行して良いならば、以下のように一つのクラスにまとめられます。

class ShowcaseBackground private constructor(val image: Image) {
    companion object {
        val MIDNIGHT = fromColor(...)
        val SKY_BLUE = fromColor(...)
        val MINT = fromColor(...)
        val PEARL = fromColor(...)

        private val ERROR_IMAGE: Image = ...

        fun fromImageUrl(url: Url): ShowcaseBackground {
            val image = fetchImage(url) ?: ERROR_IMAGE
            return ShowcaseBackground(image)
        }

        private fun fromColor(colorInt: Int): ShowcaseBackground =
            ...
    }
}

一言まとめ

一部の条件でのみ使われる引数を作らないようにする。

キーワード: argument, orthogonal relationship, sum type

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

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