Key Takeaways(划重点)
- 设计原则不要忘
- 扩展是个补锅匠
- 定向输出是好手
- 升级外挂要当心
这里假定你对Kotlin的Extension Functioins(扩展函数)有一定了解,如果没有,请先浏览官网有关它的定义和常见用法。
面向对象编程
在Kotlin的官方网站介绍扩展函数的时候,使用的例子是Collections.swap(List, int, int)
,那我们也拿它来说事吧。
这个是swap在Java中的定义:
Collections.java
public class Collections {
/** @since 1.4 */
public static void swap(List> list, int i, int j)
}
看到这个,我就琢磨一个事,为什么我不能直接把这个方法定义在List上呢,好比这样:
List.java
public interface List extends Collection {
void swap(int i, int j)
}
如果这样可行,哪里还要Collections.swap啊。
但现实是这个Collections.swap()是从Java 1.4就有的,而想要List能够原生支持swap得等到Java 8的Interface Default Methods:
public interface List extends Collection {
void swap(int i, int j) {
set(i, l.set(j, get(i))); // 不是Java 8+编译报错
}
}
所以,之前我们认为的别扭设计,不是Java的设计师没想到,而真是时机未到。
那如果我们在Kotlin中也来这么玩一下(当然是用扩展函数而不是工具类了):
fun MutableList.swap(i: int, j: int): Unit
从使用的角度,和原生函数是一样的:mutableListOf(1, 2, 3).swap(0, 2)
。但这个时候我们就需要问一句,为什么不直接使用原生的实现呢?
所以,基于设计原则的原生函数是始终优先于扩展函数的。(千万别忘了OO的多态的强大,而扩展函数恰恰由于是静态调用,不支持多态的)
扩展函数是个补锅匠
Kotlin自身就有很多扩展函数,譬如Array
。这个扩展可以让数组按需转换,也是一个高频使用的扩展函数。
public inline fun Array.map(transform: (T) -> R): List {
return mapTo(ArrayList(size), transform)
}
看到这里可能就有疑问,为什么不做成Array的原生函数呢?
其实,如果阅读代码够仔细,可以看到,接口定义和实现中分别出现了List
、ArrayList
,这些代表着依赖。Array本身其实是不应该依赖他们的,如果把这个map功能进行原生实现,就存在接口污染问题(高内聚,低耦合设计原则)。
所以这个时候,扩展函数就可以很好的补上这个锅 了。
特定场景的定向扩展
谈到原生函数,一般而言只会存放普遍需要的。对于只适用于某些特定场合/环境/上下文的,就不适合了。譬如Domain Object或OOA的Entity,如Person,在某些场景需要输出成json的时候,如果可以直接person.toJson()会很便捷。但这个不是基础需求,也不希望引入JSON带来第三方类/库。另外这个口子开了,后面其他使用者需要输出成xml的时候,同样会再次引入xml而带来更多的第三方类/库。
这样原本的一个存粹的Domain Object或Entity或者说我们的common/core module,依赖了一堆堆第三方包,咋看都怪怪的。
这个时候,扩展函数又是一个很好的输出。在需要的应用中,我们可以自行扩展:
// app: JSON app
fun Person.toJson(): String
// app: XML app
fun Person.toXml(): String
原生函数执行优先级高于扩展函数
对第三方库进行扩展的时候,需要注意扩展函数和原生函数在执行上的一个先后顺序:原生函数优先于扩展函数。
If a class has a member function, and an extension function is defined which has the same receiver type, the same name and is applicable to given arguments, the member always wins
这个问题在升级被扩展的类库的时候尤其需要注意。拿上面例子中的toJson来说,本来原生没提供,所以我们扩展了。但对方可能也意识到这个蛮有用(甭纠结上面不是说违反原则吗),也就在某次版本中添加了这个功能。问题是对方的原生实现在null值处理的时候和你不一样(假如对方null的时候String会是空串 "name": "",而你是不输出),就可能导致升级之后由于原生函数优先执行,你的程序可能就会挂了。
所以,玩外挂每次升级都需要额外注意。
总结下扩展函数的使用要点/场景:
- 设计原则不要忘
- 扩展是个补锅匠
- 定向输出是好手
- 升级外挂要当心
希望这篇博文能对你有所帮助,喜欢的话点个赞吧!
更多Kotlin的实用技巧,请参考《Kotlin边用边学系列》
参考资料
- The Ugly Truth about Extension Functions in Kotlin
- More Kotlin Extension Fun