Uzzu::Blog

Software Design, and my life.

Kotlinのsealed classとdelegateを使った型遊び

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
comments powered by Disqus