Kotlinのsealed classとdelegateを使った型遊びです。
例えば以下のような関係のinterfaceがあったとします。
interface Model {
val id: String
val name: String
}
interface Element : Model
interface Container : Element {
fun createComponent(name: String): Component
}
interface Component : Element {
val parent: Container
}
本来はComponentを生成する責務をContainerに持たせたくはない(Factoryとして分離するべき)ですが、ContainerにcreateComponentを生やさなければいけない、というケースであるとします。 その上で、Element以下の関係性をsealed classとして表現したい!と思う事があります。 具体的には
interface Model {
val id: String
}
sealed class Element : Model {
class Container : Element() {
fun createComponent(): Component = Component(this)
}
class Component internal constructor(val parent: Container) : Element()
}
のようにしたい。まだイメージ段階ですので当然ですが、上記のコードはもちろんコンパイルが通りません。 それではやっていきます。
いきなり余談ですが、Kotlinは同一ファイル内であれば、ContainerやComponentはElementの中にネストしてコードを書く必要はありません。
interface Model {
val id: String
}
sealed class Element : Model
class Container : Element() {
fun createComponent(): Component = Component(this)
}
class Component
internal constructor(
val parent: Container
) : Element()
これでも大丈夫です。REPL環境下で書いているので今回はネストする事にします。
繰り返しますが、上記のコードはもちろんコンパイルが通りません。idプロパティを実装してないからです。ContainerやComponentにはidを注入する必要があります。これらのclassを使うユーザに適当にidを振られても困ります。 IdGeneratorを注入するでも良いですが、そもそものContainer, Componentの生成自体を抽象化した方が筋が良さそうです。もちろんFactoryはユーザには見せたくありません。
internal interface ContainerFactory {
fun create(): Element.Container
}
internal interface ComponentFactory {
fun create(container: Element.Container): Element.Component
}
interface Model {
val id: String
}
sealed class Element : Model {
class Container
internal constructor(
private val factory: ComponentFactory,
override val id: String
) : Element() {
fun createComponent(): Component =
factory.create(this)
}
class Component
internal constructor(
val parent: Container,
override val id: String
) : Element()
}
ここでさらに追加の要件です。Modelにはattributeをmutableに割り当てられるようにしたいです。
interface Model {
val id: String
val attributes: Map<String, String>
fun addAttribute(name: String, value: String)
}
Mutableやめろ!Immutableに作り直せ!分かります。
interface Model {
val id: String
val attributes: Map<String, String>
fun <T : Model> addAttribute(name: String, value: String): T
}
これでどうでしょうか。Immutableになってますか。うるせ〜〜〜!すみませんが今回はそれが本筋ではないのでMutableにします。
internal interface ContainerFactory {
fun create(): Element.Container
}
internal interface ComponentFactory {
fun create(container: Element.Container): Element.Component
}
interface Model {
val id: String
val attributes: Map<String, String>
fun addAttribute(name: String, value: String)
}
sealed class Element : Model {
class Container
internal constructor(
private val factory: ComponentFactory,
override val id: String
) : Element() {
override val attributes: Map<String, String>
get() = mutableAttributes.toMap()
private val mutableAttributes: MutableMap<String, String> = LinkedHashMap()
override fun addAttribute(name: String, value: String) {
mutableAttributes[name] = value
}
fun create(container: Element.Container): Element.Component =
factory.create(this)
}
class Component
internal constructor(
val parent: Container,
override val id: String
) : Element() {
override val attributes: Map<String, String>
get() = mutableAttributes.toMap()
private val mutableAttributes: MutableMap<String, String> = LinkedHashMap()
override fun addAttribute(name: String, value: String) {
mutableAttributes[name] = value
}
}
}
実装しました。うーんだいぶ実装が被っていますね。Container or Component is a Element, Element is a Model な関係であれば、一旦Elementに持っていっても大丈夫そうでしょうか。
internal interface ContainerFactory {
fun create(): Element.Container
}
internal interface ComponentFactory {
fun create(container: Element.Container): Element.Component
}
interface Model {
val id: String
val attributes: Map<String, String>
fun addAttribute(name: String, value: String)
}
sealed class Element(
override val id: String
) : Model {
class Container
internal constructor(
private val factory: ComponentFactory,
id: String
) : Element(id) {
fun create(container: Element.Container): Element.Component =
factory.create(this)
}
class Component
internal constructor(
val parent: Container,
id: String
) : Element(id)
override val attributes: Map<String, String>
get() = mutableAttributes.toMap()
private val mutableAttributes: MutableMap<String, String> = LinkedHashMap()
override fun addAttribute(name: String, value: String) {
mutableAttributes[name] = value
}
}
ではここでさらに追加の要件で、Modelインターフェースを実装するクラスRelationshipを追加しましょう。
…アッ!Elementにある実装をRelationshipにも持っていきたいなァ…。
internal interface ContainerFactory {
fun create(name: String): Element.Container
}
internal interface ComponentFactory {
fun create(container: Element.Container, name: String): Element.Component
}
interface Model {
val id: String
val attributes: Map<String, String>
fun addAttribute(name: String, value: String)
}
internal class ModelDelegate(
override val id: String
) : Model {
override val attributes: Map<String, String>
get() = mutableAttributes.toMap()
private val mutableAttributes: MutableMap<String, String> = LinkedHashMap()
override fun addAttribute(name: String, value: String) {
mutableAttributes[name] = value
}
}
sealed class Element(
id: String
) : Model by ModelDelegate(id) {
class Container
internal constructor(
private val factory: ComponentFactory,
id: String
) : Element(id)
class Component
internal constructor(
val parent: Container,
id: String
) : Element(id)
}
interface Relationship : Model {
val source: Element
val destination: Element
}
internal class RelationshipImpl(
override val source: Element,
override val destination: Element,
id: String
): Relationship, Model by ModelDelegate(id)
Kotlinはこんなときdelegateが使えるので良いですね。「継承より移譲」がちゃんと実現できています。
さてここで、現実はこんなに簡単なclassばかりではないですから、プロパティやメソッドはどんどん増えますし、一体何が本当に必要なinterfaceなのかわからなくなってきます。 それでもsealed classの恩恵は大きいので維持したい。いっそ今後は一生sealed classに手を触れずに、sealed classの機能を追加していきたい。 そしてdelegateによる実装も有効活用していきたい。
やってみましょう。本当に必要なinterfaceですから、ContractsとかRequirementsなんて名前が良さそうですね。今回はRequirementsでいったんやっていきます。 もうちょっとプロパティやメソッドも増やしつつやっていきます。
interface Model {
val id: String
val attributes: Map<String, String>
fun addAttribute(name: String, value: String)
}
interface ElementRequirements : Model {
val name: String
val description: String
}
sealed class Element : ElementRequirements
はい。このあと、ContainerとComponentを追加する以外にElementに触る事はありません。しばしのお別れです。 同じ調子で、ContainerやComponentも本当に必要なinterfaceがなんなのか明らかにしていきましょう。
interface Model {
val id: String
val attributes: Map<String, String>
fun addAttribute(name: String, value: String)
}
interface ElementRequirements : Model {
val name: String
val description: String
}
interface ContainerRequirements : ElementRequirements {
var technology: String
fun createComponent(name: String): Element.Component
}
interface ComponentRequirements : ElementRequirements {
val parent: Element.Container
var technology: String
val codes: Set<Code>
fun createCode(type: String): Code
}
data class Code(val type: String)
Codeについてはほっといてください。まあなんか作る必要があるんでしょう。 さてinterfaceが明らかになったわけですから、ContainerやComponentを実装していきます。 ここで少し前のコードを思い出します。
interface Model {
val id: String
val attributes: Map<String, String>
fun addAttribute(name: String, value: String)
}
internal class ModelDelegate(
override val id: String
) : Model {
override val attributes: Map<String, String>
get() = mutableAttributes.toMap()
private val mutableAttributes: MutableMap<String, String> = LinkedHashMap()
override fun addAttribute(name: String, value: String) {
mutableAttributes[name] = value
}
}
このコードはModelの実装クラスの実装をElementの継承クラスであるContainer、Componentだけでなく、Relationshipでも移譲できるように作られました。 interface分離されたわけですから、ModelDelegateと同様に、sealed classであるElement、Container、Componentから実装クラスに移譲してしまえばよいのではないでしょうか。 さて実装していきます。
// region Requirements
interface Model {
val id: String
val attributes: Map<String, String>
fun addAttribute(name: String, value: String)
}
interface ElementRequirements : Model {
val name: String
val description: String
}
interface ContainerRequirements : ElementRequirements {
var technology: String
fun createComponent(name: String): Element.Component
}
interface ComponentRequirements : ElementRequirements {
val parent: Element.Container
var technology: String
val codes: Set<Code>
fun createCode(type: String): Code
}
data class Code(val type: String)
// endregion
// region Implementation
internal class ModelDelegate(
override val id: String
) : Model {
override val attributes: Map<String, String>
get() = mutableAttributes.toMap()
private val mutableAttributes: MutableMap<String, String> = LinkedHashMap()
override fun addAttribute(name: String, value: String) {
mutableAttributes[name] = value
}
}
internal class ElementDelegate(
id: String,
override val name: String
) : ElementRequirements, Model by ModelDelegate(id) {
override var description: String = ""
}
internal class ContainerDelegate(
id: String,
name: String,
private val factory: ComponentFactory
) : ContainerRequirements,
ElementRequirements by ElementDelegate(id, name) {
override var description: String = ""
override var technology: String = ""
override fun createComponent(name: String): Element.Component {
return factory.create(this, name)
}
}
internal class ComponentDelegate(
override val parent: Element.Container,
id: String,
name: String
) : ComponentRequirements,
ElementRequirements by ElementDelegate(id, name) {
override var description: String = ""
override var technology: String = ""
override val codes: Set<Code>
get() = mutableCodes.toSet()
private val mutableCodes: MutableSet<Code> = LinkedHashSet()
override fun createCode(type: String): Code {
val code = Code(type)
mutableCodes.add(code)
return code
}
}
// endregion
実装しました。すると、sealed classの定義は以下のようになります。
sealed class Element : ElementRequirements {
class Container
internal constructor(
requirements: ContainerRequirements
) : Element(),
ContainerRequirements by requirements
class Component
internal constructor(
requirements: ComponentRequirements
) : Element(),
ComponentRequirements by requirements
}
sealed classの宣言以外をすべて移譲する事ができました。やりましたね。
これの何が嬉しいかと言われれば、Element sealed classと、それらに本当に必要なinterfaceのみがpublicになり、他をinternalに閉じる事ができる点です。ComponentFactoryだのContainerFactoryだの最初にあーだこーだ言いましたが、facade以外はすべてinternalに閉じるので、オブジェクトの振る舞いや依存関係をどう解決するか、どう実装するかをinternalに閉じて検討する事ができます。 今回の実装においては各RequirementsインターフェースをDelegateクラスが直接実装しましたが、その間にもう1枚interfaceをかます事で、非publicなプロパティやメソッドも持つことができますし、結果としてモデリングが捗る、というわけです。
まとめ
ご利用は計画的に。
すべてのソースコード
// region Requirements
interface Model {
val id: String
val attributes: Map<String, String>
fun addAttribute(name: String, value: String)
}
interface ElementRequirements : Model {
val name: String
val description: String
}
interface ContainerRequirements : ElementRequirements {
var technology: String
fun createComponent(name: String): Element.Component
}
interface ComponentRequirements : ElementRequirements {
val parent: Element.Container
var technology: String
val codes: Set<Code>
fun createCode(type: String): Code
}
// endregion
// region sealed classes
sealed class Element : ElementRequirements {
class Container
internal constructor(
requirements: ContainerRequirements
) : Element(),
ContainerRequirements by requirements
class Component
internal constructor(
requirements: ComponentRequirements
) : Element(),
ComponentRequirements by requirements
}
// endregion
// region Other dependencies
interface Relationship : Model {
val source: Element
val destination: Element
}
internal class RelationshipImpl(
override val source: Element,
override val destination: Element,
id: String
): Relationship, Model by ModelDelegate(id)
data class Code(val type: String)
internal interface ContainerFactory {
fun create(): Element.Container
}
internal interface ComponentFactory {
fun create(parent: ContainerRequirements, name: String): Element.Component
}
// endregion
// region Implementation
internal class ModelDelegate(
override val id: String
) : Model {
override val attributes: Map<String, String>
get() = mutableAttributes.toMap()
private val mutableAttributes: MutableMap<String, String> = LinkedHashMap()
override fun addAttribute(name: String, value: String) {
mutableAttributes[name] = value
}
}
internal class ElementDelegate(
id: String,
override val name: String
) : ElementRequirements, Model by ModelDelegate(id) {
override var description: String = ""
}
internal class ContainerDelegate(
id: String,
name: String,
private val factory: ComponentFactory
) : ContainerRequirements,
ElementRequirements by ElementDelegate(id, name) {
override var description: String = ""
override var technology: String = ""
override fun createComponent(name: String): Element.Component {
return factory.create(this, name)
}
}
internal class ComponentDelegate(
override val parent: Element.Container,
id: String,
name: String
) : ComponentRequirements,
ElementRequirements by ElementDelegate(id, name) {
override var description: String = ""
override var technology: String = ""
override val codes: Set<Code>
get() = mutableCodes.toSet()
private val mutableCodes: MutableSet<Code> = LinkedHashSet()
override fun createCode(type: String): Code {
val code = Code(type)
mutableCodes.add(code)
return code
}
}
// endregion