【问题标题】:Handle lots of similar monomorphic classes in a polymorphic way以多态方式处理大量相似的单态类
【发布时间】:2020-03-30 13:02:57
【问题描述】:

我有一个库,可以生成一大堆看起来非常相似的 Java 类。为简洁起见,让我们在 Kotlin 中指定类似的内容:

data class A(val commonParam1: String, val commonParam2: String, val specialParam: Int)
data class B(val commonParam1: String, val commonParam2: String, val specialParam: String)
data class C(val commonParam1: String, val commonParam2: String, val specialParam: Boolean)
data class D(val commonParam1: String, val commonParam2: String, val specialParam: Float)

显然,最好生成单个泛型类或接口:

data class G<T>(val commonParam1: String, val commonParam2: String, val specialParam: T)

但库不会生成那种代码。我有几十个这样的类,它们在不同的地方生成,所以这是一个 m×n 的情况。不幸的是,这些类不共享一个通用接口(我可以使用)。所以在我看来,有五个选项可以解决这个问题:

  • 编写与任何生成的类一起工作的所有代码m×n 次。这是一大堆重复,但一点也不复杂,而且很安全
  • 使用反射。首先生成这些类的想法是提高类型安全性。反射会再次把它扔出窗外,所以感觉适得其反
  • 为我自己的代码使用代码生成。感觉有点笨拙,我的团队没有代码生成经验
  • 使用ArrowKT typeclassesExtension Interfaces 会很棒,但我可能会接受目前存在的黑客攻击。我仍然需要提供很多样板,因为我必须为这些类中的每一个提供我的接口的证据
  • 使用Arrow-Meta quasi-quoter,但我认为我不应该在生产中使用 Arrow-Meta

我还有其他选择吗?有没有办法抽象出类似的类?


在这个具体案例中,我的 Graphql 模式有很多突变,它们采用了非常相似的 MutationInputs。我们不直接控制模式的形状。由于每个突变也有自己的输入和输出类型,即使我大量使用片段,构建这些突变并解释它们的输出也是一件苦差事,因为即使是提取片段的代码也必须重复。

这在 Graphql 中通常不是问题,因为大部分是在 JS 或 TS 中完成的,两者都有鸭子类型。 Kotlin 没有鸭子类型。

【问题讨论】:

  • 类只是空数据容器吗?没有方法或逻辑?它们是否需要是数据类(因为您对数据类继承有限制)?这些问题很重要,因为它们会影响可能的解决方案。
  • 它们确实有一些逻辑/方法,但在大多数情况下,它们是数据传输对象。
  • 好的,我在答案中给出了一些解决方案的变体,以及从注释处理器生成代码的选项。

标签: kotlin


【解决方案1】:

我在这里列出了多个选项,其中一些可以组合使用以重用代码并生成最少数量的繁琐代码。

与默认实现的接口

您可以使用具有默认功能实现的接口来提供公共基础:

interface CommonDataInterface<T> {
    val commonParam1: String
    val commonParam2: String
    val specialParam: T

    fun foo() { ...implementation here }
    fun bar() { ...implementation here }
    fun zoo(x: T): T { ...implementation here }
}

// concrete variations, using common interface functions implemented above:

data class A(override val commonParam1: String, 
             override val commonParam2: String, 
             override val specialParam: Int) 
             : CommonDataInterface<Int>
data class B(override val commonParam1: String, 
             override val commonParam2: String, 
             override val specialParam: String) 
             : CommonDataInterface<String>
data class C(override val commonParam1: String, 
             override val commonParam2: String, 
             override val specialParam: Boolean) 
             : CommonDataInterface<Boolean>
data class D(override val commonParam1: String, 
             override val commonParam2: String, 
             override val specialParam: Float) 
             : CommonDataInterface<Float>

使用非数据类扩展基类

另一种方法是非数据类的泛型类:

class G<T>(val commonParam1: String, val commonParam2: String, val specialParam: T) { 
    fun foo() { ...implementation here }
    fun bar() { ...implementation here }
    fun zoo(x: T): T { ...implementation here }
}

// concrete variations, using common functions from base class

class A(commonParam1: String, commonParam2: String, specialParam: Int) 
         : G<Int>(commonParam1, commonParam2, specialParam)
class B(commonParam1: String, commonParam2: String, specialParam: String)
         : G<String>(commonParam1, commonParam2, specialParam)
class C(commonParam1: String, commonParam2: String, specialParam: Boolean)
         : G<Boolean>(commonParam1, commonParam2, specialParam)
class D(commonParam1: String, commonParam2: String, specialParam: Float)
         : G<Float>(commonParam1, commonParam2)

这并没有节省多少打字,而且有很多冗余参数。


使用接口的透明委托

如果您需要将所有内容保留在类中或想要组合多个不同的功能集(您可以实现多个接口并委托给多个支持实现),则另一种方法是使用“委托继承”:

interface CommonDataInterface {
    val commonParam1: String
    val commonParam2: String

    fun foo() 
    fun bar() 
}

data class CommonData(override val commonParam1: String, 
                      override val commonParam2: String)
                      : CommonDataInterface {
    override fun foo() { ...implementation here  }
    override fun bar() { ...implementation here  }
}

// concrete implementations using common functions via automatic delegation 

data class A(private val common: CommonDataInterface, 
             val specialParam: Int) : CommonDataInterface by common {
    fun zoo(x: Int): Int { ...implementation here using specialParam }
}

data class B(private val common: CommonDataInterface, 
             val specialParam: String) : CommonDataInterface by common {
    fun other(y: String): String { ...implementation here using specialParam }
}

data class C(private val common: CommonDataInterface, 
             val specialParam: Boolean) : CommonDataInterface by common

data class D(private val common: CommonDataInterface, 
             val specialParam: Float) : CommonDataInterface by common

但以上内容不允许specialParam 被任何通用函数使用,它将根据专业化实现。


使用注释处理器生成类的排列

您可以编写自己的注释处理器,这是代码生成或其他编译时操作的一种形式,将注释添加到您的特殊类,并让注释处理器生成字节码中的所有排列。然后它可以融入到你的编译过程中,神奇地出现那些额外的类。

您需要在某处查看 Kotlin 如何进行注释处理以及一般主题。此外,Kotlin 1.4 之后可能会是一个编译器插件 API,它可以帮助编写做类似工作的插件。

注释处理的一些链接表明它并不难处理:


使用 Kotlin 代码生成库生成 Kotlin 代码

查看 Kotlin 代码生成的一个简单方法是 KotlinPoet 库。它是一种以编程方式生成代码的方式,即使对于代码生成经验很少的团队来说也很容易。这可以与前面提到的关于如何构建类的变体一起使用。它也可以与注释处理一起使用,作为在编译期间触发代码生成的方法。

【讨论】:

  • 我不控制基础数据类,所以不能添加接口。但我认为我可能能够按照使用代码生成来复制基类并让它们实现通用接口的方式做一些事情。也许,我得看看它。
猜你喜欢
  • 1970-01-01
  • 2019-08-03
  • 1970-01-01
  • 2016-03-25
  • 1970-01-01
  • 2011-04-04
  • 1970-01-01
  • 2019-01-15
  • 2012-03-15
相关资源
最近更新 更多