为什么 Go 没有继承
本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 34 课,共 107 课。
Java 和 C++ 等传统面向对象语言会使用继承在不同类型之间共享代码。子类扩展父类,并继承其所有字段和方法。Go 则有意省略了这一特性。
继承会让类型之间产生紧密耦合。父类发生变化时,所有子类都会受到影响。
深层继承层次结构会变得难以理解和维护。“脆弱基类问题”是指对基类的修改意外导致派生类出错。
Go 的设计者选择了另一条路:组合优于继承。Go 鼓励你不要说“Dog 是 Animal”,而是说“Dog 具有类似 Animal 的行为”。这种细微的转变能让代码更灵活、更易维护。
Go 通过两种你已经学过的机制实现代码复用:
- 接口定义行为契约,不包含实现细节
- 结构体嵌入允许类型包含其他类型并复用它们的方法。
考虑一下这种对比。在传统的 OOP 中,你可能会写 class Dog extends Animal。在 Go 中,你可以在 Dog 结构体中嵌入 Animal 结构体,并实现共享接口。这样得到的功能类似,但类型之间的耦合更松散。
本章将深入探讨结构体嵌入,展示 Go 如何在避免继承缺点的同时,获得继承带来的好处。
挑战
简单让我们构建一个通知系统,展示 Go 的组合方式,而不是继承。你将创建一些类型,通过接口和结构体嵌入来共享行为,而不是使用类层次结构。
你将把代码分成三个文件:
notifier.go:定义一个Notifier接口,其中只有一个方法Notify(message string) string。再创建一个BaseNotifier结构体,其中有一个Name字段,其他类型会嵌入这个结构体。为BaseNotifier添加一个名为Format(message string) string的方法,该方法会在消息前加上方括号括起来的通知器名称。channels.go:创建两种嵌入BaseNotifier并实现Notifier接口的通知渠道类型:EmailNotifier,额外包含一个Address字段SMSNotifier,额外包含一个Phone字段
Notify方法都应使用嵌入的Format方法,并在输出中包含该类型特有的渠道信息。main.go:创建一个名为SendAlert的函数,该函数接受任意一个Notifier和一条消息,然后返回调用Notify的结果。从输入中读取通知详情,创建这两种通知器,并演示如何通过接口交替使用它们。
将提供以下输入:
- 第 1 行:电子邮件通知器名称
- 第 2 行:电子邮件地址
- 第 3 行:SMS 通知器名称
- 第 4 行:电话号码
- 第 5 行:提醒消息
你的 BaseNotifier 的 Format 方法应返回:
[Name] message你的 Notify 方法应返回:
- EmailNotifier:
Email to [Address]: [formatted message] - SMSNotifier:
SMS to [Phone]: [formatted message]
例如,给定 Alerts、user@mail.com、Urgent、555-1234 和 Server down,输出应为:
Email to user@mail.com: [Alerts] Server down
SMS to 555-1234: [Urgent] Server down注意,两种通知器类型都通过嵌入复用 BaseNotifier 中的 Format 方法,同时各自提供自己的 Notify 实现。SendAlert 函数可以处理任何 Notifier,而无需了解具体类型。这就是组合优于继承的实际应用。
亲手试试
package main
import (
"bufio"
"fmt"
"os"
)
// SendAlert 接收任意 Notifier 和一条消息,返回调用 Notify 的结果
// TODO: 实现 SendAlert 函数
func main() {
scanner := bufio.NewScanner(os.Stdin)
// 读取电子邮件通知器的名称
scanner.Scan()
emailName := scanner.Text()
// 读取电子邮件地址
scanner.Scan()
emailAddress := scanner.Text()
// 读取 SMS 通知器的名称
scanner.Scan()
smsName := scanner.Text()
// 读取电话号码
scanner.Scan()
phoneNumber := scanner.Text()
// 读取提醒消息
scanner.Scan()
alertMessage := scanner.Text()
// TODO: 使用 emailName 和 emailAddress 创建 EmailNotifier
// TODO: 使用 smsName 和 phoneNumber 创建 SMSNotifier
// TODO: 使用 SendAlert 通过两个通知器发送 alertMessage
// 并打印结果
// 避免未使用变量的警告(使用这些变量后,移除这些语句)
_ = emailName
_ = emailAddress
_ = smsName
_ = phoneNumber
_ = alertMessage
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: Go 在线编译器