☰
Go 组合(Composition)实战指南:以 Has-A 关系构建可复用、可替换的软件系统
2026/9/30 1:56:06 网站建设 项目流程
  • 示例工程

【免费下载链接】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
点击查看免费下载

本篇指南以仓库中 oop/golang/composition/README.md 为核心,系统讲解 Go 语言中组合(Composition)的概念、代码形态、与继承/聚合/关联的差异,并结合本仓库design-patterns/golang/下的真实设计模式实现,展示组合在工程中的落地方式。读完本文,你将掌握在 Go 中通过组合构建复杂对象、用接口实现运行时替换行为,以及如何判断何时该选择组合而非继承。

什么是组合(Composition)

组合是面向对象编程(OOP)中的核心概念之一,它允许用一个对象去构建另一个对象,从而促进代码复用、灵活性与可维护性。与继承建立的"is-a"(是一个)关系不同,组合表达的是"has-a"(拥有一个)关系。

在 Go 中,组合的实现方式是:一个 struct 将另一个 struct 的实例(或实例的接口)作为自己的字段。被包含的 struct 通常称为组件(component),包含方称为复合 struct(composite struct)。通过把简单的对象组合起来,可以构建出复杂的系统。

示例:一辆 Car 及其组件

考虑一辆Car,它由Engine、Wheel、Transmission等多个组件构成。与其从这些组件去"继承",不如让Car对象把它们作为字段包含进来:

package main import "fmt" type Engine struct { Horsepower int } func (e Engine) Start() { fmt.Printf("Engine started with %d HP.\n", e.Horsepower) } type Wheel struct { Type string } func (w Wheel) Rotate() { fmt.Printf("The %s wheel is rotating.\n", w.Type) } type Transmission struct { Type string } func (t Transmission) ShiftGear() { fmt.Printf("Transmission shifted: %s\n", t.Type) } type Car struct { Engine Engine Wheel Wheel Transmission Transmission } func NewCar() Car { return Car{ Engine: Engine{Horsepower: 150}, Wheel: Wheel{Type: "Alloy"}, Transmission: Transmission{Type: "Automatic"}, } } func (c Car) Drive() { c.Engine.Start() c.Wheel.Rotate() c.Transmission.ShiftGear() fmt.Println("Car is moving!") } func main() { car := NewCar() car.Drive() }

运行输出:

Engine started with 150 HP. The Alloy wheel is rotating. Transmission shifted: Automatic Car is moving!

这个示例体现了组合的三个要点:

  • 字段承载:Car通过字段持有Engine、Wheel、Transmission三个独立组件;
  • 构造封装:NewCar()集中负责各组件初始状态的装配(150 马力、合金轮、自动变速箱),调用方无需关心组件细节;
  • 行为编排:Drive()逐一调用组件方法,把多个独立能力编排为整体行为。

Engine、Wheel、Transmission各自独立,它们不知道Car的存在——这正是组合与继承的本质区别:组件可以独立存在、独立复用。

为什么优先组合而非继承(Favor Composition over Inheritance)

"优先组合而非继承"是一条被广泛认可的设计原则,其背后有四条关键理由。

1. 封装性与灵活性

组合允许在运行时通过替换组件来动态改变对象行为;而继承一旦建立,修改类层次结构往往会破坏现有代码。例如,把Car.Engine从汽油引擎换成电动引擎,只需替换字段值,无需改动Car的任何方法。

2. 更好的代码复用

组合促进可复用组件的诞生。Engine、Wheel、Transmission这些 struct 可以在多种车型(Car、Bike、Truck)中直接复用而无需修改。组件是自包含的单元,复用的粒度更小、耦合更弱。

3. 避免继承的陷阱

继承容易导致深层的类层次结构,维护困难;它强制建立严格的父子关系,对某些设计而言过于僵化。层数越深,修改底层父类带来的涟漪效应就越大,而组合把这种影响限制在组合方内部。

