【问题标题】:How to create a factory with type-safe inputs using generics如何使用泛型创建具有类型安全输入的工厂
【发布时间】:2021-10-25 10:12:55
【问题描述】:

我正在寻找一种方法将配置输入传递给从基类派生的工厂,并根据该工厂的派生类保存不同的输入参数。

我正在努力寻找实现这一点的好方法。所以让我展示一下我目前的情况以及问题出在哪里:

class ExampleFragmentFactoryImpl @Inject constructor(
    private val providers: List<ExampleFragmentProvider<out ExampleInput>>
): ExampleFragmentFactory {

    @Suppress("UNCHECKED_CAST")
    override suspend fun <T: ExampleInput> create(
        pageType: T
    ): Fragment {
        providers.forEach { provider ->
            try {
                val typesafeProvider = provider as? ExampleFragmentProvider<T>
                typesafeProvider?.let {
                    return it.provide(pageType)
                }
            } catch (e: ClassCastException) {
                // This try-except-block shall be avoided.
            }
        }
        throw IllegalStateException("could not create Fragment for pageType=$pageType")
    }
}

这里是工厂界面...

interface ExampleFragmentFactory {

    suspend fun <T : ExampleInput> create(
        pageType: T
    ): Fragment
}

现在提供者接口...

interface ExampleFragmentProvider<T: ExampleInput> {

    suspend fun provide(
        pageType: T
    ) : Fragment
}

输入类...

sealed class ExampleInput {

    object NotFound : ExampleInput()

    object WebView : ExampleInput()

    data class Homepage(
        val pageId: String
    ) : ExampleInput()
}

最后是提供者实现:

internal class ExampleHomepageProvider @Inject constructor() :
    ExampleFragmentProvider<ExampleInput.Homepage> {

    override suspend fun provide(pageType: ExampleInput.Homepage): Fragment {
        TODO()
    } 
}

如上所述,在工厂中需要 try-except 真的很糟糕。应该有很好的方法来实现这一点而无需尝试除外。不幸的是,由于类型擦除,无法在转换之前检查类型。使用多态代码无法使用具体类型 afaik。

另一种可能的解决方案是避免使用泛型并在提供程序provide() 方法中强制转换为所需的输入类型——但这也不是很好。

你有什么建议我可以改进这种工厂吗?

【问题讨论】:

  • 只是为了确保我做对了:ExampleFragmentFactory 应该与不同类型的T 一起工作,它拥有各种T 类型的提供者,当我们调用create(MyExampleInputType) 时它应该搜索与此类型相关的提供商,对吗?我认为如果不存储 Class / KClass / KType 的提供者,这是不可能的。您可以创建一个属性,如:val ExampleFragmentProvider.pageType: KType 或使用 reified 函数将提供程序添加到工厂(但我看到它是注入的,所以可能不可能)。也许有更好的方法。
  • 是的,您的要求是正确的——是的,我想注入可用的提供程序。关于您的方法的问题:如何使用提供程序类获取 KType 实例?

标签: kotlin generics factory reification


【解决方案1】:

为此,我们需要获取提供者的相关ExampleInput 的KType / KClass / Class。由于类型擦除,没有直接直接的方法来获取它,但仍然有一些方法可以获取它。

解决方案 #1:在具体参数中捕获

我们可以使用具体类型的函数一个一个地注册提供者。但是,我想这在您的情况下是不可能的,因为您使用依赖注入来获取提供程序。

解决方案 #2:由提供商提供

我们可以让提供者负责提供其相关的输入类型。在这种情况下,这是非常常见的解决方案。

首先,我们在ExampleFragmentProvider 中创建附加属性以公开其关联的T 类型:

interface ExampleFragmentProvider<T: ExampleInput> {
    val inputType: KClass<T>
    ...
}

internal class ExampleHomepageProvider ... {
    override val inputType = ExampleInput.Homepage::class
    ...
}

或者,我们可以在这里使用KType 或Class。

