Kotlin で “Thin Cake” パターンを真似る

@nobuoka / id:nobuoka
Kyobashi.dex #4

こんにちは!

  • @nobuoka です
    • Java サーバーサイド / web フロントエンド / Android アプリ
    • Kotlin 1.1 気になりますね!
  • 今日は Kotlin でのお手製 DI
    • “Thin Cake” パターンについて
    • Kotlin での疑似モジュールミックスイン合成
    • 実用的というより興味本位なので気軽に聞いてください

Dependency Injection (DI)

  • 依存オブジェクトを注入するパターン
  • Android アプリ開発でもよく使われる
  • 様々な手法
    • Guice のような DI コンテナ、Dagger のようなライブラリ
    • Cake パターンのようなパターン ← 今日はココ
    • コンストラクタで依存オブジェクトを注入 (手動で単純)

Cake パターン

Cake パターン

  • Scala におけるお手製 DI のパターン
  • Odersky 先生らの論文発祥
  • コンポーネント再利用のための 3 つの抽象化
    • 抽象型メンバー
    • 自分型アノテーション
    • モジュールミックスイン合成
(「Scalable Component Abstractions」 より)

モジュールの合成

  • Trait (実装を持てるインターフェイスぽいもの) をモジュールとして使う
  • モジュール
    • 何らかの機能 (オブジェクト) を提供
    • 別のモジュールのオブジェクトに依存
      • 依存先オブジェクトの詳細を知らない
  • モジュールの組み合わせ → オブジェクトグラフ

“Thin Cake” パターン

モジュール

trait FooModule {
  // This module provides:
  lazy val foo = "foo"
  lazy val fooBar = s"($foo+$bar)"
  lazy val fooBarBaz = s"($fooBar+$barBaz+$bazFoo)"
  
  // This module depends on:
  def bar: String
  def barBaz: String
  def bazFoo: String
}

オブジェクトグラフ

object FooBarBazComponent
    extends FooModule with BarModule with BazModule {
  val baz = "baz"
}
https://scalafiddle.io/sf/wXq8fOM/22

Kotlin で “Thin Cake” パターン

インターフェイスのデフォルト実装!

モジュール

interface FooModule {
  // This module provides:
  val foo get() = "foo" // Kotlin 1.1
  val fooBar get() = "(foo+$bar)"
  val fooBarBaz get() = "($fooBar+$barBaz+$bazFoo)"
  
  // This module depends on:
  val bar: String
  val barBaz: String
  val bazFoo: String
}

オブジェクトグラフ

object FooBarBazComponent : FooModule, BarModule, BazModule {
  override val baz = "baz"
}
http://try.kotlinlang.org/#/UserProjects/lhd18hmac9csqh32i9u5mjhmd7/6dh5fg7gqkp4tkkup3sp6vl9bh

が…… 駄目っ……!

Object 'FooBarBazComponent' must override public open val foo: String defined in FooModule because it inherits multiple interface methods of it
  • 継承先の複数のインターフェイスに同じシグネチャ
    • デフォルト実装はやっぱりデフォルト実装
  • 解決はできる : Kotlin でも Minimal Cake Pattern
  • そもそもデフォルト実装では値を保持できない問題

デリゲーションなら?

class ObjectGraph() :
    FooModule by FooModuleImpl(),
    BarModule by BarModuleImpl(),
    BazModule by BazModuleImpl()

// これではだめ。
// なぜなら FooModuleImpl が BarModule によって提供される機能に
// 依存したくでもできない (参照できない) ため。
  • 2 つの問題
    • モジュールの実装がオブジェクトグラフを参照できない
    • XxxModule インターフェイスにモジュールの依存も宣言されている

依存を分離し、依存をパラメータに

interface FooModule {
  // This module provides:
  val foo: String
  // ... (略) ...

  // This module depends on:
  interface Dependencies {
    val bar: String
    // ... (略) ...
  }

  class Impl(private val d: Lazy<Dependencies>) : FooModule {
    override val foo get() = "foo"
    // ... (略) ...
  }
}
http://try.kotlinlang.org/#/UserProjects/lhd18hmac9csqh32i9u5mjhmd7/bhtihkpmr4gvil7ib0o8qcmu3v

オブジェクトグラフ

interface FooBarBazComponent {
  interface Dependencies {
    val baz: String
  }

  class ObjectGraph(self: Lazy<ObjectGraph>, d: Dependencies) :
      Dependencies by d,
      FooModule by FooModule.Impl(self), FooModule.Dependencies,
      BarModule by BarModule.Impl(self), BarModule.Dependencies,
      BazModule by BazModule.Impl(self), BazModule.Dependencies

  companion object {
    fun create(d: Dependencies): ObjectGraph =
        AtomicReference<ObjectGraph>().let { r ->
            ObjectGraph(lazy { r.get() }, d).apply { r.set(this) } }
  }
}
http://try.kotlinlang.org/#/UserProjects/lhd18hmac9csqh32i9u5mjhmd7/bhtihkpmr4gvil7ib0o8qcmu3v

疑似的に “Thin Cake” パターンを実現できた

  • 疑似的にモジュールミックスイン合成を実現
    • Kotlin のデリゲーション
    • Lazy によりデリゲーション先オブジェクトに自分自身を渡す (無理やり感)

改善

XxxProvider というインターフェイスを経由して依存関係を記述

interface FooProvider {
  val foo: String
}
// ... こんな感じで ...

interface FooModule :
    // 提供するオブジェクト一覧
    FooProvider, FooBarProvider, FooBarBazProvider {

  interface Dependencies :
      // 依存するオブジェクト一覧
      BarProvider, BazBarProvider

  class Impl(private val d: Lazy<Dependencies>) : FooModule {
    // ... 実装 ...
  }
}
http://try.kotlinlang.org/#/UserProjects/lhd18hmac9csqh32i9u5mjhmd7/95ntg0h5b3usm2qp0afp4ibpja

インジェクトされるオブジェクトのコンストラクタ

class Hoge(d: Dependencies) {
  interface Dependencies :
      FooProvider

  fun useFoo() {
    // d.foo を使う
    d.foo + "bar"
  }
}

// そうするとモジュール側の実装は単純になる
module HogeModule : HogeProvider {
  /* 略 */
  class Impl(private val d: Lazy<Dependencies>) : HogeModule {
    override val hoge by lazy { Hoge(d.value) }
  }
}
http://try.kotlinlang.org/#/UserProjects/lhd18hmac9csqh32i9u5mjhmd7/95ntg0h5b3usm2qp0afp4ibpja

おわり

  • 疑似モジュールミックスイン合成により疑似 “Thin Cake” パターンを実現
  • まだ模索中
  • Android のサンプルプロジェクト : https://github.com/nobuoka/android-pseudo-thin-cake-pattern-sample
  • 記述量は多くなるが依存関係は追いやすいはず
  • Lazy でデリゲーション先に自分を渡すとか、依存オブジェクトをインターフェイスにまとめるとかは使えそう
  • Kotlin に Intersection Types が欲しい
    • Dependencies の定義などに使えそう