0

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

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

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()方法中使用泛型并强制转换为所需的输入类型——但这也不是很好。

你对我如何改进这种工厂有什么建议吗?

4

1 回答 1

2

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

解决方案#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:反射巫毒

一般来说,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初始化时抽象类缓存。

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

于 2021-10-25T11:32:26.353 回答