然后,我们使用这个暴露的类型/类在工厂中搜索匹配的提供者:

class ExampleFragmentFactoryImpl @Inject constructor(
    providers: List<ExampleFragmentProvider<*>>
): ExampleFragmentFactory {
    private val providersByType = providers.associateBy { it.inputType }

    override suspend fun <T: ExampleInput> create(
        pageType: T
    ): Fragment {
        @Suppress("UNCHECKED_CAST")
        val provider = checkNotNull(providersByType[pageType::class]) {
            "could not create Fragment for pageType=$pageType"
        } as ExampleFragmentProvider<T>
        return provider.provide(pageType)
    }
}

请注意,与您的原始解决方案相反,它会搜索确切的类型。如果您的ExampleInput 具有深层子类型结构,则ExampleHomepageProvider 在被要求时不会被使用,例如ExampleInput.HomepageSubtype.

解决方案 #3:反射巫术

一般来说,Java/Kotlin 中的类型参数会被擦除。但是,在某些情况下,它们仍然可以获得。例如,ExampleHomepageProvider 被定义为ExampleFragmentProvider&lt;ExampleInput.Homepage&gt; 的子类型,并且此信息存储在字节码中。那么使用此信息获取T 是否有意义?是的,这是有道理的,是的,有一些疯狂的反射巫毒是可能的:

fun <T : ExampleInput> ExampleFragmentProvider<T>.acquireInputType(): KClass<T> {
    @Suppress("UNCHECKED_CAST")
    return this::class.allSupertypes
        .single { it.classifier == ExampleFragmentProvider::class }
        .arguments[0].type!!.classifier as KClass<T>
}

那么,我们可以在工厂中使用这个函数来代替inputType:

private val providersByType = providers.associateBy { it.acquireInputType() }

注意,这是非常高级的东西,最好对 JVM 中的泛型有一些低层次的了解。例如,如果我们创建一个通用提供程序,那么它的T 可能实际上会被永久删除,并且上面的函数会抛出异常:

ExampleHomepageProvider().acquireInputType() // works fine
GenericFragmentProvider<ExampleInput.Homepage>().acquireInputType() // error

解决方案 #4:2 + 3 = 4

如果我们喜欢使用反射巫术,仍然让提供者负责获取他们的T 可能是有意义的。这对 OOP 有好处,并且更灵活,因为不同的提供者可以决定使用不同的方式来获取他们的类型。我们可以在接口处提供inputType的默认实现和/或提供抽象实现:

interface ExampleFragmentProvider<T: ExampleInput> {
    val inputType: KClass<T> get() = acquireInputType()
    ...
}

abstract class AbstractExampleFragmentProvider<T: ExampleInput> : ExampleFragmentProvider<T> {
    override val inputType = acquireInputType()
}

它们之间有重要的区别。每次访问inputType 时,接口上的默认实现必须计算所有内容。抽象类在初始化时缓存inputType。

当然,提供者仍然可以覆盖该属性,例如直接提供类型,就像前面的例子一样。

【讨论】:

  • 感谢您对 KClass 的使用进行澄清!这对我有用!每个接口实现都需要提供KClass当然不是完美的,但是正如您所说,很可能没有更好的解决方案。我喜欢在 O(1) 中找到正确提供者的地图方法。采用迭代方法,可以在没有 KClass 的情况下使用这个可为空的强制转换助手工作:``` inline fun castIfPossible(any: Any?) : T? = 任何作为?吨? ```
  • 如果您打算使用此castIfPossible() 根本不存储/使用KClass,那么我相信这是不可能的。 Reified 意味着调用者必须知道T 的类型,所以我们确实仍然存在以某种方式获取提供者的T 的问题。无论如何,我更新了我的答案。由于它部分不精确且具有误导性,因此我甚至从一开始就重写了其中的一些部分,因此您可能需要重新阅读所有内容。很抱歉。
  • 我尝试了 castIfPossible 的方法。它编译,但在运行时失败。看来我错了 :-) 我会通读你的代码,谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多