抽象クラス、インターフェース、Kotlinの記法

抽象クラスは未定義の抽象メソッドがあるクラスで、派生クラスで実装を行うことで、インスタンス化が可能になる。

abstract class AC protected constructor() {
  abstract protected fun amX(): Int
  abstract protected fun amY(): String
  fun cmZ() = "${this.amX()} ${this.amY()}"
}

val ac = object : AC() {
  override fun amX() = 42
  override fun amY() = "abc"
}

この種類の継承は、委譲に置き換えることができる場合がある。抽象メソッドを抜き出してインターフェース化し、そのインターフェースをコンストラクタにとるクラスで具体メソッドを実装する。

interface IC {
  fun amX(): Int
  fun amY(): String
}

class CC(val ic: IC) {
  fun cmZ() = "${ic.amX()} ${ic.amY()}"
}

val cc = CC(object : IC {
  override fun amX() = 42
  override fun amY() = "abc"
})

抽象メソッドを実装する具体メソッドから、スーパークラスの他のメソッドを呼び出すケースでは、こういう種類の変換はできないが、そういう、親クラスのメソッドが子クラスのメソッドに依存しつつ、子クラスのメソッドが親クラスのメソッドに依存するという、相互依存の関係になっているケースは、多くの場合は必要がないし、避けられるなら避けたいケースでもあるので、積極的に継承から委譲へ置き換える動機がある。

しかしながら問題として、継承から委譲へ置き換えたときには、余分な識別子が増えてしまっている。もとのコードでは抽象クラスACだけを定義すればよかったところが、新しいコードでは、インターフェースのICとクラスのCCに分かれてしまっている。もちろん、クラスの内部でインターフェースを定義することで、名前空間に散らからないよう工夫することはできる。

class CC(val ic: IC) {
  interface IC {
    fun amX(): Int
    fun amY(): String
  }
  fun cmZ() = "${ic.amX()} ${ic.amY()}"
}

Kotlinの記法上の問題として、匿名オブジェクトの記法がラムダ式に比べてはるかに不格好という点がある。もし、インターフェースのメソッドが1つだけなら、これは関数インターフェースとすることができる。

class CC1(val ic: IC) {
  fun interface IC {
    fun amX(): Int
  }
  fun cmZ() = "${ic.amX()}"
}

val cc1 = CC1 { 42 }

メソッドが1つの場合と2つの場合で記法上の相違がここまで大きいのはわりと理不尽で、記法上の制約が設計上不自然な選択を取らせる可能性がある。たとえば次のように、関数を複数コンストラクタで取るような形に変えるという手法で、記法の煩雑さを迂回するかもしれない。

class CC2(val amX: () -> Int, val amY: () -> String) {
  fun cmZ() = "${amX()} ${amY()}"
}

val cc2 = CC2(amX = { 42 }, amY = { "abc" })

この方法は、もとの記法より簡潔だが、問題として、評価のタイミングがある。コンストラクタの引数は、呼び出し時点で評価されてしまうから、関数ではなく即値で渡すようなシグネチャにしてしまう可能性がある。

class CC3(val pX: Int, val pY: String) {
  fun cmZ() = "${pX} ${pY}"
}

val cc3 = CC3(pX = 42, pY = "abc")

こう定義してしまうと、あとからゲッターメソッドの実装を変えて、評価タイミングをコントロールすることが厳しくなるから、やはり適切にインターフェースを使うことが望ましい場面も存在するように思われる。

class CC4(val ic: IC) {
  interface IC {
    val pX: Int
    val pY: String
  }
  fun cmZ() = "${ic.pX} ${ic.pY}"
}

val cc4 = CC4(object : CC4.IC {
  override val pX = 42
  override val pY by lazy { println("computed!"); "abc" }
})

これが

val cc4 = CC4 {
  val pX = 42
  val pY by lazy { println("computed!"); "abc" }
}

ぐらい簡単に書けたらいいのだけれども。

それか、メソッド引数のほうをどうにかして、評価戦略を変えられるようにするとか? まあ、メソッド呼び出しの形式で書いたものが正格評価・値渡しになっていないのは、いろいろと言語の前提とかメンタルモデルとか壊していそうではある。 Ceylon言語のLazySpecifierみたいな概念を導入して、もう文法は適当なんだけど、

class CC5(lazy val pX: Int, lazy val pY: String) {
  fun cmZ() = "${pX} ${pY}"
}

val cc5 = CC5(lazy pX = 42, lazy pY = { println("computed!"); "abc" })

とか? まあ、なんかもっとマシな記法でできるといいんだけど