Menu
CoddyTech

嵌入与聚合

本课属于 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 更简洁,就使用嵌入。如果内部类型是无需公开的实现细节,或者你想明确控制访问内部类型的方式,就使用聚合。

challenge icon

挑战

简单

让我们构建一个报告生成系统,演示何时使用嵌入,何时使用聚合。你将创建一个系统,其中一些组件应可直接访问(嵌入),而其他组件应保留为内部实现细节(聚合)。

你将把代码分成三个文件:

  • 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]

在主文件中,打印三行:

  1. 直接从 Report 访问的作者(展示嵌入会提升字段的访问级别)
  2. 通过嵌入的类型名称访问的日期(report.Metadata.Date)
  3. 调用 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
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: Go 在线编译器