☰
Go 聚合关系(Aggregation)完全指南:以 awesome-low-level-design 仓库为例掌握 “has-a“ 组合设计
2026/9/30 1:53:14 网站建设 项目流程
  • 示例工程

【免费下载链接】awesome-low-level-design

Learn Low Level Design (LLD) and prepare for interviews using free resources.

项目地址:https://gitcode.com/GitHub_Trending/aw/awesome-low-level-design
点击查看免费下载

聚合(Aggregation)是面向对象设计中承载 "has-a" 关系的核心概念:容器持有被包含对象的引用,但两者的生命周期彼此独立。本文以 awesome-low-level-design 仓库中 oop/golang/aggregation/README.md 的讲解为主线,结合仓库内 Go 版低层设计(LLD)源码,系统说明聚合的定义、与组合(Composition)的差异、基于接口的聚合写法以及何时选用聚合,读完你可以在 Go 面试题与系统设计实战中准确建模"独立对象被共享持有"的关系。

什么是聚合(Aggregation)

在面向对象编程中,聚合表示两个类(在 Go 中即两个 struct)之间的 "has-a" 关系,但其关键区别在于:被包含对象的生命周期与容器对象相互独立。也就是说,当一个 struct 持有另一个 struct 时,被包含的 struct 可以脱离容器单独存在。

从 Go 语言的实现角度看,聚合通常通过**指针(引用)**字段来表达:容器 struct 中存放的是*T或[]*T类型的引用,而不是值拷贝。这保证了被引用的对象可以独立于容器被创建、销毁或在应用的其他地方复用。

聚合的关键特征

  • 表示has-a关系;
  • 被包含对象可以独立于容器存在;
  • 通过**引用(指针)**实现;
  • 促进对象之间的松耦合(loose coupling)。

示例:大学与其教授

考虑这样一个场景:一个University包含多名Professor,但Professor可以脱离任何一所大学独立存在——这正是聚合的典型例子。原文档给出了完整的可运行代码:

package main import "fmt" // Professor struct (independent entity) type Professor struct { Name string Subject string } func (p Professor) Teach() { fmt.Printf("%s is teaching %s\n", p.Name, p.Subject) } // University struct contains a list of professors (aggregation) type University struct { Name string Professors []*Professor // Aggregation: University has a list of professors } func (u *University) AddProfessor(professor *Professor) { u.Professors = append(u.Professors, professor) } func (u University) ShowProfessors() { fmt.Printf("Professors at %s:\n", u.Name) for _, professor := range u.Professors { fmt.Printf(" - %s\n", professor.Name) } } func main() { prof1 := &Professor{Name: "Dr. Smith", Subject: "Computer Science"} prof2 := &Professor{Name: "Dr. Johnson", Subject: "Mathematics"} university := &University{Name: "Harvard University"} university.AddProfessor(prof1) university.AddProfessor(prof2) university.ShowProfessors() // Professors can exist independently prof1.Teach() prof2.Teach() }

输出结果:

Professors at Harvard University: - Dr. Smith - Dr. Johnson Dr. Smith is teaching Computer Science Dr. Johnson is teaching Mathematics

注意main函数末尾:prof1.Teach()与prof2.Teach()完全绕过university直接调用,演示了"教授不依赖大学也能工作"的独立性——这正是聚合与组合在生命周期上的本质区别。此外,University.AddProfessor使用指针接收者修改切片,而ShowProfessors使用值接收者只读遍历,这也是 Go 中"需要修改状态用指针接收者、只读操作可用值接收者"的常见实践。

聚合 vs 组合(Aggregation vs Composition)

聚合与组合都是 "has-a" 关系,差异集中在**所有权(Ownership)与生命周期(Lifetime)**上。原文档用下表做了清晰对比:

FeatureAggregationComposition
Relationship"Has-a""Has-a"
OwnershipContained objectcan exist independentlyContained objectcannot exist withoutthe container
LifetimeContained objectoutlivesthe containerContained objectis destroyedwith the container
ExampleUniversity and ProfessorsCar and Engine

仓库中 oop/golang/composition/README.md 的Car与Engine、Wheel、Transmission示例是组合的典型对照:Car以值字段直接内嵌组件(Engine Engine),组件随Car一起构造、一起消亡,离开Car便没有独立存在的意义。而聚合的University持有的是[]*Professor引用集合,删除大学并不会销毁教授对象。

从内存与生命周期角度理解差异

用 Go 的语义可以这样区分:

  • 组合:容器通过值字段"拥有"部件,部件生命周期与容器严格绑定,销毁容器即销毁部件;
  • 聚合:容器通过指针"引用"部件,部件在其自身作用域中被管理,可以在多个容器间共享,容器销毁不影响部件继续存活。

关联关系(Association)的补充定位

聚合其实是关联(Association)关系的一种特化。仓库 oop/golang/association/README.md 给出了三者对照表:

FeatureAssociationAggregationComposition
Relationship"Knows-a""Has-a""Has-a"
Object IndependenceObjects are independentContained objectcan exist independentlyContained objectcannot exist withoutthe container
LifetimeObjects exist separatelyContained objectoutlivesthe containerContained objectis destroyedwith the container
ExampleTeacher and StudentUniversity and ProfessorsCar and Engine

三者由弱到强依次为:关联(纯 "knows-a",如 Teacher 与 Student)→ 聚合("has-a" 且生命周期独立)→ 组合("has-a" 且生命周期绑定)。

为什么使用聚合(Why Use Aggregation)

原文档从四个维度阐述了聚合的价值:

