Upgrade to Pro — share decks privately, control downloads, hide ads and more …

data class と継承の組み合わせによる問題と、その解決法

data class と継承の組み合わせによる問題と、その解決法

Avatar for Tomoyuki TAKEZAKI

Tomoyuki TAKEZAKI

April 20, 2025
Tweet

More Decks by Tomoyuki TAKEZAKI

Other Decks in Programming

Transcript

  1. 今日の話の要約 状況設定 data class と実装の継承を組み合わせて利用するケースを考える。 問題 data class の copy

    メソッドは、基底クラスのインスタンスを保たない。 原因 基底クラスは data class のコンストラクタに含まれておらず、 copy メソッドは基 底クラスの情報を引き継がない。 そのため、copy により基底クラスの状態は初期化されてしまう。 解決策 委譲パターンを利用し、copy メソッドで基底クラスの情報を引き継がせる。 3
  2. data class とは data class キーワードで、プライマリコンストラクタや copy メソッドなどをもつクラス を簡単に定義できる。 data

    class User(val name: String, val isActive: Boolean) fun main() { // 以降では main 関数を省略する // プライマリコンストラクタを使ってインスタンスを生成する val user1 = User(name="John", isActive = true) // data class には `toString` メソッドが自動生成される。 println(user1.toString()) // User(name="John", isActive=true) // copy メソッドにより、異なる値を持つ新しいインスタンスを生成することができる。 val user2 = user1.copy(isActive = false) } 4
  3. 以降の説明で利用するモデル 実際に遭遇した問題を、簡素化したモデルで説明する。エンティティであるユーザー情報 User と、データベースで共通に保存するメタデータ Metadata を考える。 data class User(val name:

    String, val isActive: Boolean) : Metadata() open class Metadata(var version: Int = 0) Metadata の使い方は次の通り。 val user1 = User(name="John", isActive = true) println("user1.version: ${user1.version}") // user1.version: 0 user1.version = user1.version + 1 println("user1.version: ${user1.version}") // user1.version: 1 5
  4. copy メソッドは基底クラスの情報を保持しない しかし、この version は copy によって引き継がれず、リセットされる! val user1 =

    User(name="John", isActive = true) user1.version = user1.version + 1 println("user1.version: ${user1.version}") // user1.version: 1 val user2 = user1.copy(isActive = false) println("user2.version: ${user2.version}") // user2.version: 0 これは、 data class およびその copy メソッドは、基底クラスの知識を持たないためである。 委譲という実装パターンは、この問題を解消するのに適している。 Kotlin における委譲の書 き方を見ていく。 6
  5. Kotlin における委譲の書き方 interface Base { fun print() } class BaseImpl(val

    x: Int) : Base { override fun print() { print(x) } } // Derived は Base 型だが、その機能は b が担うことを示している class Derived(b: Base) : Base by b fun main() { val base = BaseImpl(10) val derived = Derived(base) // print メソッドの呼び出しは、実際には Base 型の base に転送される derived.print() // 10 } 7
  6. 修正したモデル 委譲を利用して修正したモデルは次の通り。 data class User( val name: String, val isActive:

    Boolean, private val metadata: Metadata = Metadata.newInstance() ): Metadata by metadata interface Metadata { var version: Int companion object { fun newInstance(): Metadata = MetadataImpl() } // 呼び出し側に Metadata の実装を意識させないために隠蔽している private class MetadataImpl(override var version: Int = 0): Metadata } 8
  7. 修正後の挙動 次の通り、 version が copy によって引き継がれている。 呼び出し側のコードに変更は不要。つまり、既存のコードに与える影響を最小化しつつ 改善ができた。 val user1

    = User(name="John", isActive = true) println("user1.version: ${user1.version}") // user1.version: 0 user1.version = user1.version + 1 val user2 = user1.copy(isActive = false) println("user2.version: ${user2.version}") // user2.version: 1 9
  8. まとめ data class と実装の継承を組み合わせると、 copy で基底クラスのインスタンスが作り直 しとなり情報が失われるケースがある。 data class に共通の実装を持たせつつ上の問題を回避する方法の一つとして、委譲とい

    う手法を紹介した。 今後の展望 業務では、JPA およびその実装である Hibernate を利用している。今回の提案が Hibernate と組み合わせて動かすことができるかを検証する必要がある。 10
  9. 参考資料とコメント Kotlin 言語公式サイト https://kotlinlang.org/docs/data-classes.html https://kotlinlang.org/docs/delegation.html p7 のサンプルコードはこちらを参考にしている Jemerov, D., Isakova,

    S. (2017). Kotlin in Action. United States: Manning. Kotlin の言語機能について一通り学ぶことができる Bloch, J. (2008). Effective Java. United Kingdom: Pearson Education. 今回の話題に関連する item に Favour composition over inheritance がある Java の本だが、Kotlin の場合にどう書くか?という観点で読むのも面白い 11