4. 支持基于接口的设计

组合可以与接口结合,实现强解耦。调用方只依赖接口,不依赖具体实现——这也是依赖倒置原则(DIP)在 Go 中的自然表达。

组合与接口:运行时替换行为的钥匙

仅靠字段组合是"静态"的——Car的引擎类型在编译期就确定了。要让行为可以在运行时替换,就需要把字段类型改为接口。原文档给出了PetrolEngine/DieselEngine两种引擎的实现:

package main import "fmt" type Engine interface { Start() } type PetrolEngine struct {} func (p PetrolEngine) Start() { fmt.Println("Petrol Engine started.") } type DieselEngine struct {} func (d DieselEngine) Start() { fmt.Println("Diesel Engine started.") } type Car struct { engine Engine } func (c Car) StartCar() { c.engine.Start() fmt.Println("Car is ready to go!") } func main() { petrolCar := Car{engine: PetrolEngine{}} petrolCar.StartCar() dieselCar := Car{engine: DieselEngine{}} dieselCar.StartCar() }

运行输出:

Petrol Engine started. Car is ready to go! Diesel Engine started. Car is ready to go!

这里的核心变化是:Car的字段类型从具体的Enginestruct 变成了Engine接口。于是:

  • 同一个Car结构可以容纳任意实现了Start()的类型,未来新增ElectricEngine、HybridEngine无需改动Car;
  • Car与具体引擎实现彻底解耦,它只依赖Engine接口约定的行为契约;
  • 这种"面向接口组合"正是依赖注入的雏形,让单元测试时可以用假引擎替换真实引擎。

注意示例中Car构造使用字面量Car{engine: PetrolEngine{}}而非构造函数,这是因为engine字段是小写(包内私有),无法在包外直接注入;在生产代码中,通常通过构造函数或 setter 完成注入。

组合 vs 聚合 vs 关联:三种关系如何区分

组合容易与聚合(Aggregation)、关联(Association)混淆。三者都表达对象之间的联系,但生命周期归属与独立性截然不同。仓库中 oop/golang/aggregation/README.md 与 oop/golang/association/README.md 给出了完整的对比:

特性关联(Association)聚合(Aggregation)组合(Composition)
关系语义Knows-a(认识)Has-a(拥有)Has-a(拥有)
对象独立性双方完全独立被包含对象可以独立存在被包含对象不能脱离容器存在
生命周期各自独立存活被包含对象比容器活得久被包含对象随容器销毁
典型示例教师和学生大学和教授汽车和引擎

三个概念的关键判别点:

  • 关联最松散:Teacher认识Student,两者生命周期完全无关,语义只是"知道彼此存在",可以是单向或双向;
  • 聚合是弱拥有:University拥有Professors,但教授辞职后依然作为独立个体存在,容器销毁不影响组件。实现上通常用指针/切片引用(如[]*Professor);
  • 组合是强拥有:Car的Engine无法脱离汽车独立发挥意义,组件的生命周期由容器管理。实现上用值字段(如Engine而非*Engine)。

在 Go 中,这种差异可以直接从字段形态读出:值类型字段倾向组合,指针/切片引用倾向聚合。Go 没有显式的"删除对象"语义(由 GC 决定回收),但设计上区分这两种关系,能帮助你在建模时想清楚"谁拥有谁的生命周期"。

仓库实战:设计模式源码中的组合

本仓库的 design-patterns/golang/ 目录中,多个经典设计模式都以组合为骨架实现。以下实例可以直接验证"组合是 Go 工程的默认组织方式"。

装饰器模式:组合的层层包装

装饰器模式是组合的典型应用——用一个装饰器包住另一个对象,逐层叠加行为。看 beverage_decorator.go:

// BeverageDecorator is the base decorator that wraps a Beverage type BeverageDecorator struct { beverage Beverage }

而具体的装饰器通过内嵌方式复用基类组合能力,milk_decorator.go:

