组合
面向对象编程最具有吸引力的一个论点是代码重用。
你可能最初会将“重用”理解为“复制代码”。复制似乎是一个简单的解决方案,但它效果不是很好。随着时间的推移,您的需求会发生变化。对已复制的代码应用更改会变成一个维护噩梦。您是否找到了所有的副本?您是否以相同的方式对每个副本进行了更改?通过重用的代码,您只需要在一个地方进行更改。
在面向对象编程中,通过创建新类来重用代码,但与其从头开始创建新类,不如使用其他人已经构建和调试过的现有类。关键在于在不污染现有代码的情况下使用这些类。
继承是实现这一点的一种方式。继承将一个新类创建为现有类的类型。您可以向现有类的形式添加代码,而无需修改原始类。继承是面向对象编程的基石。
您还可以选择更直接的方法,通过在新类内部创建现有类的对象。这称为组合,因为新类由现有类的对象组成。您正在重用代码的功能,而不是其形式。
本书中经常使用组合。组合经常被忽视,因为它似乎如此简单 —— 您只需将一个对象放在一个类中。
组合是一种有一个的关系。 “一个房子 是一个 建筑物并且 有一个 厨房” 可以这样表示:
// Composition/House1.kt
package composition1
interface Building
interface Kitchen
interface House: Building {
val kitchen: Kitchen
}
继承描述了一个是一个的关系,通常有助于将描述大声朗读出来:“一个房子是一个建筑物。” 这听起来是正确的,不是吗?当是一个关系有意义时,继承通常是合理的。
如果您的房子有两个厨房,组合可以提供一个简单的解决方案:
// Composition/House2.kt
package composition2
interface Building
interface Kitchen
interface House: Building {
val kitchen1: Kitchen
val kitchen2: Kitchen
}
要允许任意数量的厨房,使用带有集合的组合:
// Composition/House3.kt
package composition3
interface Building
interface Kitchen
interface House: Building {
val kitchens: List<Kitchen>
}
我们花费时间和精力来理解继承,因为它更复杂,而且这种复杂性可能会给人一种它在某种程度上更重要的印象。相反:
优先选择组合而不是继承。
组合会产生更简单的设计和实现。这并不意味着您应该避免继承。只是我们往往陷入了更复杂的关系中。优先选择组合而不是继承 这一原则提醒我们要退后一步,看看我们的设计,思考是否可以通过组合来简化它。最终的目标是正确应用您的工具并产生一个良好的设计。
组合似乎很琐碎,但却很强大。当一个类增长并且负责不同的不相关的事情时,组合有助于将它们分开。使用组合来简化类的复杂逻辑。
在组合和继承之间选择
组合和继承都会将子对象放置在您的新类内部 —— 组合具有显式的子对象,而继承具有隐式的子对象。何时选择其中之一?
组合提供了现有类的功能,但不包括其接口。您嵌入一个对象以在新类中使用其功能,但用户看到的是您为新类定义的接口,而不是嵌入对象的接口。为了完全隐藏对象,将其私有嵌入:
// Composition/Embedding.kt
package composition
class Features {
fun f1() = "feature1"
fun f2() = "feature2"
}
class Form {
private val features = Features()
fun operation1() =
features.f2() + features.f1()
fun operation2() =
features.f1() + features.f2()
}
Features
类为 Form
的操作提供了实现,但使用 Form
的客户程序员无法访问 features
—— 实际上,用户对于 Form
的实现方式是无感知的。这意味着,如果您找到了一种更好的实现 Form
的方法,可以删除 features
并更改为新的方法,而不会影响调用 Form
的代码。
如果 Form
继承了 Features
,客户程序员可能会期望将 Form
向上转型为 Features
。继承关系随后成为 Form
的一部分 —— 这种联系是显式的。如果您改变了这一点,会破坏依赖于该连接的代码。
有时候,允许类用户直接访问您的新类的组合是有意义的;也就是说,使成员对象变为公开。这是相对安全的,假设成员对象使用了适当的实现隐藏。对于某些系统,这种方法可以使接口更易于理解。考虑一个 Car
:
// Composition/Car.kt
package composition
import atomictest.*
class Engine {
fun start() = trace("
Engine start")
fun stop() = trace("Engine stop")
}
class Wheel {
fun inflate(psi: Int) =
trace("Wheel inflate($psi)")
}
class Window(val side: String) {
fun rollUp() =
trace("$side Window roll up")
fun rollDown() =
trace("$side Window roll down")
}
class Door(val side: String) {
val window = Window(side)
fun open() = trace("$side Door open")
fun close() = trace("$side Door close")
}
class Car {
val engine = Engine()
val wheel = List(4) { Wheel() }
// 两扇门:
val leftDoor = Door("left")
val rightDoor = Door("right")
}
fun main() {
val car = Car()
car.leftDoor.open()
car.rightDoor.window.rollUp()
car.wheel[0].inflate(72)
car.engine.start()
trace eq """
left Door open
right Window roll up
Wheel inflate(72)
Engine start
"""
}
Car
的组合是问题分析的一部分,而不仅仅是底层实现的一部分。这有助于客户程序员理解如何使用类,同时也需要更少的代码复杂性来创建类。
当您继承时,您会为现有类创建一个定制版本。这将一个通用的类专门用于满足特定的需求。在这个示例中,使用一个 Vehicle
类来构建一个新类 Car
是没有意义的 —— Car
不是 包含 Vehicle
,而是 是 一个 Vehicle
。继承关系通过继承来表达,而有一个关系通过组合来表达。
多态的巧妙之处可能使人们觉得一切都应该继承。这将使您的设计变得繁琐。实际上,当您在使用现有类来构建新类时,首先选择组合可能会使事情变得不必要地复杂。更好的方法是首先尝试组合,尤其是当不清楚哪种方法最适合时。
习题和解答可以在 www.AtomicKotlin.com 找到。