程序设计#3%1 - 面向对象程序设计
本篇教程不是必须的,我们写的算法题一般用不上面向对象程序设计(除了少数实现数据结构的题目)。但是因为非常重要,它作为程序设计的一条支线发布。面向对象程序设计是程序设计#3 - 结构体和数据结构入门的支线。
摘要:面向对象程序设计是基于对象的编程。对象是对现实世界事物的抽象表示,并将具有相同特征和行为的事物归类为类。Go 语言通过结构体、方法和接口来实现面向对象编程的特性。
本文通过汽车的例子来减小理解概念的困难。为了避免打断理解,完整的代码示例被移动到了额外部分。
从过程到对象
我们在 #2 以及 #2 之前的教程中设计的程序都有一个明确的过程。比如,题目中给了一个数组,我们应该对这个数组做些操作才能符合样例的要求。这些操作通过变量赋值和流程控制实现。因为我们希望实现具体的目标,所以就一定要依赖一系列步骤。像这样强调过程的程序设计,我们叫做面向过程程序设计(POP,procedure oriented programming)。面向过程的程序只要程序结束了,事情就完成了。
面向过程的程序优点在于,这样写的程序通常直观高效。例如我们发现哪一步出问题了,只需要在出错的地方修改就行。
但代码量堆叠起来,堆到了几十万行,那面向过程程序是很麻烦的,因为出一点问题就可能导致完整的项目要大修。我们希望有一个办法,把程序的每个操作改成某个对象(object,它是本条支线的主角)的动作。这样,我们在操作程序时就只需要让对象做出指定的动作就行了。
我们以造汽车和开车为例,先看看用面向过程的思想1解决这个问题会是怎样。
- 造引擎
- 制造外壳
- 安装螺丝 A B C D
- 安装活塞……
- 造车身
- 焊接钢板 W X Y Z
- 喷涂喷漆……
- 开车
- 拧钥匙(方向盘右侧)
- 检查油压
- 如果油压足够,那么启动车辆成功。否则失败。
- 踩离合器
- 挂挡……
这样看来,面向过程的程序确实很直观,因为它把所有可能的步骤都写进去了。
但有个问题,如果不想开手动挡汽车,换成了自动档汽车,那这个过程就需要几乎完全重写。因为踩离合器,挂档的逻辑完全变了。我们把这种各组件之间依赖性特别强的特点叫做高度耦合。
如果我们发现引擎有一枚螺丝需要换型号,那我们就要从所有的汽车中找到这枚螺丝,在这个过程中非常容易出现缺漏和出错。如果要加上新的活塞,也有这种隐患。这就是面向过程的第二个问题,难以维护和扩展。
而且更关键的是,程序员必须百分百了解汽车从生产到实际使用的过程以及所有的数据,因为这部车的所有内容都暴露在外,万一被破坏者修改了数据,就会导致不可逆的错误,程序员必须及时改正。这种风险大大加重了程序员的负担。
接下来换一个方法,我们用面向对象程序设计(OOP,object oriented programming)的思想来试着解决上面的问题。
需要注意的是,对象是具体的个体,也就是上文中具体的车。车的蓝图叫做类(class)。2
相应地,把类变成具体的对象,这个过程叫做实例化。
- 创建类(画蓝图)
type Engine struct {isMotivated bool ...}
接着写启动的方法func (e Engine) Start() {}
(引擎)type Transmission struct {speedRatio float64}
(变速箱)type Doors [4]bool
(汽车门)type Car {Engine Engine; Transmission Trasmission;}
(汽车蓝图,结合了引擎、变速箱等)
- 协作和封装
- 我们定义了引擎的蓝图之后,只需要告诉引擎
Engine.Start()
就能够让引擎自己发动,而不用我们知道启动原理。因为启动的方式已经在引擎的结构体中写好了,采用哪一种启动方式(燃油喷射或者电机启动)是引擎自己的事情。我们发现,这种操作将具体的操作细节隐藏起来,而只需要执行对象预先设置好的接口,这种方式叫做封装(encapsulation)。 - 汽车对象“有”一个变速箱。当我们希望驾驶汽车时,汽车对象内部会自动协调变速箱等元件,让它能够适应实际驾驶的需要。比如,自动档汽车和手动挡汽车的变速箱运行逻辑不同,但这都能够交给汽车自己协调,我们驾驶汽车的指令不需要改变。不同的汽车运行逻辑不一致,但实际驾驶时都用到了
Car.Drive()
这样的指令。同样的指令却能适应不同的场景,这就是面向对象的多态(polymorphism)。
- 我们定义了引擎的蓝图之后,只需要告诉引擎
- 继承和扩展
- 我们上面写的是燃油车的蓝图,现在我们需要电动车的蓝图。因为它们都是车,就没有必要重写了。我们只要将燃油车的蓝图复制一份给电动车,然后再做修改就行。复制过程就是面向对象的继承(inheritance)。3
- 在复制好蓝图后(继承后),我们只要给电动车蓝图加上电池类和通电自检方法即可。
- 依照上面说的,电动车继承自车,这叫做重用(reuse)。
- 这一过程不需要我们触碰原来的蓝图,这是很安全的。
面向对象解决了上面的几个痛点。
封装隐藏了复杂性,而只用操作简单的接口。这与面向过程相比具有很大的优越性,我们只要会踩油门,会转方向盘就行,车里的零件怎么运转我们不用管。这极大地拉低了使用和理解的门槛。
继承实现了代码的高效率复用和扩展。已经写好的对象可以直接复用通用的代码,我们就能专注在写有差别的部分。复制模板提高了开发的效率。
多态提高了代码的灵活性和可维护性。即使燃油车和电动车的启动逻辑不同,它们也能根据自己的特点做出不同反应。日后增加新的车型时,我们几乎不用修改驾驶方法的代码。
封装、继承和多态是面向对象程序设计的三要素。
面向对象是很有利于团队协作的。团队中的每个人负责一个具体的类,这样能减少冲突,而只要在最后把所有的类搭起来就好。
在这一部分的最后,我们需要给面向过程和面向对象下一个公平的结论。
- 面向过程最适合完成小而简单的任务。因为它的脉络是很明确的。
- 面向对象适合完成大的任务。举个例子,在之后我们手写大型数据结构(比如红黑树)时,面向对象的模块化就很适合使用。
大部分的现代语言都是面向对象的,例如 Go、C++ 以及 Python(Python 中万物皆对象,任何可能的操作都是涉及对象的)。像 C、BASIC 和 Pascal,这些语言是为面向过程设计的。
其实在这之前我们已经见识过了面向对象编程的好处,那就是 #3 中的栈。
💡栈(Stack)是一种后进先出(LIFO,Last In First Out)的数据结构。
距离栈的入口最近的元素,我们叫做栈顶元素。相应的,距离栈的入口最远的元素叫做栈底元素。
我们把栈抽象成了一个类,要用时就将它实例化。
现在我们有了两把锋利的剑。一把叫做面向过程,它很轻,但是遇见大的怪物就束手无策;另外一把叫做面向对象,它比上一把剑重得多,但是在处理大怪物时效率奇高。同样,我们在实际的程序设计中,往往需要结合起它们的优点才能写出兼顾美观和性能的程序。
额外部分
一台完整的车
package main
import (
"fmt"
"math/rand"
"time"
)
// 启动器,用来发动汽车
type Starter interface {
Start() bool
}
// 可驾驶的接口
type Drivable interface {
Accelerate(speed float64) bool
Brake() bool
GetSpeed() float64
}
// 可加油的接口
type Fuelable interface {
Refuel(amount float64) bool
GetFuel() float64
}
// ====汽车基本组件====
type Engine struct {
power float64 // 全部都是私有属性(小写字母开头)
isRunning bool
fuelType string
}
func (e *Engine) Start() bool {
if e.isRunning {
fmt.Println("引擎已启动")
return false
}
fmt.Println("启动中")
time.Sleep(1 * time.Second)
// 打火失败
if rand.Float32() < 0.1 {
fmt.Println("引擎启动失败了")
}
e.isRunning = true
fmt.Println("引擎启动成功")
return true
}
func (e *Engine) Stop() {
if e.isRunning {
e.isRunning = false
fmt.Println("引擎已停止")
}
}
func (e *Engine) IsRunning() bool {
return e.isRunning
}
// 写一个油箱
type FuelTank struct {
capacity float64
fuelLevel float64
}
// 实现油箱接口
func (f *FuelTank) Refuel(amount float64) bool {
if amount <= 0 {
fmt.Println("无效油量")
return false
}
newLevel := f.fuelLevel + amount
if newLevel > f.capacity {
fmt.Printf("油箱溢出,当前容量为 %.1f L\n", f.capacity)
f.fuelLevel = f.capacity
} else {
f.fuelLevel = newLevel
}
fmt.Printf("加入了 %.1f L 的燃油。当前油量:%.1fL\n", amount, f.fuelLevel)
return true
}
func (f *FuelTank) Consume(amount float64) bool {
if amount <= 0 {
return false
}
if f.fuelLevel < amount {
fmt.Println("燃油不足!")
return false
}
f.fuelLevel -= amount
return true
}
func (f *FuelTank) GetFuel() float64 {
return f.fuelLevel
}
// 变速箱结构体
type Transmission struct {
gearCount int // 全部档位数
nowGear int // 当前的档位
}
func (t *Transmission) ShiftUp() bool {
if t.nowGear < t.gearCount {
t.nowGear++
fmt.Printf("档位提高至 %d 档\n", t.nowGear)
return true
}
fmt.Println("当前已是最高档位")
return false
}
func (t *Transmission) ShiftDown() bool {
if t.nowGear > 0 {
t.nowGear--
fmt.Printf("档位减至 %d 档\n", t.nowGear)
return true
}
fmt.Println("当前汽车未启动")
return false
}
// 下面是具体的组件
// 也就是体现面向对象的多态
type GasolineEngine struct {
Engine // 组合我们写好的引擎结构体
cylinder int // 汽缸数
}
// 重写 start 方法,因为多了一个引擎预热的操作
func (g *GasolineEngine) Start() bool {
fmt.Println("预热引擎中……")
time.Sleep(500 * time.Millisecond) // 预热五百毫秒(半秒)
return g.Engine.Start()
}
// 电动马达
type ElectricMotor struct {
Engine
batteryCapacity float64
}
// 重写 start
func (e *ElectricMotor) Start() bool {
fmt.Println("启动电动马达……")
time.Sleep(200 * time.Millisecond) // 电动马达比汽油机启动更快
e.isRunning = true
if rand.Float32() < 0.02 {
fmt.Println("电池电压不足,启动失败")
return false
}
e.isRunning = true
fmt.Println("电动马达启动成功")
return true
}
// 自动变速箱
type AutomaticTransmission struct {
Transmission
}
func (a *AutomaticTransmission) ShiftUp() bool {
fmt.Println("自动换档中")
time.Sleep(300 * time.Millisecond)
return a.Transmission.ShiftUp()
}
// 手动变速箱
type ManualTransmission struct {
Transmission
}
func (m *ManualTransmission) ShiftUp() bool {
fmt.Println("手动加速中……离合器正在发力")
time.Sleep(500 * time.Millisecond)
return m.Transmission.ShiftUp()
}
// 汽车主体
type Car struct {
model string
speed float64
engine Starter // 调用了接口作为类型,以实现多态
fuelTank Fuelable
transmission interface {
ShiftUp() bool
ShiftDown() bool
}
bodyStyle string
}
// 实现 Drivable 接口
func (c *Car) Accelerate(speed float64) bool {
if !c.engine.Start() {
fmt.Println("引擎未发动,无法加速")
return false
}
// 检查油/电量
if fuelable, ok := c.engine.(interface{ GetFuelLevel() float64 }); ok { // 类型断言
if fuelable.GetFuelLevel() <= 0 {
fmt.Println("无法加速,燃料已耗尽")
return false
}
}
c.speed += speed
fmt.Printf("加速至 %.1f km/h \n", c.speed)
// 自动换档(简化逻辑)
if c.transmission != nil && speed > 0 {
if c.speed > 30 {
c.transmission.ShiftUp()
}
}
return true
}
func (c *Car) Brake() bool {
if c.speed <= 0 {
fmt.Println("汽车已停下")
return false
}
c.speed -= 10
if c.speed < 0 {
c.speed = 0
}
fmt.Printf("减速至 %.1f km/h \n", c.speed)
// 根据速度自动降档
if c.transmission != nil && c.speed < 20 {
c.transmission.ShiftDown()
}
return true
}
func (c *Car) GetSpeed() float64 {
return c.speed
}
func (c *Car) Refuel(amount float64) bool {
return c.fuelTank.Refuel(amount)
}
func (c *Car) GetFuelLevel() float64 {
return c.fuelTank.GetFuel()
}
// === 构造函数 ===
// 创建汽油车
func NewGasolineCar(model, bodyStyle string) *Car {
engine := &GasolineEngine{
Engine: Engine{
power: 150,
fuelType: "gasoline",
},
cylinder: 4,
}
fuelTank := &FuelTank{
capacity: 50.0,
fuelLevel: 10.0,
}
transmission := &AutomaticTransmission{
Transmission: Transmission{
gearCount: 6,
},
}
return &Car{
model: model,
engine: engine,
fuelTank: fuelTank,
transmission: transmission,
bodyStyle: bodyStyle,
}
}
// 创建电动车
func NewElectricCar(model, bodyStyle string) *Car {
engine := &ElectricMotor{
Engine: Engine{
power: 200,
fuelType: "electricity",
},
batteryCapacity: 75.0,
}
// 电动车的"油箱"实际上是电池
fuelTank := &FuelTank{
capacity: 75.0,
fuelLevel: 50.0,
}
// 电动车通常使用单速变速箱
transmission := &AutomaticTransmission{
Transmission: Transmission{
gearCount: 1,
},
}
return &Car{
model: model,
engine: engine,
fuelTank: fuelTank,
transmission: transmission,
bodyStyle: bodyStyle,
}
}
// ========== 主函数 ==========
func main() {
rand.Seed(time.Now().UnixNano())
fmt.Println("=== 汽油车演示 ===")
gasCar := NewGasolineCar("Toyota Camry", "Sedan")
fmt.Printf("初始油量: %.1f L\n", gasCar.GetFuelLevel())
gasCar.Refuel(20.0)
gasCar.Accelerate(50.0)
gasCar.Accelerate(30.0)
gasCar.Brake()
gasCar.Brake()
fmt.Println()
fmt.Println("=== 电动车演示 ===")
electricCar := NewElectricCar("Tesla Model 3", "Sedan")
fmt.Printf("初始电量: %.1f %%\n", electricCar.GetFuelLevel()/0.75) // 转换为百分比
electricCar.Accelerate(60.0)
electricCar.Accelerate(40.0)
electricCar.Brake()
// 演示多态 - 统一处理不同类型的汽车
fmt.Println("\n=== 多态演示 ===")
cars := []Drivable{gasCar, electricCar}
for i, car := range cars {
fmt.Printf("汽车 %d 当前速度: %.1f km/h\n", i+1, car.GetSpeed())
}
}
输出
=== 汽油车演示 ===
初始油量: 10.0L
加入了 20.0 L 的燃油。当前油量:30.0L
预热引擎中……
启动中
引擎启动成功
加速至 50.0 km/h
自动换档中
档位提高至 1 档
预热引擎中……
引擎已启动
引擎未发动,无法加速
减速至 40.0 km/h
减速至 30.0 km/h
=== 电动车演示 ===
初始电量: 66.7%
启动电动马达……
电动马达启动成功
加速至 60.0 km/h
自动换档中
档位提高至 1 档
启动电动马达……
电动马达启动成功
加速至 100.0 km/h
自动换档中
当前已是最高档位
减速至 90.0 km/h
=== 多态演示 ===
汽车 1 当前速度: 30.0 km/h
汽车 2 当前速度: 90.0 km/h
这一段代码就很好地展示了面向对象程序设计的核心思想。
封装:在上面的结构体中,所有的结构体字段都是私有(private)的(通过字段首字母小写实现)。
访问接口则通过公共方法提供。例如 Engine
的 isRunning
字段只能通过 IsRunning()
方法访问。
多态:通过接口实现多态行为。Starter
、Drivable
和 Fuelable
接口定义了通用行为,而具体的方法(GasolineEngine
和 ElectricMotor
)提供特定实现。
也就是说,汽车可以统一处理不同类型的引擎和变速箱。
组合/继承:Go 没有传统继承,我们在这里使用结构体嵌入(embedding)实现组合。可以重写父结构体的方法(如 Start()
方法)来实现代码复用和扩展。
顺便说一句,如果使用 VS Code 编辑代码的话,把光标悬浮在结构体上就能看到我们写好的注释。

如果需要显示变速箱结构体那一行说明,只要在 type Transmission struct {}
上面紧贴着写注释就可以了。