type MilkDecorator struct { *BeverageDecorator }

MilkDecorator内嵌*BeverageDecorator,本质是"组合 + 方法提升",再覆写GetDescription()与Cost()增加" with Milk"和+0.5费用。配合 simple_coffee.go(基础咖啡成本 1.0),可以任意叠加牛奶、糖等装饰器而不改动既有类——这正是组合带来的"开闭原则"优势。

策略模式:组合 + 接口实现运行时行为切换

shopping_cart.go 中,购物车通过接口字段持有支付策略:

type ShoppingCart struct { amount float64 strategy PaymentStrategy }

SetPaymentStrategy(strategy PaymentStrategy)在运行时替换策略;Checkout()调用strategy.Pay(c.amount)。具体的 credit_card_payment.go、PayPalPayment各自实现Pay。这与原文档Car+Engine接口的写法完全同构——策略模式就是"组合 + 接口"的封装形态。

责任链模式:对象组合成链

base_handler.go 用接口字段把处理器串成链:

type BaseHandler struct { next RequestHandler }

每个处理器组合"下一个处理器",HandleNext把请求向后传递。链条的"后继关系"由对象组合而非继承表达,新增一种校验、鉴权或限流处理器只需实现接口并挂到链上。

桥接模式:抽象与实现解耦

shape.go 中BaseShape组合了渲染器接口:

type BaseShape struct { renderer Renderer }

形状(Circle/Rectangle)与渲染方式(Raster/Vector)各自独立变化,通过组合桥接起来——这正是"组合优于继承"用于解决多维度变化问题的经典场景。

组合模式:递归的树形组合

composite/folder.go 中Folder持有children []FileSystemItem(文件与文件夹的统一接口),实现文件系统的树形递归结构。这里的"组合"既是模式名,也字面意义上用组合实现了任意深度的嵌套。

这些实例共同印证:在 Go 中,组合不是一种可选项,而是语言层面的默认工程手段——因为 Go 本身不支持类继承(只有 struct 内嵌与接口),"通过字段/接口组装对象"是唯一且自然的组织方式。仓库 oop/golang/ 目录下还提供了 abstraction、encapsulation、inheritance、polymorphism、aggregation、association 等配套主题的文档,可以对照阅读,构建完整的 Go OOP 知识图谱。

何时使用组合

综合原文档,以下场景优先选择组合:

  • 构建由多个组件构成的复杂对象时(如Car由引擎、车轮、变速箱组装);
  • 希望在不引入僵化继承层次的前提下实现代码复用时;
  • 需要在运行时动态切换不同行为时(如车辆使用不同类型的引擎,购物车切换支付策略);
  • 遵循"favor composition over inheritance"(优先组合而非继承)原则时。

反向信号同样清晰:如果你只需要复用某类型的全部行为、且不存在"多维度独立变化",可以考虑 struct 内嵌(Go 对继承的近似替代);如果组件需要脱离容器独立存活(教授离职后依然存在),则应选择聚合而非组合——具体可对照 oop/golang/aggregation/README.md 中的判别标准。

小结

组合是 Go 中构建可复用、可替换系统的基本功。通过"值字段组合"实现静态装配,通过"接口字段组合"实现运行时多态,再配合聚合与关联的边界辨析,你就能在建模时准确回答"谁拥有谁、谁的生命周期归谁"。仓库中 oop/golang/composition/README.md 提供了基础定义与可运行示例,而 design-patterns/golang/ 下五个模式的源码,则展示了组合在装饰器、策略、责任链、桥接、组合模式中的真实落地——建议动手运行这些示例,把"组合优于继承"从原则变成肌肉记忆。

  • 示例工程

【免费下载链接】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
点击查看免费下载
上一篇:Area51模型材质置换:高度图与细节级别
下一篇:Sneedacity插件完全手册:安装、管理与15款必备效果插件推荐

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

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

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

立即咨询