如何使用泛型创建具有类型安全输入的工厂



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

我正在努力寻找一个好方法来实现这一点。那么让我展示一下我目前得到的内容以及问题所在:

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是非常糟糕的。应该有很好的方法来实现这一点,而不需要尝试。不幸的是,由于类型擦除,不可能在强制转换之前检查类型。使用多态代码是无法处理具体化类型的。

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

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

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

解决方案#1:在指定参数内捕获

我们可以使用指定类型的函数逐个注册提供程序。然而,我猜这在你的情况下是不可能的,因为你使用依赖注入来获取提供商。

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

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

首先,我们在ExampleFragmentProvider中创建额外的属性,以公开其相关的T类型:

interface ExampleFragmentProvider<T: ExampleInput> {
val inputType: KClass<T>
...
}
internal class ExampleHomepageProvider ... {
override val inputType = ExampleInput.Homepage::class
...
}

也可以使用KTypeClass

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

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:reflection voodoo

一般来说,Java/Kotlin中的类型参数被擦除。然而,在某些情况下,它们仍然是可用的。例如,ExampleHomepageProvider被定义为ExampleFragmentProvider<ExampleInput.Homepage>的子类型,并且该信息存储在字节码中。所以用这个信息来获取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

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

相关内容

  • 没有找到相关文章

最新更新