嵌入与聚合
本课属于 Coddy GO 学习旅程的“面向对象编程”单元。第 38 课,共 107 课。
你已经学过,嵌入会将字段和方法提升到外层结构体中。但 Go 也支持聚合,也就是将另一个结构体作为具名字段包含进来。了解何时使用这两种方式是良好设计的关键。
使用嵌入时,你可以省略字段名,内层类型的成员就能直接访问:
type Writer struct{}
func (w Writer) Write() string { return "writing" }
type Document struct {
Writer // 嵌入,没有字段名
}
d := Document{}
d.Write() // 直接访问
使用聚合时,你需要为字段指定一个明确的名称,并通过该名称访问内部类型:
type Document struct {
writer Writer // 聚合:具名字段
}
d := Document{writer: Writer{}}
d.writer.Write() // 必须使用字段名
关键区别在于你表达的关系。嵌入表示“是一个”的关系,外层类型会采用内层类型的身份。聚合表示“拥有一个”的关系,外层类型只是包含内层类型。
如果你希望外部类型满足由内部类型实现的接口,或者直接访问方法能让 API 更简洁,就使用嵌入。如果内部类型是无需公开的实现细节,或者你想明确控制访问内部类型的方式,就使用聚合。
挑战
简单让我们构建一个报告生成系统,演示何时使用嵌入,何时使用聚合。你将创建一个系统,其中一些组件应可直接访问(嵌入),而其他组件应保留为内部实现细节(聚合)。
你将把代码分成三个文件:
formatter.go:创建一个包含Style字段(字符串)和Format(text string) string方法的Formatter结构体,该方法会将文本用样式包裹起来,例如[Style] text [/Style]。这是一个不应直接公开的内部辅助工具。report.go:创建两个以不同方式处理报告生成的结构体:- 创建一个包含
Author和Date字段的Metadata结构体。它表示应可直接访问的公开信息。 - 创建一个包含
Title字段的Report结构体。它嵌入Metadata(以便直接访问作者和日期),但对Formatter使用聚合(作为内部实现细节,通过名为formatter的字段保存)。
Report添加一个Generate() string方法,用内部格式化器设置标题的样式,并包含元数据。- 创建一个包含
main.go:从输入中读取报告详情,创建一个同时包含嵌入式元数据和聚合式格式化器的Report。通过直接从报告访问作者(使用嵌入),然后生成格式化报告(在内部使用隐藏的格式化器),来展示两者的区别。
将提供以下输入:
- 第 1 行:报告标题
- 第 2 行:作者姓名
- 第 3 行:日期
- 第 4 行:格式化器样式
你的 Format 方法应返回:
[Style] text [/Style]你的 Generate 方法应返回:
[formatted title]
Author: [Author], Date: [Date]在主文件中,打印三行:
- 直接从 Report 访问的作者(展示嵌入会提升字段的访问级别)
- 通过嵌入的类型名称访问的日期(
report.Metadata.Date) - 调用
Generate()的结果
例如,给定 Quarterly Results、Jane Doe、2024-01-15 和 BOLD,你的输出应为:
Jane Doe
2024-01-15
[BOLD] Quarterly Results [/BOLD]
Author: Jane Doe, Date: 2024-01-15注意,通过嵌入可以直接从 Report 访问 Author 和 Date,而格式化器仍然是隐藏的。你不能直接调用 report.Format(),因为它是通过命名字段聚合的。这展示了“是一个”和“拥有一个”这两种关系在实际中的区别。
亲手试试
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
scanner := bufio.NewScanner(os.Stdin)
scanner.Scan()
title := scanner.Text()
scanner.Scan()
author := scanner.Text()
scanner.Scan()
date := scanner.Text()
scanner.Scan()
style := scanner.Text()
// TODO:创建一个包含作者和日期的 Metadata 结构体
// TODO:使用给定的样式创建一个 Formatter
// TODO:创建一个嵌入 Metadata 并聚合 Formatter 的 Report
// TODO:打印直接通过 Report 访问的作者字段(嵌入会提升该字段)
// TODO:打印通过嵌入类型名称访问的日期(report.Metadata.Date)
// TODO:打印调用 Generate() 的结果
// 避免未使用变量的警告(使用这些变量时删除下面的代码)
_ = title
_ = author
_ = date
_ = style
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: Go 在线编译器