如何在Kotlin中为两个数据类实现可复用的times方法?
问题描述
我正在通过《TDD Explained》一书学习Kotlin,目前编写了如下代码:
data class Dollar (private val amount: Int) { fun times(multiplier: Int) = Dollar(amount = this.amount * multiplier) } data class Franc (private val amount: Int) { fun times(multiplier: Int) = Franc(amount = this.amount * multiplier) }
如你所见,Dollar相乘返回Dollar,Franc相乘返回Franc,这是我需要的逻辑,但我希望实现可复用函数以减少重复代码。我尝试了以下写法,但不确定是否正确:
data class Dollar (val amount: Int) data class Franc (val amount: Int) fun Dollar.times(multiplier: Int) = Dollar(amount = this.amount * multiplier) fun Franc.times(multiplier: Int) = Franc(amount = this.amount * multiplier)
请问如何用Kotlin的惯用方式复用该逻辑?以下是当前已通过的测试代码:
@Test fun testDollarMultiplication() { val five = Dollar(5) assertEquals(Dollar(10), five.times(2)) assertEquals(Dollar(15), five.times(3)) } @Test fun testFrancMultiplication() { val five = Franc(5) assertEquals(Franc(10), five.times(2)) assertEquals(Franc(15), five.times(3)) } @Test fun testEquality() { assertTrue(Dollar(5).equals(Dollar(5))) assertFalse(Dollar(5).equals(Dollar(10))) assertTrue(Franc(5).equals(Franc(5))) assertFalse(Franc(5).equals(Franc(10))) assertFalse(Dollar(5).equals("hi")) }
解决方案
在Kotlin中,针对这类“数值运算返回自身类型”的场景,有几种符合惯用风格的复用方式,以下是具体实现:
1. 你的扩展函数写法完全正确
首先明确:你把times写成扩展函数的方式是Kotlin中复用类方法的常用手段,完全符合惯用风格。这种写法无需修改原有data class结构,保持类的简洁性,同时实现了逻辑复用,对于当前只有两个类的场景,已经足够高效且易维护。
2. 泛型接口+默认方法(适合后续扩展更多货币类)
如果后续需要新增更多类似的货币类,推荐用F-bounded多态的泛型接口来抽象公共契约,避免重复代码:
interface Money<T : Money<T>> { val amount: Int fun times(multiplier: Int): T } data class Dollar(override val amount: Int) : Money<Dollar> { override fun times(multiplier: Int): Dollar = Dollar(amount * multiplier) } data class Franc(override val amount: Int) : Money<Franc> { override fun times(multiplier: Int): Franc = Franc(amount * multiplier) }
这种方式通过泛型约束T : Money<T>确保times方法返回当前类的实例,把公共属性和方法契约抽象到接口中,子类仅需实现具体的实例化逻辑,结构清晰且扩展性强。
3. 接口+泛型扩展函数(完全复用逻辑)
如果想彻底复用times的实现逻辑,无需子类重复编写实例化代码,可以结合接口和带reified参数的扩展函数:
interface Valuable { val amount: Int } inline fun <reified T : Valuable> T.times(multiplier: Int): T { // 依赖子类有Int参数的构造函数(data class默认满足) return T::class.java.getConstructor(Int::class.java).newInstance(amount * multiplier) } data class Dollar(override val amount: Int) : Valuable data class Franc(override val amount: Int) : Valuable
这种写法把times逻辑完全抽离,子类仅需实现Valuable接口暴露amount即可。注意这里用到了反射创建实例,对于测试场景完全适用,若追求极致性能,可结合注解或工厂模式替代反射。
以上三种方式都能通过你提供的测试代码,可根据后续扩展需求选择合适的方案。
内容的提问来源于stack exchange,提问作者Daryn
相关产品推荐
相关产品推荐

