Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

组合

面向对象编程最具有吸引力的一个论点是代码重用。

你可能最初会将“重用”理解为“复制代码”。复制似乎是一个简单的解决方案,但它效果不是很好。随着时间的推移,您的需求会发生变化。对已复制的代码应用更改会变成一个维护噩梦。您是否找到了所有的副本?您是否以相同的方式对每个副本进行了更改?通过重用的代码,您只需要在一个地方进行更改。

在面向对象编程中,通过创建新类来重用代码,但与其从头开始创建新类,不如使用其他人已经构建和调试过的现有类。关键在于在不污染现有代码的情况下使用这些类。

继承是实现这一点的一种方式。继承将一个新类创建为现有类的类型。您可以向现有类的形式添加代码,而无需修改原始类。继承是面向对象编程的基石。

您还可以选择更直接的方法,通过在新类内部创建现有类的对象。这称为组合,因为新类由现有类的对象组成。您正在重用代码的功能,而不是其形式。

本书中经常使用组合。组合经常被忽视,因为它似乎如此简单 —— 您只需将一个对象放在一个类中。

组合是一种有一个的关系。 “一个房子 是一个 建筑物并且 有一个 厨房” 可以这样表示:

// 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 找到。