- 示例工程
【免费下载链接】awesome-low-level-design
Learn Low Level Design (LLD) and prepare for interviews using free resources.
聚合(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)**上。原文档用下表做了清晰对比:
| Feature | Aggregation | Composition |
|---|---|---|
| Relationship | "Has-a" | "Has-a" |
| Ownership | Contained objectcan exist independently | Contained objectcannot exist withoutthe container |
| Lifetime | Contained objectoutlivesthe container | Contained objectis destroyedwith the container |
| Example | University and Professors | Car 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 给出了三者对照表:
| Feature | Association | Aggregation | Composition |
|---|---|---|---|
| Relationship | "Knows-a" | "Has-a" | "Has-a" |
| Object Independence | Objects are independent | Contained objectcan exist independently | Contained objectcannot exist withoutthe container |
| Lifetime | Objects exist separately | Contained objectoutlivesthe container | Contained objectis destroyedwith the container |
| Example | Teacher and Student | University and Professors | Car 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.
相关推荐
StarRocks json_contains 函数完全指南:JSON 包含关系判定与源码级实现解析
StarRocks json_contains 函数完全指南:JSON 包含关系判定与源码级实现解析 json_contains 是 StarRocks 提供的
示例工程awesome-low-level-design设计模式组合
awesome low level design设计模式组合 在软件开发中,设计模式的灵活组合能够有效解决复杂问题。本文将通过装饰器模式与其他模式的组合应用,展
示例工程深入理解Go语言中的接口设计——以awesome-low-level-design项目为例
深入理解Go语言中的接口设计——以awesome low level design项目为例 引言 在Go语言的面向对象编程 OOP 中,接口 interface
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考