こんにちは。コミュニケーションアプリ「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 つのことを保証できます:
- (プロパティの値が不変ならば) 各プロパティは動的に変更されない
- インスタンス固有の (特異の) ロジックはない
一言まとめ
値が違うだけでロジックが同じときは、継承を使わずに、値の異なるインスタンスを作る。
キーワード: inheritance
, instantiation
, dynamic dispatch