首页 > 解决方案 > 如何在 Kotlin 中动态组合接口?

问题描述

我不确定如何以更正确的方式表达这个问题,或者如何想出一个不那么做作的例子。

假设我有一个描述某些字段的界面(Data在我的示例中)。

然后我想对其应用一些转换(在我的示例中对字段进行求和并相乘)。理想情况下,这应该类似于应用程序用户在运行时可配置的数据管道(例如,通过配置文件)。SummedData我还为通过每个转换 (和MultipliedData)添加到我的对象的属性定义接口。

我还定义了一些实现这些接口以及原始接口的类 (DataSummer和) ,因此它们可以“级联”并一个接一个地应用。在我的示例中,这些非常简单,但您可以想象它们懒惰地调用外部 API 或进行一些昂贵的处理。DataMultiplierData

一切正常,除了每个后续转换都会删除前一个转换的类型信息(在我的示例中,尽管它们都应用于原始对象,但data is SummedData始终为真且始终为假)。data is MultipliedData我想要的是无论我应用转换的顺序如何,这两项检查都是真实的。

我知道如何用 Python 等更动态的语言来做到这一点,但我想知道 Kotlin 是否可以做类似的事情。

代码示例:

interface Data {
    val field1 : Int
    val field2 : Int
}

interface SummedData  {
    val fSum : Int
}

interface MultipliedData {
    val fProd : Int
}

data class DataSummer(private val iData : Data, private val iSum : SummedData) : Data by iData, SummedData by iSum{
    constructor(data : Data) : this(
        data,
        object:SummedData {override val fSum:Int = data.field1 + data.field2}
    )
}

data class DataMultiplier(private val iData : Data, private val iMul : MultipliedData) : Data by iData, MultipliedData by iMul{
    constructor(data : Data) : this(
        data,
        object:MultipliedData {override val fProd:Int = data.field1 * data.field2}
    )
}

fun main() {
    var data:Data = object : Data {
        override val field1 = 2
        override val field2 = 3}

    data = DataMultiplier(data)
    data = DataSummer(data)

    //Always false :(
    if (data is MultipliedData){
        println("Product of ${data.field1} and ${data.field2} is ${data.fProd}" )
    }
    if (data is SummedData){
        println("Sum of ${data.field1} and ${data.field2} is ${data.fSum}" )
    }
}

提前感谢您的回答!

标签: oopkotlindesign-patternsjvm

解决方案


后续变换去掉前一个的类型信息

如果要保留此信息,则需要显式存储它:

//abstract class is used here instead of interface to introduce its property as private
//pass thread-safe implementation of MutableList into constructor if you are about to add mixins in multiple threads
abstract class DataWithMixins(private val mixins: MutableList<Any> = ArrayList()) : Data {
    fun addMixin(mixin: Any): DataWithMixins {
        mixins.add(mixin)
        return this //to allow chaining calls
    }

    inline fun <reified T : Any> getMixin(): T? = getMixinOfType(T::class) as? T

    //mediator-function to access non-public API from public-API inline function
    fun getMixinOfType(type: KClass<*>): Any? = mixins.find { type.isInstance(it) }
}

用法:

fun main() {
    val data = object : DataWithMixins() {
        override val field1 = 2
        override val field2 = 3
    }

    data.addMixin(DataSummer(data))
        .addMixin(DataMultiplier(data))

    //get mixin of interface type
    data.getMixin<SummedData>()?.run {
        println("Sum of ${data.field1} and ${data.field2} is $fSum")
    }
    data.getMixin<MultipliedData>()?.run {
        println("Product of ${data.field1} and ${data.field2} is $fProd")
    }

    //get mixin of class type, now we can access field1 & field2 directly
    data.getMixin<DataSummer>()?.run {
        println("Sum of $field1 and $field2 is $fSum")
    }
    data.getMixin<DataMultiplier>()?.run {
        println("Product of $field1 and $field2 is $fProd")
    }
}

为了让 mixin 的 API 和它的 getter 一样好,我们必须:

  1. 使用反射
  2. 隐式(没有编译时错误,但运行时)限制 mixin,可以根据从data: Data(或没有任何源)明确实例化的可能性以这种方式添加。在以下实现中,这些 mixin 是:
    1. 单例(又名对象类)
    2. 具有不超过一个Data参数的构造函数的类(因为我们不会使用其默认值,但会传递我们自己的参数)并且没有其他参数或所有其他参数都是可选的(这包括无参数构造函数)
inline fun <reified T : Any> DataWithMixins.addMixin(): DataWithMixins {
    val objectInstance = T::class.objectInstance
    val mixin = when {
        objectInstance != null -> objectInstance
        else -> {
            //there is no more than one Data parameter
            //AND
            //there is no more than one mandatory parameter and it is Data
            var niceConstructor: KFunction<T>? = null
            var dataParameter: KParameter? = null
            constructorsLoop@
            for (constructor in T::class.constructors) {
                dataParameter = null
                var mandatoryParametersCounter = 0
                for (param in constructor.parameters) {
                    val isMandatory = !param.isOptional
                    val isData = param.type.classifier == Data::class
                    if (isData) {
                        dataParameter = if (dataParameter == null) param else continue@constructorsLoop
                        if (isMandatory && ++mandatoryParametersCounter > 1) continue@constructorsLoop
                    } else if (isMandatory) continue@constructorsLoop
                }
                if (mandatoryParametersCounter == 0 || dataParameter != null) {
                    niceConstructor = constructor
                    break@constructorsLoop
                }
            }
            if (niceConstructor == null) error("${T::class} can't be auto-instantiated as a mixin for Data")
            if (dataParameter == null) {
                niceConstructor.call()
            } else {
                niceConstructor.callBy(mapOf(dataParameter to this))
            }
        }
    }
    //can't access non-public API from public-API inline function directly, so will use this method as mediator
    return addMixin(mixin)
}

用法:

    data.addMixin<DataSummer>()
        .addMixin<DataMultiplier>()

//instead of

//  data.addMixin(DataSummer(data))
//      .addMixin(DataMultiplier(data))

另外我相信您的DataSummer课程DataMultiplier可以简化:

data class DataSummer(private val iData: Data) : Data by iData, SummedData {
    override val fSum: Int = iData.field1 + iData.field2
}

data class DataMultiplier(private val iData: Data) : Data by iData, MultipliedData {
    override val fProd: Int = iData.field1 * iData.field2
}

推荐阅读