1. 促进代码复用(Promotes Code Reusability)

被聚合的对象可以在多处使用,而不必与单一容器 struct 紧耦合。例如同一个Professor既可以属于University,也可以被邀请去Conference做报告,无需修改Professor本身。

2. 鼓励松耦合(Encourages Loose Coupling)

聚合允许对象相互协作,却不必依赖彼此的生命周期。容器只依赖被包含对象的公开接口,而不是其内部实现细节。

3. 更好的可维护性(Better Maintainability)

一个 struct 的变更不会对另一个造成严重影响,代码库更容易修改与扩展。

4. 贴近真实世界的适用性(Real-World Applicability)

许多真实关系天然符合聚合模型,例如学校与教师、公司与雇员——这些对象在现实中本来就可以独立存在,代码建模与业务语义保持一致,降低了理解成本。

聚合与接口的结合(Aggregation with Interfaces)

原文档强调:通过接口可以进一步增强聚合的灵活性。把聚合字段的类型从具体 struct 提升为接口后,容器可以聚合任何实现该接口的类型,而不局限于某一种具体类型。

package main import "fmt" // Teachable interface type Teachable interface { Teach() } // Professor struct implementing Teachable interface type Professor struct { Name string Subject string } func (p Professor) Teach() { fmt.Printf("%s is teaching %s\n", p.Name, p.Subject) } // University struct containing a list of professors type University struct { Name string Professors []Teachable } func (u *University) AddProfessor(professor Teachable) { u.Professors = append(u.Professors, professor) } func (u University) ShowProfessors() { fmt.Printf("Professors at %s:\n", u.Name) for _, professor := range u.Professors { professor.Teach() } } func main() { prof1 := Professor{Name: "Dr. Adams", Subject: "Physics"} prof2 := Professor{Name: "Dr. Lee", Subject: "Chemistry"} university := &University{Name: "MIT"} university.AddProfessor(prof1) university.AddProfessor(prof2) university.ShowProfessors() }

输出结果:

Professors at MIT: Dr. Adams is teaching Physics Dr. Lee is teaching Chemistry

这段代码体现了 Go 接口的隐式实现特性:Professor无需显式声明"我实现了Teachable",只要拥有Teach()方法,就能被当作Teachable存入University.Professors。仓库 oop/golang/interfaces/README.md 详细说明了这一机制:接口定义一组方法签名,类型通过实现这些方法来满足接口;接口还支持组合(如CarInterface内嵌Engine与Transmission),这与 struct 层面的组合/聚合形成呼应——在 Go 中,"组合"不仅发生在 struct 之间,也发生在接口之间。

相比具体类型的聚合,接口聚合带来了两个实战收益:

  • 可替换性:日后新增AssistantProfessor、VisitingProfessor等类型,只要实现Teach(),University无需任何改动;
  • 解耦:University依赖的是Teachable契约而非具体类型,符合依赖倒置原则(DIP)的方向。

仓库实战印证:Library Management System 中的聚合

为印证聚合在真实 LLD 题目中的落地方式,仓库 Go 版解决方案 solutions/golang/librarymanagementsystem/ 是一个极佳案例。其核心关系如下:

  • LibraryManager 持有catalog map[string]*Book与members map[string]*Member——容器持有的是指针(引用),而非值拷贝,这正是聚合的 Go 语言特征;
  • Member 持有borrowedBooks map[string]*Book——成员聚合了自己借阅的书籍引用,这些Book对象同时存在于LibraryManager的目录中,同一本书可以被多个成员(聚合方)共享引用,且Book的生命周期由LibraryManager的目录管理,Member被注销时其借阅映射随之清理,但书本身依然存在;
  • Book 通过IsAvailable()/SetAvailable()维护借阅状态,BorrowBook流程(见 library_manager.go)在成员上限maxBooksPerMember = 5、借期loanDurationDays = 14等约束下协调双方。

可以推断,这种"目录拥有书籍、成员聚合书籍"的设计,与大学-教授的聚合关系同构:书籍属于馆藏(组合意义上的归属),但成员只是借阅持有引用,成员与书都能独立存在。如果你在 LLD 面试中被问到"Member 与 Book 是什么关系",正确的回答是聚合——因为书不随成员消失而消失。

何时使用聚合(When to Use Aggregation)

原文档给出了清晰的决策指引,在以下场景优先选择聚合:

  • 当一个对象可以独立于容器存在时;
  • 在设计松耦合系统时;
  • 当不同对象需要在多个容器之间共享时;
  • 当遵循SOLID 原则,特别是**依赖倒置原则(DIP)**时。

结合前面的对比可以总结出快速决策法:问自己"容器销毁后,被包含对象是否还需要继续存活?"——需要存活用聚合(指针引用),随容器一起消亡用组合(值字段);如果两者之间连 "has-a" 都谈不上、只是短暂协作的 "knows-a",则属于关联而非聚合。在 Go 中,判断聚合关系最直接的代码信号,就是字段类型是否以*(指针)开头:[]*Professor、map[string]*Book都是聚合的典型形态。

  • 示例工程

【免费下载链接】awesome-low-level-design

Learn Low Level Design (LLD) and prepare for interviews using free resources.

项目地址:https://gitcode.com/GitHub_Trending/aw/awesome-low-level-design
点击查看免费下载
上一篇:如何通过mssql-scripter实现SQL Server版本迁移的自动化脚本生成
下一篇:emilianJR/chilloutmix_NiPrunedFp32Fix提示词数据库:社区共享资源

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询