LINEヤフー Tech Blog

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

コード品質向上のテクニック: 第 24 回(遺産の価値)

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

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

遺産の価値

ボタンやテキストなどの UI 要素を持つ FooScreen というクラスがあり、その UI 要素の色や画像は「テーマ」によって変わるとします。この仕様を実現するために、以下の実装では FooScreenThemeStrategy というインターフェースを定義しています。

class FooScreen {
    private val okButton: Button = ...
    private val cancelButton: Button = ...
    private val mainDescription: TextView = ...
    ...

    fun bindTheme(strategy: FooScreenThemeStrategy) {
        okButton.bindTheme(strategy)
        cancelButton.bindTheme(strategy)

        mainDescription.textColor = strategy.textColorInt
        ...
    }

    companion object {
        private fun Button.bindTheme(strategy: FooScreenThemeStrategy) {
            buttonColor = strategy.buttonColorInt
            textColor = strategy.textColorInt
        }
    }
}

interface FooScreenThemeStrategy {
    val backgroundColorInt: Color
    val textColorInt: Color
    val buttonColorInt: Color

    val headerIcon: Image
    val errorIcon: Image
    val checkMarkIcon: Image
}

インターフェース FooScreenThemeStrategy は、テーマとして利用可能な要素を定義しているだけで、実際の値は以下のように継承によって決められています。

class FooScreenLightTheme : FooScreenThemeStrategy {
    override val backgroundColorInt: Color = Color.WHITE
    override val textColorInt: Color = Color.DARK_GRAY
    override val buttonColorInt: Color = Color.LIGHT_GREEN

    override val headerIcon: Image = GREEN_HEADER_ICON
    override val errorIcon: Image = RED_ERROR_ICON
    override val checkMarkIcon: Image = GREEN_CHECK_MARK_ICON
}

class FooScreenDarkTheme : FooScreenThemeStrategy {
    override val backgroundColorInt: Color = Color.BLACK
    override val textColorInt: Color = Color.OFF_WHITE
    override val buttonColorInt: Color = Color.DARK_GRAY

    override val headerIcon: Image = OFF_WHITE_HEADER_ICON
    override val errorIcon: Image = OFF_WHITE_ERROR_ICON
    override val checkMarkIcon: Image = OFF_WHITE_CHECK_MARK_ICON
}

このコードに改善できる部分は何かありますか?

相続しないという選択

多くの場合、値が違うだけでロジックが同じときは、継承は必要ありません

継承を必要とするケースの典型例としては、以下のようなものが挙げられます。

  • ロジックを動的に変える (動的ディスパッチの利用)
  • 直和型を実現する (Java や Kotlin の sealed class など)
  • 実装とインターフェースを分離する (フレームワークの制約への対応、DI コンテナの利用、ビルドの高速化など)
  • 依存性逆転の法則を適用する (循環依存の解決、依存の一方向化)

これら以外の目的に対しては、継承を使わない方が簡潔な実装にできることが多いです。例えば、単なるロジックの共通化であれば集約やコンポジションを使うこともできます。

FooScreenThemeStrategy が必要としているのは、「ロジックを動的に変える」に近いです。しかし実際には、ロジックを変える必要はなく、値を変えるだけでも十分でした。今回の場合は、インターフェースを使うかわりに値を持つクラスを定義することで、継承を使わずに実装できます。

class FooScreenThemeModel(
    val backgroundColorInt: Color,
    val textColorInt: Color,
    val buttonColorInt: Color,
    val headerIcon: Image,
    val errorIcon: Image,
    val checkMarkIcon: Image
)

val FOO_SCREEN_LIGHT_THEME_MODEL = FooScreenThemeModel(
    backgroundColorInt = Color.WHITE,
    textColorInt = Color.DARK_GRAY,
    buttonColorInt = Color.LIGHT_GREEN,
    headerIcon = GREEN_HEADER_ICON,
    errorIcon = RED_ERROR_ICON,
    checkMarkIcon = GREEN_CHECK_MARK_ICON,
)

特に Kotlin の場合は open などをつけない限り、class の継承はできなくなります。(Java で言うところの final class になります。) FooScreenThemeModel をインターフェースではなく継承不能なクラスとして定義することで、次の 2 つのことを保証できます:

  1. (プロパティの値が不変ならば) 各プロパティは動的に変更されない
  2. インスタンス固有の (特異の) ロジックはない

一言まとめ: 値が違うだけでロジックが同じときは、継承を使わずに、値の異なるインスタンスを作る。

キーワード: inheritance, instantiation, dynamic dispatch