构建 Kotlin 类的实例从 Java 代码中

huangapple go评论70阅读模式
英文:

Constructing instance of Kotlin class from Java code

问题

我有一个包含15个字段的Kotlin数据类。在我的Kotlin代码中使用主构造函数构造这个类的实例时,我可以利用一些有用的特性,比如:

  • 省略具有默认值的字段
  • 使用命名参数

然而,在从Java代码中构造这个Kotlin类的实例时,我面临一个问题,我需要按照正确的顺序为构造函数提供所有15个参数,而且不能为它们命名。当这段Java代码是一个单元测试时,这尤其不方便,因为我并不是真的想填充所有那些字段,而只关心测试中有用的一个或两个字段。

当完全使用Java时,我不会遇到这个问题,因为我会使用(Lombok)构建器来构造对象的实例,从而可以灵活地只提供我感兴趣的字段。

有没有办法减轻这个问题,或者这就是在Java和Kotlin混合使用时必须付出的代价?

英文:

I have a Kotlin data class with 15 fields. When I construct an instance of this class in my Kotlin code using the primary constructor, I can make use of useful features such as:

  • omitting fields with default values
  • use named parameters

However, when constructing an instance of this Kotlin class from Java code, I am faced with the fact I need to provide all 15 parameters in the constructor, in the correct order, without the ability to name them. This is especially inconvenient when this Java code is a unit test, where I'm not really interested in filling all those fields, but only one or two useful for the test.

When using purely Java, I would not face this problem, as I would use a (Lombok) builder to construct the instance of the object, having the flexibility to only provide the fields I'm interested in.

Is there a way to mitigate this problem, or is this the price I have to pay for mixing Java and Kotlin?

答案1

得分: 1

> Is there a way to mitigate this problem...

有一些方法可以缓解这个问题...

> ... is this the price I have to pay for mixing Java and Kotlin?

不,不是的。如果我们从另一个角度看待这个问题,你会注意到是 Java 无法提供默认值,因此在与 Kotlin 一起使用时变得不方便。构建复杂对象是 Builder 模式 存在的主要原因。但是 Kotlin 给了我们一个解决方案,可以避免大多数与构建复杂对象相关的问题,并使 builder 模式过时。

如何处理?

没有单一的解决方案,而是有多种方法。我将至少列出两种我马上想到的方法:

  1. 主构造函数 + 次要构造函数;
  2. 工厂模式。

主构造函数 + 次要构造函数

使用这个选项,你可以创建一个带有整个参数列表的主构造函数,并引入一个次要构造函数,仅接受那些必需的或不能默认设置的值

class Example(val param1: Any, val param2: Any? = null, val param3: Any? = null) {
    // 在这种情况下,我们必须使用不同的参数名,
    // 以显式使用主构造函数。
    // 例如,如果我们删除 `param1 = ` 或将 `requiredParam` 重命名为 `param1`,
    // 我们将得到一个错误,提示:"在委托调用链中存在循环"
    constructor(requiredParam: Any) : this(param1 = requiredParam)
}

工厂模式

在工厂模式的情况下,几乎一切看起来都是一样的。尽管这个解决方案在 Java 中会更冗长,但它消除了使用命名参数的需要,并且给了我们在对象初始化之前进行更多准备的自由我们甚至可以将其变成异步的(就像是网络调用一样)!

class Example(val param1: Any, val param2: Any? = null, val param3: Any? = null) {
    object Factory {
        fun from(param1: Any): Example {
            return Example(param1)
        }
    }
}

结论

没有所谓的“正确”解决方案。我们可以选择我们喜欢的方法,或者可能想出一个新方法。

  1. 主构造函数 + 次要构造函数:
    • (+) 从 Java 中使用时不那么冗长;
    • (-) 如果次要构造函数发生变化,你将不得不更新 Java 代码;
    • (-) 需要为构造函数参数使用不同的名称。
  2. 工厂模式:
    • (+) 提供更多自由:例如,你可以在 from 函数内计算一些内容,相比于构造函数更易读;
    • (+) 隐藏了构造函数的实现。你将能够在不修改任何 Java 代码或工厂函数的情况下稍后更改构造函数;
    • (-) 从 Java 中使用时会更冗长。
英文:

> Is there a way to mitigate this problem...

Sort of, but it is not an out-of-the-box solution. More about it below.

> ... is this the price I have to pay for mixing Java and Kotlin?

No, it is not. If we turn this problem around you will notice that it is Java that is incapable of providing default values and thus fails to be convenient when used with Kotlin. Construction of complex objects is the main reasons why Builder Pattern exists. But Kotlin gives us a solution to avoid most of the problems related to construction of complex objects and makes builder pattern obsolete.

How to deal with it?

There is not one solution but multiple. I'll list at least two that came to my mind right away:

  1. Primary + secondary constructor;
  2. Factories.

Primary + secondary constructor

Using this option you can create a primary constructor with the whole list of
parameters your class should hold and introduce a secondary constructor that only accepts the values that are required or cannot be set by default:

class Example(val param1: Any, val param2: Any? = null, val param3: Any? = null) {
    // In this situation we have to use different argument names
    // to make explicit use of primary constructor.
    // e.g. if we remove `param1 = ` or rename `requiredParam` to `param1`
    // we will get an error saying: "There's a cycle in the delegation calls chain" 
    constructor(requiredParam: Any) : this(param1 = requiredParam)
}

Factories

In the case of factories, everything looks almost the same. Although, this solution will be more verbose in Java but it eliminates the need for using named arguments and gives us the freedom to do more preparation before object initialization. We can even make it asynchronous (as if it was a network call)!

class Example(val param1: Any, val param2: Any? = null, val param3: Any? = null) {
    object Factory {
        fun from(param1: Any): Example {
            return Example(param1)
        }
    }
}

Conclusion

There is no "correct" solution. We can pick the one we like or maybe figure out a new one.

  1. Primary + secondary constructor:
    • (+) less verbose when used from Java;
    • (-) if secondary constructor change you will have to update Java code;
    • (-) requires different names for constructor arguments.
  2. Factories:
    • (+) gives more freedom: e.g. you can calculate something within from function and it will be more readable in comparison to constructors;
    • (+) hides constructor implementation. You will be able to change constructors later without modifying any Java code or factory functions;
    • (-) will be more verbose when used from Java.

huangapple
  • 本文由 发表于 2020年8月21日 23:26:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/63525703.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定