本文由 简悦SimpRead 转码,原文地址 [blog.kotlin-academy.com] (blog.kotlin-academy.com/inheritance…)
分享共同行为是编程中最重要的事情之一。通过编程,我们代表......
分享共同行为是编程中最重要的事情之一。通过编程,我们代表了随时间变化的知识。同一知识的实例越多,改变它就越难。只是为了操作一个具体的例子,让我们拿RxJava的订阅在onDestroy中收集和取消订阅的常见行为来说。
var subscriptions: List<Subscription> = listOf()
fun onDestroy() {
subscriptions.forEach { it.unsubscribe() }
}
我们需要在所有实现一些逻辑的类中使用它,因为它们都会产生订阅。我们还不知道,但很快RxJava 2就会来到我们的项目中,我们需要把它改成以下模式。
val subscriptions = CompositeDisposable()
fun onDestroy() {
subscriptions.dispose()
}
operator fun CompositeDisposable.plus(d: Disposable) {
subscriptions.add(d)
}
这就解释了为什么把之前的模式复制到我们使用的每一个类中是一个非常糟糕的主意。在所有的类中改变这一点将是非常困难的。很好,我们很聪明,我们记住了代码重用的问题;) 我们今天来讨论一下我们的替代方案。
继承的问题
类很时髦,看起来也不错,但它们已经不被程序员真正喜欢了。
继承是OOP(面向对象编程)中重复使用代码的最直观的方式。我们可以用它来解决这个问题,在一个开放的类里面定义我们的模式。
open class RxJavaUser {
var subscriptions: List<Subscription> = listOf()
fun onDestroy() {
subscriptions.forEach { it.unsubscribe() }
}
}
虽然继承有很多弊端。例如,我们不能在JVM中扩展一个以上的类。结果是,我们最终用Base类来处理所有的事情:BaseActivity, BasePresenter, BaseViewAdapter, ...它们中的每一个都聚集了方法和属性,经常被子类使用。虽然这并不是一个好的模式。在一个类中混合多种功能是不好的。我们可能还需要在许多基类中加入我们的订阅和取消订阅的功能,如BaseActivity,BaseFragment等。更重要的是,采取你不需要的能力也是一种不好的做法。
继承的问题还有很多。最大的一个问题是,继承破坏了封装。看一下下面这个类。
class CountingHashSet<T> : HashSet<T>() {
var addCount = 0
private set
override fun add(o: T): Boolean {
addCount += 1
return super.add(o)
}
override fun addAll(collection: Collection<T>): Boolean {
addCount += collection.size
return super.addAll(collection)
}
}
这里是用法。
val countingSet = CountingHashSet()
countingSet.addAll(listOf(1,2,3))
print(countingSet.addCount)
结果是什么?
不,它不是 "3"。它打印的是 "6"。
原因是addAll使用了add方法。那么,也许我们可以直接在addAll中删除加法?是的,我们可以,而且我们也许应该这样做--我们和其他许多开发者和库的创建者。Java的创造者们知道,他们也知道他们不能再改变HashSet的实现。这只会破坏所有依赖其内部实现的库,而且会引起意想不到的、难以发现的错误。这就是为什么我们应该让我们的类成为最终的,以及为什么我们应该选择替代方案而不是继承。最常见的替代方法是组合。
组成
Composition是一个复杂的词,在实践中意味着一个类包含另一个类的实例并使用其能力。例如,订阅收集和取消订阅的能力可以在一个单独的类中持有,并使用组成。
class SubscriptionsCollector {
private var subscriptions: List<Subscription> = listOf()
fun add(s: Subscription) {
subscriptions += s
}
fun onDestroy() {
subscriptions.forEach { it.unsubscribe() }
}
}
class MainPresenter {
val subscriptionsCollector = SubscriptionsCollector()
fun onDestroy() {
subscriptionsCollector.onDestroy()
}
//...
}
注意,我们的SubscriptionsCollector与RxJava 2的CompositeDisposable是同一个概念。这意味着我们在未来会有额外的包装。不过这并不坏。它的优点是我们可以控制这种包装,如果我们需要迁移到比如说RxJava 3,那么我们只需在一个单一的类中替换使用。
不过,用法并不那么简单。我们每次都需要声明 "onDestroy",而且每次我们想添加另一个订阅时都需要使用 "subscriptionsCollector"。继承提供了更简单的用法。它也提供了多态的行为。例如,我们可以用下面的方式定义CountingHashSet,使用组合方式。
class CountingHashSet<T> {
val set = HashSet<T>()
var addCount = 0
private set
fun add(o: T): Boolean {
addCount += 1
return set.add(o)
}
fun addAll(collection: Collection<T>): Boolean {
addCount += collection.size
return set.addAll(collection)
}
}
虽然这个类不是一个MutableSet,而且除了add和addAll之外,它没有实现任何其他函数。我们可以让它实现MutableSet,但是我们最终会得到下面这个怪物。
class CountingHashSet<T>: MutableSet<T> {
val set = HashSet<T>()
var addCount = 0
private set
override fun add(element: T): Boolean {
addCount += 1
return set.add(element)
}
override fun addAll(elements: Collection<T>): Boolean {
addCount += elements.size
return set.addAll(elements)
}
override fun clear() {
set.clear()
}
override fun iterator(): MutableIterator<T> {
return set.iterator()
}
override fun remove(element: T): Boolean {
return set.remove(element)
}
override fun removeAll(elements: Collection<T>): Boolean {
return set.removeAll(elements)
}
override fun retainAll(elements: Collection<T>): Boolean {
return set.retainAll(elements)
}
override val size: Int
get() = set.size
override fun contains(element: T): Boolean {
return set.contains(element)
}
override fun containsAll(elements: Collection<T>): Boolean {
return set.containsAll(elements)
}
override fun isEmpty(): Boolean {
return set.isEmpty()
}
}
这是一个委托的例子--使用组合的模式--而Kotlin支持它,并允许更简单的符号。
授权
上述巨大的类可以用下面的实现来代替。
class CountingHashSet<T>(
val innerSet: MutableSet<T> = HashSet<T>()
) : MutableSet<T> by innerSet {
var addCount = 0
private set
override fun add(element: T): Boolean {
addCount += 1
return innerSet.add(element)
}
override fun addAll(elements: Collection<T>): Boolean {
addCount += elements.size
return innerSet.addAll(elements)
}
}
它的作用完全一样--来自MutableSet的方法如果没有被实现,将被生成只使用innerSet的主体。因此,我们有多态的行为(我们实现了MutableSet),代码重用和简短的声明。这很完美。尽管这种模式并不常用。那我们的订阅管理呢?我们可以在那里使用它吗?嗯,我们可能可以。
interface SubscriptionsCollector {
var subscriptions: List<Subscription>
fun onDestroy()
}
class SubscriptionsCollectorImpl {
var subscriptions: List<Subscription> = listOf()
fun onDestroy() {
subscriptions.forEach { it.unsubscribe() }
}
}
class MainPresenter(
val subscrCollector = SubscriptionsCollectorImpl()
): SubscriptionsCollector by subscrCollector {
//...
}
这种用法是很可怕的,它在扰乱我们的论据。如果做自定义的onDestroy,也是不直观的。唯一的好处是,我们可以用和使用继承时一样的方式添加订阅。我绝对不会使用委托支持来解决这个问题
特质
尝试了这么多次,还是很难找到一个好的替代方案来解决我们的问题。虽然,还有一种技术应该是每个程序员的工具包的一部分。我们可以使用traits。在Kotlin中,接口就是traits(历史上甚至使用关键字 "trait "而不是 "interface")。在实践中,这意味着我们可以为方法定义默认体,并声明属性(但没有任何实际值,所以它们仍然必须在非抽象类中被重写)。
interface NumberHolder {
val number: Int
fun doubled() = number * 2
}
class TenNumberHolder: NumberHolder {
override val number = 10
}
print(TenNumberHolder().doubled()) // 20
(Mixins是一个类似的概念,但在mixins中,我们也可以保持状态。例如,我们将能够声明具有默认值的属性。Kotlin还不支持它们)。
特质的概念是非常强大的。让我们来玩一玩。比方说,我们写一个格斗模拟器。每个战士都被表述为一个类。他们都有一些职业,和这个职业一起,他们有一些特征。还有一些不是角色的怪物。我们想让这些角色之间进行战斗模拟。
让我们从战斗能力开始。我们可以用一个接口来表达战斗能力。
interface Fighter {
var lifePoints: Int
fun attack(opponent: Fighter)
fun turnEnd() {}
}
怪兽只是战斗机。
data class Goblin(override var lifePoints: Int = 50) : Fighter {
override fun attack(opponent: Fighter) {
println("Goblin attack (5)")
opponent.lifePoints -= 5
}
}
还有一些角色。让我们说,他们另外有名字。
interface Character : Fighter {
val name: String
get() = this.javaClass.name
}
基于类名的名称构造的行为已经是一个特质。尽管这只是一个开始。人物的类别很少。每个人都可以是 "巫师 "和/或 "战士"。由于我们不能扩展一个以上的类,我们不能使用继承来表达 "战士 "和 "巫师 "的能力。我们将使用特质来代替。比方说,战士可以使用近战攻击。命中率取决于他的力量,所以我们也需要要求这种属性。
interface Warrior : Character {
val strength: Int
fun meleeAttack(opponent: Fighter) {
println("$name melee attack with power $strength")
opponent.lifePoints -= strength
}
}
另一方面,"巫师 "可以 "施法"。他需要有一些法术和法力值。
interface Sorcerer : Character {
val spell: Spell
var manaPoints: Int
fun canCastSpell() = manaPoints > spell.manaCost
fun castSpell(opponent: Fighter) {
if (manaPoints < spell.manaCost) {
println("$name tried to cast spell but not enough mana")
return
}
println("$name cast spell ${spell.strength}")
manaPoints -= spell.manaCost
opponent.lifePoints -= spell.strength
}
override fun turnEnd() {
manaPoints += 1
}
}
注意,还声明了一个默认的法力值恢复方式(在turnEnd)。现在我们可以使用上述职业声明一些角色。比方说,我们有明斯克,强大的战士,所以他使用近战攻击进行战斗。
data class Minsk(
override var lifePoints: Int = 60
) : Warrior {
override val strength: Int = 15
override fun attack(opponent: Fighter) {
meleeAttack(opponent)
}
}
我们还有亚瑟,他既是一个战士又是一个巫师。他同时使用法术和近战攻击进行战斗。
data class Artur(
override var lifePoints: Int = 80,
override var manaPoints: Int = 10
) : Warrior, Sorcerer {
override var spell = Spell(4, 17)
override val strength: Int = 5
override fun attack(opponent: Fighter) {
if (canCastSpell()) {
castSpell(opponent)
} else {
meleeAttack(opponent)
}
}
}
我们可以模拟他们之间的一些战斗。
fun simulateCombat(c1: Fighter, c2: Fighter) {
while (c1.lifePoints > 0 && c2.lifePoints > 0) {
c1.attack(c2)
c2.attack(c1)
c1.turnEnd()
c2.turnEnd()
}
val text = when {
c1.lifePoints > 0 -> "$c1 won"
c2.lifePoints > 0 -> "$c2 won"
else -> "Both $c1 and $c2 are dead"
}
println(text)
}
simulateCombat(Artur(), Minsk())
Artur cast spell 17
Minsk melee attack with power 15
Artur cast spell 17
Minsk melee attack with power 15
Artur melee attack with power 5
Minsk melee attack with power 15
Artur cast spell 17
Minsk melee attack with power 15
Artur melee attack with power 5
Minsk melee attack with power 15
Artur(lifePoints=5, manaPoints=3) won
simulateCombat(Goblin(), Minsk())
Goblin attack (5)
Minsk melee attack with power 15
Goblin attack (5)
Minsk melee attack with power 15
Goblin attack (5)
Minsk melee attack with power 15
Goblin attack (5)
Minsk melee attack with power 15
Minsk(lifePoints=40) won
这个模式很强大,在很多情况下都能找到用途。但它有一个问题。特质不能保持状态。它们只能被用来重用行为。如果我们回到我们的订阅问题,我们将被迫在每个使用我们trait的类中实现订阅。
interface RxJavaUser {
var subscriptions: List<Subscription>
fun onDestroy() {
subscriptions.forEach { it.unsubscribe() }
}
}
class MainPresenter(): RxJavaUser {
var subscriptions: List<Subscription> = listOf()
}
随着过渡到RxJava 2,我们将被迫改变每个类中的订阅定义。这可不是好事。
那么最好的解决方案是什么呢?很抱歉,我没有一个完美的答案。我们已经看到了一些不同的尝试,它们都有一些优势和劣势。尽管我们也看到了这些尝试中的每一种都可以被用在非常有用的地方。我们已经看到了组合是如何帮助我们克服继承问题和表达更健康的关系。我们看到了委托是如何给我们带来多态行为的。最后,我们看到了特质,以及当我们需要表达类似于几个类的行为时,它们是多么强大。我希望你能在脑海中记住这些继承的替代方案,因为它们是重要的工具,可以使你的代码变得更好、更有表现力。
关于作者
Marcin Moskała (@marcinmoskala)是一名培训师和顾问,目前专注于举办Kotlin in Android和高级Kotlin研讨会(填写表格,以便我们可以讨论你的需求)。他也是一个演讲者、文章的作者和一本书关于Kotlin的Android开发。