【问题标题】:How do I force initialization of a child-parent relationship?如何强制初始化父子关系?
【发布时间】:2018-10-01 10:23:31
【问题描述】:

我有一个配置元素的层次结构,它们都实现了一个接口(简化):

interface Configuration {
    val elements: Long
    val data: Long?
    var parent: Configuration?
}

jackson-kotlin 将通过读取 Yaml 文件来构建实现,如下所示:

checks:
  elements: 10
  data: 1234
  ports:
    elements: 5

代码也可以这样做:

CheckConfig(elements = 10, data = 1234, portConfig = PortConfig(elements = 5))

我的问题是 - 当我构造 PortConfig 时,我还不知道它的父级。 Jackson 可能,但我不知道如何利用这一点。

如果用户没有为此特定配置部分提供任何data 属性,PortConfig 实现需要其父级回退。

我现在做的事情感觉很老套:

inline fun <reified T: Configuration> T.updateChildLinks() {
    (this::class as KClass<T>).memberProperties.forEach {
        if (it.returnType.isSubtypeOf(Configuration::class.createType(nullable = true))) {
            it.isAccessible = true
            val config: Configuration = it.get(this) as Configuration
            config.parent = this
        }
    }
}

并在每个父母的init 块中调用它。我可以做得更好吗(特别是我不喜欢我必须记住在 init 阶段调用辅助函数)?

编辑添加:以前,我有一个纯代码解决方案,可以将data 传递到链中:

CheckConfig(elements = 10, data = 1234, 
        portConfig = PortConfig(elements = 5, parentData = 1234))

然后让data 旁观parentData,但我在与杰克逊一起工作时遇到了麻烦。

【问题讨论】:

  • 为什么CheckConfig 不能适当地设置portConfig.parent?如果您害怕忘记设置它,您也可以创建一个界面,例如PreConfiguration 包含与 Configuration 相同的内容,但 parent 除外。然后将PreConfiguration 作为参数给出,并且必须通过方法(采用parent)将其转换为Configuration,然后才能使用。
  • 是的,这基本上就是我的 hack 所做的。但是由于层次结构是用 jackson 构建的(并且只有测试显式地构建类),我想我可以利用它。

标签: kotlin jackson


【解决方案1】:

所以为了将来参考,这是我最终的结果:

诀窍是承认我的data class 不能有到它的父级的链接,因为子级将在其父级之前构造。

但是,我可以利用 Jackson 来解决最初的回退问题,方法是实现一个自定义反序列化器,该反序列化器跟踪父属性值并在缺失时应用它。

我在下面的实现使用了Deque,即使使用 Kotlin 感觉有点尴尬。

唯一的缺点是反序列化器仅在反序列化后才知道您的属性值(显然),因此无法将其传递给可能需要它的子值。所以这会错误地给你123,它应该是345

top:
  data: 123
  sub:
    subsub:
      important: false
    data: 345

但如果你可以控制,你可以使用以下:

class PropInheritance : JsonDeserializer<Long>() {
    override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Long {
        val currentContextName = p.parsingContext
                .pathAsPointer().toString()
                // cut the last "prop" variable name
                .replaceAfterLast("/", "")
        val prop = p.longValue
        val propStack = initStackIfNeeded(ctxt)
        propStack.push(currentContextName to prop)
        return prop
    }

    override fun getNullValue(ctxt: DeserializationContext): Long {
        val propStack = initStackIfNeeded(ctxt)

        // here, the prop name is missing the context is just the parent
        val currentContextName = ctxt.parser.parsingContext
                .pathAsPointer().toString()
        // find a prop on stack that has the same path
        while (propStack.isNotEmpty() 
                && !currentContextName.startsWith(propStack.peek().first)) {
            propStack.pop()
        }

        val prop = if (propStack.isNotEmpty()) 
                       propStack.peek().second
                   else 
                       ctxt.getAttribute("default") as Long

        // record current prop on the stack so potential children can find it
        propStack.push(currentContextName to prop)
        return prop
    }

    private fun initStackIfNeeded(ctxt: DeserializationContext)
            : Deque<Pair<String, Long>> {
        if (ctxt.getAttribute("stack") == null) {
            ctxt.setAttribute("stack", ArrayDeque<Pair<String, Long>>())
        }
        return ctxt.getAttribute("stack") as Deque<Pair<String, Long>>
    }
}

就我而言,我使用mix-in@JsonDeserialize(using = PropInheritance::class) 应用于我的接口方法的getter:

interface HasProp {
    val prop: Long
}

interface HasPropMixin {
    @JsonDeserialize(using = PropInheritance::class)
    fun getProp(): Long
}

mapper = ObjectMapper(YAMLFactory()).registerModule(KotlinModule())
    .addMixin(HasProp::class.java, HasPropMixin::class.java)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2011-04-03
    • 1970-01-01
    • 2019-01-24
    • 1970-01-01
    • 2018-04-01
    • 2011-09-19
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多