Go 语言设计模式入门(一):创建型模式
我会用一个统一的场景贯穿始终:
🎮 RPG 游戏英雄创建系统:我们需要在游戏中创建各种英雄角色(战士、法师、弓箭手),每个英雄有名字、职业、武器、技能等属性。
一、为什么要学习设计模式?
1. 设计模式是什么?
设计模式是前人在软件开发中反复遇到的问题的通用解决方案 。它不是具体的代码,而是一种思想、模板和最佳实践。
打个比方:
-
不学设计模式,就像盖房子每次都要重新设计地基、承重墙、水电走向------累且容易出错。
-
学了设计模式,就像有了标准建筑图纸------地基怎么打、梁柱怎么架,都有成熟方案,你只需根据需求微调。
2. 为什么要学?(对 Go 程序员尤其重要)
| 好处 | 通俗解释 |
|---|---|
| 代码复用 | 不用重复造轮子,一次写好,到处复用 |
| 易于维护 | 代码结构清晰,改需求时不用"牵一发而动全身" |
| 团队协作 | 你说"这里用了单例模式",队友立刻懂你的意图 |
| 应对变化 | 需求变了(比如要加新英雄),只需扩展,不用改原有代码 |
| Go 语言特色 | Go 没有类继承,设计模式能帮你用组合+接口写出优雅的代码 |
3. Go 语言实现设计模式的特殊之处
Go 与传统 OOP 语言(Java/C++)不同:
-
❌ 没有
class继承 -
✅ 有
struct(结构体)和interface(接口) -
✅ 推崇组合优于继承 、显式优于隐式
所以 Go 的设计模式实现更简洁、直接,没有繁琐的继承链。
二、创建型模式(Creational Patterns)
核心问题:如何优雅地创建对象?
在 Go 中,"对象"就是结构体实例。创建型模式解决的是:什么时候创建、由谁创建、怎么创建的问题。
1. 单例模式(Singleton Pattern)
是什么?
确保一个类(在 Go 中是结构体)在整个程序运行期间只有一个实例,并提供一个全局访问点。
为什么需要?
游戏中很多资源应该是全局唯一的:
-
游戏配置管理器(读取一次配置文件,全局共享)
-
数据库连接池
-
全局 ID 生成器
如果创建多个实例,会造成:
-
配置不一致
-
资源浪费(内存、连接数)
-
数据冲突
适用场景
-
全局配置管理
-
数据库连接池
-
日志记录器(Logger)
-
线程池
代码案例:游戏配置管理器
package main
import (
"fmt"
"sync"
)
// GameConfig 游戏配置结构体(单例)
type GameConfig struct {
GameName string
Version string
MaxLevel int
}
// 私有变量,保存唯一实例
var (
instance *GameConfig
once sync.Once
)
// GetConfig 获取全局唯一的配置实例(线程安全)
func GetConfig() *GameConfig {
// sync.Once 确保这里的代码只执行一次,即使在多线程环境下
once.Do(func() {
instance = &GameConfig{
GameName: "Go语言冒险",
Version: "1.0.0",
MaxLevel: 100,
}
fmt.Println(">>> 配置初始化完成(只执行一次)")
})
return instance
}
func main() {
// 模拟多处代码获取配置
config1 := GetConfig()
config2 := GetConfig()
config3 := GetConfig()
fmt.Printf("配置1: %+v\n", config1)
fmt.Printf("配置2: %+v\n", config2)
fmt.Printf("配置3: %+v\n", config3)
// 验证是否是同一个实例
fmt.Printf("是否是同一个实例? %v\n", config1 == config2)
// 修改配置(全局生效)
config1.Version = "1.1.0"
fmt.Printf("config2 的版本也变成了: %s\n", config2.Version)
}
运行结果
>>> 配置初始化完成(只执行一次)
配置1: &{GameName:Go语言冒险 Version:1.0.0 MaxLevel:100}
配置2: &{GameName:Go语言冒险 Version:1.0.0 MaxLevel:100}
配置3: &{GameName:Go语言冒险 Version:1.0.0 MaxLevel:100}
是否是同一个实例? true
config2 的版本也变成了: 1.1.0
关键点
-
sync.Once是 Go 的"核武器",保证无论多少 goroutine 同时调用,初始化代码只执行一次。 -
单例不是让你到处用全局变量,而是有节制地使用。
2. 简单工厂模式(Simple Factory)
是什么?
一个工厂类(函数)根据传入的参数,决定创建哪种具体产品。
为什么需要?
直接 new 对象的问题:
-
创建逻辑散落在各处
-
如果新增英雄类型,所有
new的地方都要改 -
调用者需要知道太多内部细节
工厂模式把创建逻辑集中管理,调用者只需说"我要一个战士",不用关心战士怎么造出来的。
适用场景
-
创建的对象种类不多
-
创建逻辑相对简单
-
不想暴露具体结构体的构造细节
代码案例:简单英雄工厂
package main
import (
"fmt"
)
// ===== 抽象层:英雄接口 =====
type Hero interface {
GetName() string
Attack() string
}
// ===== 具体实现:战士 =====
type Warrior struct {
name string
}
func (w *Warrior) GetName() string {
return w.name
}
func (w *Warrior) Attack() string {
return fmt.Sprintf("%s 使用【旋风斩】,造成物理伤害!", w.name)
}
// ===== 具体实现:法师 =====
type Mage struct {
name string
}
func (m *Mage) GetName() string {
return m.name
}
func (m *Mage) Attack() string {
return fmt.Sprintf("%s 使用【火球术】,造成魔法伤害!", m.name)
}
// ===== 具体实现:弓箭手 =====
type Archer struct {
name string
}
func (a *Archer) GetName() string {
return a.name
}
func (a *Archer) Attack() string {
return fmt.Sprintf("%s 使用【多重射击】,造成远程伤害!", a.name)
}
// ===== 简单工厂:一个函数创建所有英雄 =====
func CreateHero(heroType, name string) Hero {
switch heroType {
case "warrior":
return &Warrior{name: name}
case "mage":
return &Mage{name: name}
case "archer":
return &Archer{name: name}
default:
return nil
}
}
func main() {
// 玩家只需告诉工厂"我要什么",不用关心内部怎么创建
hero1 := CreateHero("warrior", "亚瑟")
hero2 := CreateHero("mage", "甘道夫")
hero3 := CreateHero("archer", " Legolas")
heroes := []Hero{hero1, hero2, hero3}
for _, h := range heroes {
fmt.Printf("英雄: %s | 攻击: %s\n", h.GetName(), h.Attack())
}
}
运行结果
英雄: 亚瑟 | 攻击: 亚瑟 使用【旋风斩】,造成物理伤害!
英雄: 甘道夫 | 攻击: 甘道夫 使用【火球术】,造成魔法伤害!
英雄: Legolas | 攻击: Legolas 使用【多重射击】,造成远程伤害!
优缺点
-
✅ 优点:集中管理创建逻辑,调用者简单
-
❌ 缺点:工厂函数里
switch会随新类型增加而膨胀,违反开闭原则(对扩展开放,对修改关闭)
3. 工厂方法模式(Factory Method)
是什么?
定义一个创建对象的接口(工厂接口),但由子类(具体工厂)决定实例化哪个类。
为什么需要?
简单工厂的痛点:新增英雄类型时,要修改工厂函数的 switch。
工厂方法模式把"创建权"下放给具体的工厂结构体 ,新增类型时只需新增工厂,不用改原有代码。
适用场景
-
产品类型较多,且经常扩展
-
需要把创建逻辑拆分到不同类中
-
一个产品有多种变体,创建方式不同
代码案例:每个英雄有自己的工厂
package main
import (
"fmt"
)
// ===== 抽象层 =====
type Hero interface {
GetName() string
Attack() string
}
// HeroFactory 工厂接口:每个工厂都能"生产英雄"
type HeroFactory interface {
CreateHero(name string) Hero
}
// ===== 具体英雄:战士 =====
type Warrior struct {
name string
}
func (w *Warrior) GetName() string { return w.name }
func (w *Warrior) Attack() string {
return fmt.Sprintf("%s 使用【旋风斩】!", w.name)
}
// ===== 具体英雄:法师 =====
type Mage struct {
name string
}
func (m *Mage) GetName() string { return m.name }
func (m *Mage) Attack() string {
return fmt.Sprintf("%s 使用【火球术】!", m.name)
}
// ===== 具体英雄:弓箭手 =====
type Archer struct {
name string
}
func (a *Archer) GetName() string { return a.name }
func (a *Archer) Attack() string {
return fmt.Sprintf("%s 使用【多重射击】!", a.name)
}
// ===== 具体工厂:战士工厂 =====
type WarriorFactory struct{}
func (f *WarriorFactory) CreateHero(name string) Hero {
fmt.Println(">>> 战士工厂正在训练新兵...")
return &Warrior{name: name}
}
// ===== 具体工厂:法师工厂 =====
type MageFactory struct{}
func (f *MageFactory) CreateHero(name string) Hero {
fmt.Println(">>> 法师工厂正在传授魔法...")
return &Mage{name: name}
}
// ===== 具体工厂:弓箭手工厂 =====
type ArcherFactory struct{}
// 这里结构体赋值给接口类型,相当于装箱。
// 结构体是"具体的",接口是"抽象的"。具体可以向上转为抽象。
// 接口是抽象的,无法直接向下转为具体。必须通过运行时断言确认动态类型。
// 具体类型可以自动"升级"为接口,但接口必须"显式确认"后才能降级为具体类型。
func (f *ArcherFactory) CreateHero(name string) Hero {
fmt.Println(">>> 弓箭手工厂正在校准弓弦...")
return &Archer{name: name}
}
func main() {
// 定义工厂列表
factories := map[string]HeroFactory{
"warrior": &WarriorFactory{},
"mage": &MageFactory{},
"archer": &ArcherFactory{},
}
// 玩家选择职业
choices := []struct {
heroType string
name string
}{
{"warrior", "亚瑟"},
{"mage", "甘道夫"},
{"archer", "Legolas"},
}
for _, choice := range choices {
factory := factories[choice.heroType]
hero := factory.CreateHero(choice.name)
fmt.Printf("创建成功: %s | %s\n\n", hero.GetName(), hero.Attack())
}
}
运行结果
>>> 战士工厂正在训练新兵...
创建成功: 亚瑟 | 亚瑟 使用【旋风斩】!
>>> 法师工厂正在传授魔法...
创建成功: 甘道夫 | 甘道夫 使用【火球术】!
>>> 弓箭手工厂正在校准弓弦...
创建成功: Legolas | Legolas 使用【多重射击】!
与简单工厂的对比
| 维度 | 简单工厂 | 工厂方法 |
|---|---|---|
| 创建者 | 一个工厂函数 | 多个工厂结构体 |
| 扩展方式 | 修改 switch |
新增工厂结构体 |
| 符合开闭原则 | ❌ 不符合 | ✅ 符合 |
| 复杂度 | 简单 | 稍复杂 |
4. 抽象工厂模式(Abstract Factory)
是什么?
创建相关或依赖对象的家族(产品族),而不需要明确指定具体类。
为什么需要?
工厂方法只能创建一种产品 (英雄)。但游戏中,东方风格的英雄应该配东方风格的武器,西方风格的英雄应该配西方风格的武器。这两个是配套的产品族。
抽象工厂一次性创建一整套相关产品。
适用场景
-
需要创建多个相关/配套的对象(产品族)
-
系统应该独立于产品的创建、组合和表示
-
例如:UI 组件库(Windows 按钮 + Windows 文本框 vs Mac 按钮 + Mac 文本框)
代码案例:东方 vs 西方英雄产品族
package main
import (
"fmt"
)
// ===== 产品族接口:英雄 =====
type Hero interface {
GetName() string
Attack() string
}
// ===== 产品族接口:武器 =====
type Weapon interface {
GetWeaponName() string
DealDamage() string
}
// ===== 抽象工厂:能创建英雄和武器 =====
type GameFactory interface {
CreateHero(name string) Hero
CreateWeapon() Weapon
}
// ==================== 东方产品族 ====================
// 东方英雄
type EasternHero struct {
name string
}
func (h *EasternHero) GetName() string { return h.name }
func (h *EasternHero) Attack() string {
return fmt.Sprintf("%s 使用【太极拳】,以柔克刚!", h.name)
}
// 东方武器
type EasternWeapon struct{}
func (w *EasternWeapon) GetWeaponName() string { return "青龙偃月刀" }
func (w *EasternWeapon) DealDamage() string {
return "青龙偃月刀划出一道青色刀芒!"
}
// 东方工厂
type EasternFactory struct{}
func (f *EasternFactory) CreateHero(name string) Hero {
return &EasternHero{name: name}
}
func (f *EasternFactory) CreateWeapon() Weapon {
return &EasternWeapon{}
}
// ==================== 西方产品族 ====================
// 西方英雄
type WesternHero struct {
name string
}
func (h *WesternHero) GetName() string { return h.name }
func (h *WesternHero) Attack() string {
return fmt.Sprintf("%s 使用【骑士冲锋】,勇猛无畏!", h.name)
}
// 西方武器
type WesternWeapon struct{}
func (w *WesternWeapon) GetWeaponName() string { return "Excalibur圣剑" }
func (w *WesternWeapon) DealDamage() string {
return "圣剑绽放金色光芒,斩破黑暗!"
}
// 西方工厂
type WesternFactory struct{}
func (f *WesternFactory) CreateHero(name string) Hero {
return &WesternHero{name: name}
}
func (f *WesternFactory) CreateWeapon() Weapon {
return &WesternWeapon{}
}
// ==================== 客户端代码 ====================
func PlayGame(factory GameFactory, heroName string) {
hero := factory.CreateHero(heroName)
weapon := factory.CreateWeapon()
fmt.Printf("=== 英雄登场: %s ===\n", hero.GetName())
fmt.Printf("装备武器: %s\n", weapon.GetWeaponName())
fmt.Printf("英雄攻击: %s\n", hero.Attack())
fmt.Printf("武器特效: %s\n", weapon.DealDamage())
fmt.Println()
}
func main() {
// 东方风格游戏
easternFactory := &EasternFactory{}
PlayGame(easternFactory, "关羽")
// 西方风格游戏
westernFactory := &WesternFactory{}
PlayGame(westernFactory, "亚瑟王")
}
运行结果
=== 英雄登场: 关羽 ===
装备武器: 青龙偃月刀
英雄攻击: 关羽 使用【太极拳】,以柔克刚!
武器特效: 青龙偃月刀划出一道青色刀芒!
=== 英雄登场: 亚瑟王 ===
装备武器: Excalibur圣剑
英雄攻击: 亚瑟王 使用【骑士冲锋】,勇猛无畏!
武器特效: 圣剑绽放金色光芒,斩破黑暗!
三种工厂模式对比
| 模式 | 解决的问题 | 比喻 |
|---|---|---|
| 简单工厂 | 把创建逻辑集中到一个函数 | 一家小作坊,老板什么都会做 |
| 工厂方法 | 把创建权下放给子工厂 | 连锁加盟店,每个店做一种特色菜 |
| 抽象工厂 | 创建一整套配套产品 | 主题餐厅,中式餐厅配中式餐具,西式餐厅配西式餐具 |
5. 建造者模式(Builder Pattern)
是什么?
一步步构建复杂对象 。当对象的构造过程很复杂(有很多可选参数、有严格的构建顺序),用建造者模式把构建过程拆分成多个步骤。
为什么需要?
创建一个英雄可能需要:
-
必填:名字
-
选填:等级、武器、技能、皮肤、坐骑...
-
约束:等级 1-100,必须先有武器才能学技能...
如果用一个巨大的构造函数 NewHero(name, level, weapon, skill1, skill2, skill3, skin, mount...),参数太多,容易传错。
建造者模式让你像搭积木一样,一步步构建,且可以灵活组合。
适用场景
-
对象有很多可选参数
-
构建过程需要分步骤、有顺序
-
需要创建不同配置的同类型对象
-
例如:SQL 构建器、HTTP 请求构建器、复杂配置对象
代码案例:一步步构建复杂英雄
package main
import (
"fmt"
"strings"
)
// ===== 英雄结构体(复杂对象)=====
type Hero struct {
Name string
Level int
Weapon string
Skills []string
Skin string
Mount string
}
func (h *Hero) ShowInfo() {
fmt.Printf("╔════════════════════════════╗\n")
fmt.Printf(" 英雄: %s\n", h.Name)
fmt.Printf(" 等级: %d\n", h.Level)
fmt.Printf(" 武器: %s\n", h.Weapon)
fmt.Printf(" 技能: %s\n", strings.Join(h.Skills, ", "))
fmt.Printf(" 皮肤: %s\n", h.Skin)
fmt.Printf(" 坐骑: %s\n", h.Mount)
fmt.Printf("╚════════════════════════════╝\n\n")
}
// ===== 建造者接口 =====
type HeroBuilder interface {
SetName(name string) HeroBuilder
SetLevel(level int) HeroBuilder
SetWeapon(weapon string) HeroBuilder
AddSkill(skill string) HeroBuilder
SetSkin(skin string) HeroBuilder
SetMount(mount string) HeroBuilder
Build() *Hero
}
// ===== 具体建造者 =====
type ConcreteHeroBuilder struct {
hero *Hero
}
func NewHeroBuilder() HeroBuilder {
return &ConcreteHeroBuilder{
hero: &Hero{
Level: 1, // 默认值
Skills: []string{}, // 初始化空切片
Skin: "默认皮肤",
},
}
}
func (b *ConcreteHeroBuilder) SetName(name string) HeroBuilder {
b.hero.Name = name
return b
}
func (b *ConcreteHeroBuilder) SetLevel(level int) HeroBuilder {
if level < 1 {
level = 1
}
if level > 100 {
level = 100
}
b.hero.Level = level
return b
}
func (b *ConcreteHeroBuilder) SetWeapon(weapon string) HeroBuilder {
b.hero.Weapon = weapon
return b
}
func (b *ConcreteHeroBuilder) AddSkill(skill string) HeroBuilder {
b.hero.Skills = append(b.hero.Skills, skill)
return b
}
func (b *ConcreteHeroBuilder) SetSkin(skin string) HeroBuilder {
b.hero.Skin = skin
return b
}
func (b *ConcreteHeroBuilder) SetMount(mount string) HeroBuilder {
b.hero.Mount = mount
return b
}
func (b *ConcreteHeroBuilder) Build() *Hero {
// 构建前的校验
if b.hero.Name == "" {
panic("英雄必须有名字!")
}
if b.hero.Weapon == "" {
b.hero.Weapon = "木剑" // 默认武器
}
return b.hero
}
// ===== 导演类(可选):封装常用构建流程 =====
type Director struct {
builder HeroBuilder
}
func NewDirector(builder HeroBuilder) *Director {
return &Director{builder: builder}
}
// 构建满级顶配英雄
func (d *Director) BuildMaxHero(name string) *Hero {
return d.builder.
SetName(name).
SetLevel(100).
SetWeapon("传说之剑").
AddSkill("终极奥义").
AddSkill("无敌护盾").
AddSkill("瞬移").
SetSkin("传说皮肤").
SetMount("神龙").
Build()
}
// 构建新手英雄
func (d *Director) BuildNewbieHero(name string) *Hero {
return d.builder.
SetName(name).
SetLevel(1).
SetWeapon("木剑").
AddSkill("普通攻击").
Build()
}
func main() {
// 方式1:自由建造(像流式 API)
fmt.Println("=== 自由建造:自定义英雄 ===")
// 这段代码使用的是 建造者模式(Builder Pattern) 中的 链式调用(Method Chaining / Fluent Interface),在 Go 中完全可以这样写。
customHero := NewHeroBuilder().
SetName("自定义小明").
SetLevel(50).
SetWeapon("冰霜长矛").
AddSkill("冰冻术").
AddSkill("火球术").
SetSkin("冰雪节限定").
Build()
customHero.ShowInfo()
// 方式2:使用导演类,一键构建标准配置
fmt.Println("=== 导演模式:一键生成顶配英雄 ===")
director := NewDirector(NewHeroBuilder())
maxHero := director.BuildMaxHero("龙傲天")
maxHero.ShowInfo()
fmt.Println("=== 导演模式:一键生成新手英雄 ===")
newbieHero := director.BuildNewbieHero("萌新小白")
newbieHero.ShowInfo()
}
运行结果
=== 自由建造:自定义英雄 ===
╔════════════════════════════╗
英雄: 自定义小明
等级: 50
武器: 冰霜长矛
技能: 冰冻术, 火球术
皮肤: 冰雪节限定
坐骑:
╚════════════════════════════╝
=== 导演模式:一键生成顶配英雄 ===
╔════════════════════════════╗
英雄: 龙傲天
等级: 100
武器: 传说之剑
技能: 终极奥义, 无敌护盾, 瞬移
皮肤: 传说皮肤
坐骑: 神龙
╚════════════════════════════╝
=== 导演模式:一键生成新手英雄 ===
╔════════════════════════════╗
英雄: 萌新小白
等级: 1
武器: 木剑
技能: 普通攻击
皮肤: 默认皮肤
坐骑:
╚════════════════════════════╝
关键点
-
链式调用 :每个设置方法返回
HeroBuilder接口,可以连续.SetXxx().SetYyy() -
Director(导演):封装常用构建流程,让客户端更简单
-
Build() 校验:最后统一检查必填项,确保对象合法
6. 原型模式(Prototype Pattern)
是什么?
通过复制现有对象来创建新对象,而不是从头新建。
为什么需要?
-
创建成本高:英雄数据从数据库读取、计算属性、加载资源,耗时很长。
-
大部分相同 :新英雄和已有英雄只有微小差别(比如换个名字、改个等级)。
-
保存状态 :需要复制一个对象当前的状态,作为备份或起点。
与其重新创建,不如克隆一个已有的,再微调。
适用场景
-
对象创建成本高(需要数据库查询、复杂计算)
-
需要保留对象当前状态作为副本
-
对象结构复杂,但大部分字段相同
-
例如:游戏中的存档复制、文档模板复制
代码案例:克隆英雄
package main
import (
"fmt"
)
// ===== 原型接口 =====
type Prototype interface {
Clone() Prototype
}
// ===== 英雄结构体(实现原型接口)=====
type Hero struct {
Name string
Level int
Weapon string
Skills []string
}
// Clone 实现深拷贝(关键!)
func (h *Hero) Clone() Prototype {
// 深拷贝:新建 Skills 切片,复制每个元素
newSkills := make([]string, len(h.Skills))
copy(newSkills, h.Skills)
return &Hero{
Name: h.Name,
Level: h.Level,
Weapon: h.Weapon,
Skills: newSkills,
}
}
func (h *Hero) ShowInfo() {
fmt.Printf("英雄: %s | 等级: %d | 武器: %s | 技能: %v\n",
h.Name, h.Level, h.Weapon, h.Skills)
}
func main() {
// 创建一个"模板英雄"(可能是从数据库加载的满级英雄)
template := &Hero{
Name: "模板战士",
Level: 80,
Weapon: "屠龙宝刀",
Skills: []string{"旋风斩", "狂暴", "盾墙"},
}
fmt.Println("=== 原始模板 ===")
template.ShowInfo()
// 克隆英雄1:基于模板,微调
hero1 := template.Clone().(*Hero)
hero1.Name = "克隆战士A"
hero1.Level = 85
hero1.Skills = append(hero1.Skills, "新技能:雷霆一击")
fmt.Println("\n=== 克隆英雄A(修改后)===")
hero1.ShowInfo()
// 克隆英雄2:另一个微调版本
hero2 := template.Clone().(*Hero)
hero2.Name = "克隆战士B"
hero2.Weapon = "冰霜之刃"
fmt.Println("\n=== 克隆英雄B(修改后)===")
hero2.ShowInfo()
// 验证:模板没有被修改(深拷贝生效)
fmt.Println("\n=== 验证:模板是否被污染?===")
template.ShowInfo()
// 验证 Skills 切片是否独立
fmt.Printf("\n模板 Skills 地址: %p\n", template.Skills)
fmt.Printf("英雄A Skills 地址: %p\n", hero1.Skills)
fmt.Printf("英雄B Skills 地址: %p\n", hero2.Skills)
}
运行结果
=== 原始模板 ===
英雄: 模板战士 | 等级: 80 | 武器: 屠龙宝刀 | 技能: [旋风斩 狂暴 盾墙]
=== 克隆英雄A(修改后)===
英雄: 克隆战士A | 等级: 85 | 武器: 屠龙宝刀 | 技能: [旋风斩 狂暴 盾墙 新技能:雷霆一击]
=== 克隆英雄B(修改后)===
英雄: 克隆战士B | 等级: 80 | 武器: 冰霜之刃 | 技能: [旋风斩 狂暴 盾墙]
=== 验证:模板是否被污染?===
英雄: 模板战士 | 等级: 80 | 武器: 屠龙宝刀 | 技能: [旋风斩 狂暴 盾墙]
模板 Skills 地址: 0xc0000b6000
英雄A Skills 地址: 0xc0000b6030
英雄B Skills 地址: 0xc0000b6060
深拷贝 vs 浅拷贝(重点!)
| 类型 | 说明 | 后果 |
|---|---|---|
| 浅拷贝 | 只复制指针,新对象和原对象共享内部数据 | 修改一个,另一个也变(危险!) |
| 深拷贝 | 递归复制所有引用类型的数据 | 完全独立,互不干扰 |
在 Go 中:
-
struct值类型自动深拷贝 -
slice、map、指针、channel是引用类型,需要手动复制
上面的代码中 Skills 是 []string(切片),必须用 make + copy 做深拷贝。
创建型模式总结
| 模式 | 核心思想 | 一句话记忆 | 我们的游戏案例 |
|---|---|---|---|
| 单例 | 全局唯一 | 独此一家 | 游戏配置管理器 |
| 简单工厂 | 一个函数创建多种对象 | 小作坊,老板全能 | 一个函数创建战士/法师/弓箭手 |
| 工厂方法 | 每个产品有自己的工厂 | 专卖店,各卖各的 | 战士工厂、法师工厂、弓箭手工厂 |
| 抽象工厂 | 创建配套产品族 | 主题餐厅,全套服务 | 东方英雄+东方武器 / 西方英雄+西方武器 |
| 建造者 | 一步步构建复杂对象 | 定制组装,自由搭配 | 自由设置名字、等级、武器、技能... |
| 原型 | 复制现有对象 | 克隆复制,稍作修改 | 复制满级模板英雄,改个名字直接用 |
Go 语言设计模式入门(二):结构型模式
我们继续用 RPG 游戏英雄系统 这个统一场景,深入讲解 结构型模式。
结构型模式的核心问题是:如何把简单的结构体组合成更复杂的结构? 就像搭积木,单个积木很简单,但组合起来能搭出城堡。
1. 代理模式(Proxy Pattern)
是什么?
为其他对象提供一种代理/替身,以控制对这个对象的访问。
为什么需要?
想象你的游戏英雄数据存储在远程数据库里:
-
每次查询都要网络请求,很慢
-
有些英雄信息只有 VIP 玩家才能查看,需要权限校验
-
频繁查询同一英雄,浪费带宽
直接访问"真实对象"太笨重,我们找一个"代理"挡在前面,代理可以做缓存、权限检查、日志记录等。
适用场景
-
远程代理:访问远程服务(RPC、微服务)
-
缓存代理:缓存查询结果,减少数据库压力
-
保护代理:权限控制
-
虚拟代理:延迟加载(先显示占位图,真正加载完成后再替换)
代码案例:英雄信息查询代理(带缓存)
package main
import (
"fmt"
"time"
)
// ===== 抽象主题:英雄服务接口 =====
type HeroService interface {
GetHeroInfo(heroID string) string
}
// ===== 真实主题:实际的数据库查询服务 =====
type RealHeroService struct{}
func (r *RealHeroService) GetHeroInfo(heroID string) string {
fmt.Printf(" 📡 [真实服务] 正在从数据库查询英雄 %s(模拟网络延迟)...\n", heroID)
time.Sleep(1 * time.Second) // 模拟耗时操作
return fmt.Sprintf("英雄 %s 的详细信息:等级100,传说装备,战力99999", heroID)
}
// ===== 代理:缓存代理 =====
type HeroCacheProxy struct {
realService *RealHeroService
cache map[string]string
}
func NewHeroCacheProxy() *HeroCacheProxy {
return &HeroCacheProxy{
realService: &RealHeroService{},
cache: make(map[string]string),
}
}
func (p *HeroCacheProxy) GetHeroInfo(heroID string) string {
// 1. 先查缓存
if info, ok := p.cache[heroID]; ok {
fmt.Printf(" ⚡ [代理缓存] 直接返回英雄 %s 的信息\n", heroID)
return info
}
// 2. 缓存没有,调用真实服务
fmt.Printf(" 🔍 [代理缓存] 缓存未命中,代理去请求真实服务...\n")
info := p.realService.GetHeroInfo(heroID)
// 3. 存入缓存
p.cache[heroID] = info
fmt.Printf(" 💾 [代理缓存] 已缓存英雄 %s 的信息\n", heroID)
return info
}
func main() {
proxy := NewHeroCacheProxy()
// 第一次查询:缓存未命中,走真实服务
fmt.Println("=== 第1次查询 亚瑟 ===")
info1 := proxy.GetHeroInfo("亚瑟")
fmt.Println("结果:", info1)
// 第二次查询:缓存命中,直接返回
fmt.Println("\n=== 第2次查询 亚瑟 ===")
info2 := proxy.GetHeroInfo("亚瑟")
fmt.Println("结果:", info2)
// 第三次查询新英雄:缓存未命中
fmt.Println("\n=== 第1次查询 甘道夫 ===")
info3 := proxy.GetHeroInfo("甘道夫")
fmt.Println("结果:", info3)
}
运行结果
=== 第1次查询 亚瑟 ===
🔍 [代理缓存] 缓存未命中,代理去请求真实服务...
📡 [真实服务] 正在从数据库查询英雄 亚瑟(模拟网络延迟)...
💾 [代理缓存] 已缓存英雄 亚瑟 的信息
结果: 英雄 亚瑟 的详细信息:等级100,传说装备,战力99999
=== 第2次查询 亚瑟 ===
⚡ [代理缓存] 直接返回英雄 亚瑟 的信息
结果: 英雄 亚瑟 的详细信息:等级100,传说装备,战力99999
=== 第1次查询 甘道夫 ===
🔍 [代理缓存] 缓存未命中,代理去请求真实服务...
📡 [真实服务] 正在从数据库查询英雄 甘道夫(模拟网络延迟)...
💾 [代理缓存] 已缓存英雄 甘道夫 的信息
结果: 英雄 甘道夫 的详细信息:等级100,传说装备,战力99999
关键点
-
客户端完全不知道自己在和代理打交道 ,因为代理和真实对象实现了相同的接口
-
代理在真实对象前后增加了额外逻辑(缓存),但不改变真实对象的行为
2. 桥接模式(Bridge Pattern)
是什么?
将抽象部分 与实现部分 分离,使它们可以独立变化。
为什么需要?
假设你的游戏英雄有"展示方式"(简略展示、详细展示),同时展示需要"渲染引擎"(控制台渲染、Web 渲染)。
如果用继承,你会得到:
-
简略控制台展示
-
详细控制台展示
-
简略 Web 展示
-
详细 Web 展示
-
... 组合爆炸!
桥接模式说:把"展示方式"和"渲染引擎"拆成两个独立维度,用组合连接它们。
适用场景
-
一个类存在两个独立变化的维度
-
不想用继承导致类爆炸
-
需要在运行时切换实现
-
例如:不同形状的图形 + 不同颜色的画笔
代码案例:英雄展示系统(展示方式 × 渲染引擎)
package main
import (
"fmt"
)
// ===== 实现部分:渲染引擎接口 =====
type RenderEngine interface {
RenderText(text string)
RenderImage(path string)
}
// 控制台渲染引擎
type ConsoleRender struct{}
func (c *ConsoleRender) RenderText(text string) {
fmt.Printf("[控制台] %s\n", text)
}
func (c *ConsoleRender) RenderImage(path string) {
fmt.Printf("[控制台] 显示图片: %s (ASCII艺术)\n", path)
}
// Web 渲染引擎
type WebRender struct{}
func (w *WebRender) RenderText(text string) {
fmt.Printf("<p style='color:blue'>%s</p>\n", text)
}
func (w *WebRender) RenderImage(path string) {
fmt.Printf("<img src='%s' />\n", path)
}
// ===== 抽象部分:英雄展示 =====
type HeroDisplay interface {
Show()
}
// 简略展示
type BriefDisplay struct {
heroName string
engine RenderEngine // 桥接点:持有渲染引擎
}
func NewBriefDisplay(name string, engine RenderEngine) HeroDisplay {
return &BriefDisplay{heroName: name, engine: engine}
}
func (b *BriefDisplay) Show() {
b.engine.RenderText("=== 简略信息 ===")
b.engine.RenderText("英雄: " + b.heroName)
b.engine.RenderText("等级: 50")
}
// 详细展示
type DetailDisplay struct {
heroName string
engine RenderEngine
}
func NewDetailDisplay(name string, engine RenderEngine) HeroDisplay {
return &DetailDisplay{heroName: name, engine: engine}
}
func (d *DetailDisplay) Show() {
d.engine.RenderText("=== 详细信息 ===")
d.engine.RenderText("英雄: " + d.heroName)
d.engine.RenderText("等级: 50")
d.engine.RenderText("武器: 屠龙刀")
d.engine.RenderText("技能: 旋风斩, 狂暴")
d.engine.RenderImage("hero_portrait.png")
}
func main() {
// 组合1:简略展示 + 控制台渲染
fmt.Println("=== 组合1:简略展示 + 控制台 ===")
display1 := NewBriefDisplay("亚瑟", &ConsoleRender{})
display1.Show()
// 组合2:详细展示 + Web 渲染
fmt.Println("\n=== 组合2:详细展示 + Web ===")
display2 := NewDetailDisplay("甘道夫", &WebRender{})
display2.Show()
// 组合3:详细展示 + 控制台渲染
fmt.Println("\n=== 组合3:详细展示 + 控制台 ===")
display3 := NewDetailDisplay("Legolas", &ConsoleRender{})
display3.Show()
}
运行结果
=== 组合1:简略展示 + 控制台 ===
[控制台] === 简略信息 ===
[控制台] 英雄: 亚瑟
[控制台] 等级: 50
=== 组合2:详细展示 + Web ===
<p style='color:blue'>=== 详细信息 ===</p>
<p style='color:blue'>英雄: 甘道夫</p>
<p style='color:blue'>等级: 50</p>
<p style='color:blue'>武器: 屠龙刀</p>
<p style='color:blue'>技能: 旋风斩, 狂暴</p>
<img src='hero_portrait.png' />
=== 组合3:详细展示 + 控制台 ===
[控制台] === 详细信息 ===
[控制台] 英雄: Legolas
[控制台] 等级: 50
[控制台] 武器: 屠龙刀
[控制台] 技能: 旋风斩, 狂暴
[控制台] 显示图片: hero_portrait.png (ASCII艺术)
关键点
-
桥接模式的核心是**"用组合代替继承"**
-
HeroDisplay(抽象)通过RenderEngine(实现)接口桥接,两者独立扩展 -
新增一种展示方式或渲染引擎,不需要修改对方
3. 组合模式(Composite Pattern)
是什么?
将对象组合成树形结构 ,以表示"部分-整体"的层次结构。组合模式让客户端可以统一处理单个对象和组合对象。
为什么需要?
你的游戏中有:
-
单个英雄(叶子节点)
-
英雄小队(组合节点,包含多个英雄)
-
英雄军团(组合节点,包含多个小队)
如果不用组合模式,你需要写两套逻辑:一套处理单个英雄,一套处理队伍。组合模式让你用同一套接口处理所有层级。
适用场景
-
需要表示树形结构(菜单、文件系统、组织架构)
-
希望客户端忽略组合对象和单个对象的差异
-
例如:UI 组件树、游戏中的队伍/军团系统
代码案例:英雄编队系统(单英雄 vs 队伍 vs 军团)
package main
import (
"fmt"
)
// ===== 组件接口:战斗单位 =====
type Unit interface {
ShowInfo(indent string)
GetPower() int
}
// ===== 叶子节点:单个英雄 =====
type Hero struct {
Name string
Power int
}
func (h *Hero) ShowInfo(indent string) {
fmt.Printf("%s🧑 英雄: %s (战力: %d)\n", indent, h.Name, h.Power)
}
func (h *Hero) GetPower() int {
return h.Power
}
// ===== 组合节点:队伍/军团 =====
type Squad struct {
Name string
members []Unit
}
func NewSquad(name string) *Squad {
return &Squad{Name: name, members: []Unit{}}
}
func (s *Squad) Add(member Unit) {
s.members = append(s.members, member)
}
func (s *Squad) Remove(member Unit) {
// 简化:实际应查找并删除
}
func (s *Squad) ShowInfo(indent string) {
fmt.Printf("%s🛡️ 队伍: %s\n", indent, s.Name)
for _, m := range s.members {
m.ShowInfo(indent + " ")
}
}
func (s *Squad) GetPower() int {
total := 0
for _, m := range s.members {
total += m.GetPower()
}
return total
}
func main() {
// 创建单个英雄
hero1 := &Hero{Name: "亚瑟", Power: 100}
hero2 := &Hero{Name: "甘道夫", Power: 120}
hero3 := &Hero{Name: "Legolas", Power: 90}
hero4 := &Hero{Name: "赵云", Power: 110}
// 创建第一小队
squad1 := NewSquad("先锋小队")
squad1.Add(hero1)
squad1.Add(hero2)
// 创建第二小队
squad2 := NewSquad("远程小队")
squad2.Add(hero3)
squad2.Add(hero4)
// 创建军团(组合里套组合!)
legion := NewSquad("无敌军团")
legion.Add(squad1)
legion.Add(squad2)
// 统一展示:无论是单个英雄还是军团,都用 ShowInfo
fmt.Println("=== 展示整个军团结构 ===")
legion.ShowInfo("")
// 统一计算战力
fmt.Printf("\n=== 总战力统计 ===\n")
fmt.Printf("亚瑟战力: %d\n", hero1.GetPower())
fmt.Printf("先锋小队战力: %d\n", squad1.GetPower())
fmt.Printf("无敌军团总战力: %d\n", legion.GetPower())
}
运行结果
=== 展示整个军团结构 ===
🛡️ 队伍: 无敌军团
🛡️ 队伍: 先锋小队
🧑 英雄: 亚瑟 (战力: 100)
🧑 英雄: 甘道夫 (战力: 120)
🛡️ 队伍: 远程小队
🧑 英雄: Legolas (战力: 90)
🧑 英雄: 赵云 (战力: 110)
=== 总战力统计 ===
亚瑟战力: 100
先锋小队战力: 220
无敌军团总战力: 420
关键点
-
Squad(组合)和Hero(叶子)都实现了Unit接口 -
客户端完全不用区分"这是单个英雄还是队伍" ,统一调用
ShowInfo和GetPower -
组合模式是递归的:军团里可以有小队,小队里还可以有小队
4. 装饰器模式(Decorator Pattern)
是什么?
动态地给一个对象添加一些额外的职责,而不影响其他对象。
为什么需要?
你的英雄基础攻击力是 100。现在要给英雄加装备:
-
火焰剑:攻击力 +20
-
暴击戒指:攻击力 ×1.5
-
力量药水:攻击力 +10
如果用继承,你需要:
-
HeroWithFireSword -
HeroWithCriticalRing -
HeroWithFireSwordAndCriticalRing -
... 组合爆炸!
装饰器模式说:把每个装备做成一个"装饰器",像穿衣服一样,一层一层套在英雄身上。
适用场景
-
需要动态、透明地给对象添加职责
-
需要扩展功能,但不想用子类
-
例如:HTTP 中间件(日志、认证、压缩一层层套)、I/O 流包装(
bufio.Reader包装os.File)
代码案例:英雄装备系统(动态叠加装备)
package main
import (
"fmt"
)
// ===== 抽象组件:英雄接口 =====
type Hero interface {
GetDescription() string
GetAttackPower() int
}
// ===== 具体组件:基础英雄 =====
type BaseHero struct {
Name string
}
func (b *BaseHero) GetDescription() string {
return b.Name
}
func (b *BaseHero) GetAttackPower() int {
return 100 // 基础攻击力
}
// ===== 装饰器基类(可选,Go 中可用嵌入简化)=====
type HeroDecorator struct {
hero Hero
}
// ===== 具体装饰器:火焰剑 =====
type FireSword struct {
HeroDecorator
}
func NewFireSword(h Hero) Hero {
return &FireSword{HeroDecorator{hero: h}}
}
func (f *FireSword) GetDescription() string {
return f.hero.GetDescription() + " + 🔥火焰剑"
}
func (f *FireSword) GetAttackPower() int {
return f.hero.GetAttackPower() + 20
}
// ===== 具体装饰器:暴击戒指 =====
type CriticalRing struct {
HeroDecorator
}
func NewCriticalRing(h Hero) Hero {
return &CriticalRing{HeroDecorator{hero: h}}
}
func (c *CriticalRing) GetDescription() string {
return c.hero.GetDescription() + " + 💍暴击戒指"
}
func (c *CriticalRing) GetAttackPower() int {
// 暴击戒指:攻击力提升 50%
base := c.hero.GetAttackPower()
return int(float64(base) * 1.5)
}
// ===== 具体装饰器:力量药水 =====
type PowerPotion struct {
HeroDecorator
}
func NewPowerPotion(h Hero) Hero {
return &PowerPotion{HeroDecorator{hero: h}}
}
func (p *PowerPotion) GetDescription() string {
return p.hero.GetDescription() + " + 🧪力量药水"
}
func (p *PowerPotion) GetAttackPower() int {
return p.hero.GetAttackPower() + 10
}
func main() {
// 创建一个基础英雄
hero := &BaseHero{Name: "亚瑟"}
fmt.Printf("基础英雄: %s | 攻击力: %d\n\n", hero.GetDescription(), hero.GetAttackPower())
// 给英雄装备火焰剑
heroWithSword := NewFireSword(hero)
fmt.Printf("装备火焰剑: %s | 攻击力: %d\n\n", heroWithSword.GetDescription(), heroWithSword.GetAttackPower())
// 再装备暴击戒指(在火焰剑的基础上叠加)
heroWithSwordAndRing := NewCriticalRing(heroWithSword)
fmt.Printf("再装备暴击戒指: %s | 攻击力: %d\n\n",
heroWithSwordAndRing.GetDescription(), heroWithSwordAndRing.GetAttackPower())
// 再喝力量药水(继续叠加)
superHero := NewPowerPotion(heroWithSwordAndRing)
fmt.Printf("再喝力量药水: %s | 攻击力: %d\n\n",
superHero.GetDescription(), superHero.GetAttackPower())
// 另一种装备顺序:先药水,再火焰剑,再暴击戒指
hero2 := NewCriticalRing(NewFireSword(NewPowerPotion(&BaseHero{Name: "甘道夫"})))
fmt.Printf("不同顺序装备: %s | 攻击力: %d\n",
hero2.GetDescription(), hero2.GetAttackPower())
}
运行结果
基础英雄: 亚瑟 | 攻击力: 100
装备火焰剑: 亚瑟 + 🔥火焰剑 | 攻击力: 120
再装备暴击戒指: 亚瑟 + 🔥火焰剑 + 💍暴击戒指 | 攻击力: 180
再喝力量药水: 亚瑟 + 🔥火焰剑 + 💍暴击戒指 + 🧪力量药水 | 攻击力: 190
不同顺序装备: 甘道夫 + 🧪力量药水 + 🔥火焰剑 + 💍暴击戒指 | 攻击力: 195
关键点
-
每个装饰器都实现了
Hero接口,所以可以无限嵌套 -
装饰器在调用
hero.GetAttackPower()前后添加自己的逻辑 -
顺序很重要:先喝药水(+10)再戴暴击戒指(×1.5) 和 先戴暴击戒指(×1.5)再喝药水(+10) 结果不同!
5. 适配器模式(Adapter Pattern)
是什么?
将一个类的接口转换成客户希望的另外一个接口。适配器让原本接口不兼容的类可以合作。
为什么需要?
你的游戏系统已经定义了 GameHero 接口(要求有 Attack() 方法)。现在你想接入一个第三方英雄库 ,但第三方库的英雄用的是 SpecialMove() 方法,而且结构完全不同。
你不能修改第三方库(可能是闭源的),也不想改自己的系统 。适配器就像电源转换插头,把第三方的接口转换成你的系统能用的接口。
适用场景
-
使用已有的类,但接口不符合需求
-
接入第三方库/遗留系统
-
统一不同数据源的接口
-
例如:不同支付接口的统一适配、不同数据库驱动的统一
代码案例:接入第三方英雄库
package main
import (
"fmt"
)
// ===== 我们的系统接口 =====
type GameHero interface {
Attack() string
GetName() string
}
// ===== 我们原有的英雄 =====
type NativeHero struct {
name string
}
func (n *NativeHero) GetName() string {
return n.name
}
func (n *NativeHero) Attack() string {
return fmt.Sprintf("%s 使用【普通攻击】!", n.name)
}
// ===== 第三方库(假设这是别人写的,我们不能修改)=====
type ThirdPartyLibrary struct{}
// 第三方英雄结构
type ThirdPartyCharacter struct {
CharName string
CharLevel int
CharSkill string
}
// 第三方创建方法
func (t *ThirdPartyLibrary) CreateCharacter(name string) *ThirdPartyCharacter {
return &ThirdPartyCharacter{
CharName: name,
CharLevel: 50,
CharSkill: "神秘招式",
}
}
// 第三方的方法名和我们的接口完全不同!
func (c *ThirdPartyCharacter) SpecialMove() string {
return fmt.Sprintf("%s(Lv.%d) 释放【%s】!", c.CharName, c.CharLevel, c.CharSkill)
}
func (c *ThirdPartyCharacter) QueryName() string {
return c.CharName
}
// ===== 适配器:把第三方英雄适配成我们的 GameHero 接口 =====
type ThirdPartyHeroAdapter struct {
thirdParty *ThirdPartyCharacter
}
func NewThirdPartyHeroAdapter(third *ThirdPartyCharacter) GameHero {
return &ThirdPartyHeroAdapter{thirdParty: third}
}
func (a *ThirdPartyHeroAdapter) GetName() string {
return a.thirdParty.QueryName()
}
func (a *ThirdPartyHeroAdapter) Attack() string {
// 把第三方的 SpecialMove 适配成我们的 Attack
return a.thirdParty.SpecialMove()
}
// ===== 游戏系统:统一处理所有英雄 =====
func Battle(hero GameHero) {
fmt.Printf("英雄 %s 加入战斗!\n", hero.GetName())
fmt.Println(hero.Attack())
fmt.Println("---")
}
func main() {
// 我们的原生英雄
native := &NativeHero{name: "亚瑟"}
Battle(native)
// 第三方英雄(通过适配器接入)
thirdLib := &ThirdPartyLibrary{}
thirdChar := thirdLib.CreateCharacter("神秘剑客")
adapter := NewThirdPartyHeroAdapter(thirdChar)
Battle(adapter)
// 再来一个第三方英雄
thirdChar2 := thirdLib.CreateCharacter("暗影刺客")
adapter2 := NewThirdPartyHeroAdapter(thirdChar2)
Battle(adapter2)
}
运行结果
英雄 亚瑟 加入战斗!
亚瑟 使用【普通攻击】!
---
英雄 神秘剑客 加入战斗!
神秘剑客(Lv.50) 释放【神秘招式】!
---
英雄 暗影刺客 加入战斗!
暗影刺客(Lv.50) 释放【神秘招式】!
---
关键点
-
适配器持有第三方对象的引用,把
SpecialMove()包装成Attack() -
游戏系统
Battle()完全不知道自己在调用第三方库,它只认GameHero接口 -
这是对象适配器 (用组合),Go 中最常用的方式
6. 外观模式(Facade Pattern)
是什么?
为子系统中的一组接口提供一个统一的、高层接口。外观模式定义了一个简单入口,让子系统更容易使用。
为什么需要?
玩家点击"开始冒险"按钮,背后其实要做很多事情:
-
加载玩家英雄数据
-
加载游戏地图
-
连接游戏服务器
-
初始化物理引擎
-
播放背景音乐
-
显示加载界面
如果让玩家自己调用 6 个不同的子系统,太复杂了!外观模式提供一个 StartGame() 方法,一键搞定。
适用场景
-
为一个复杂的子系统提供简单入口
-
将子系统与客户程序解耦
-
分层架构中,层与层之间通过外观交互
-
例如:开机按钮(背后涉及 BIOS、硬盘、内存、显卡初始化)、启动 Spring Boot 应用
代码案例:游戏启动系统
package main
import (
"fmt"
)
// ===== 子系统1:英雄加载器 =====
type HeroLoader struct{}
func (h *HeroLoader) LoadHeroData() {
fmt.Println(" [英雄系统] 加载英雄数据... ✅")
}
// ===== 子系统2:地图加载器 =====
type MapLoader struct{}
func (m *MapLoader) LoadMap() {
fmt.Println(" [地图系统] 加载场景地图... ✅")
}
// ===== 子系统3:网络管理器 =====
type NetManager struct{}
func (n *NetManager) ConnectServer() {
fmt.Println(" [网络系统] 连接游戏服务器... ✅")
}
func (n *NetManager) SyncData() {
fmt.Println(" [网络系统] 同步玩家数据... ✅")
}
// ===== 子系统4:音效播放器 =====
type AudioPlayer struct{}
func (a *AudioPlayer) PlayBGM() {
fmt.Println(" [音效系统] 播放背景音乐... ✅")
}
func (a *AudioPlayer) PlaySoundEffect(name string) {
fmt.Printf(" [音效系统] 播放音效: %s... ✅\n", name)
}
// ===== 子系统5:UI 管理器 =====
type UIManager struct{}
func (u *UIManager) ShowLoadingScreen() {
fmt.Println(" [UI系统] 显示加载界面... ✅")
}
func (u *UIManager) HideLoadingScreen() {
fmt.Println(" [UI系统] 隐藏加载界面... ✅")
}
func (u *UIManager) ShowMainMenu() {
fmt.Println(" [UI系统] 显示主菜单... ✅")
}
// ===== 外观类:游戏启动器 =====
type GameLauncher struct {
heroLoader *HeroLoader
mapLoader *MapLoader
netManager *NetManager
audioPlayer *AudioPlayer
uiManager *UIManager
}
func NewGameLauncher() *GameLauncher {
return &GameLauncher{
heroLoader: &HeroLoader{},
mapLoader: &MapLoader{},
netManager: &NetManager{},
audioPlayer: &AudioPlayer{},
uiManager: &UIManager{},
}
}
// 一键启动游戏(外观方法)
func (g *GameLauncher) StartGame() {
fmt.Println("🎮 === 游戏启动中 ===")
g.uiManager.ShowLoadingScreen()
g.heroLoader.LoadHeroData()
g.mapLoader.LoadMap()
g.netManager.ConnectServer()
g.netManager.SyncData()
g.audioPlayer.PlayBGM()
g.uiManager.HideLoadingScreen()
g.uiManager.ShowMainMenu()
fmt.Println("🎮 === 游戏启动完成,欢迎冒险! ===")
}
// 一键退出游戏
func (g *GameLauncher) ExitGame() {
fmt.Println("\n🛑 === 游戏退出中 ===")
g.audioPlayer.PlaySoundEffect("退出音效")
g.netManager.SyncData() // 保存数据
fmt.Println("🛑 === 游戏已安全退出 ===")
}
func main() {
// 玩家只需调用外观类,完全不用关心背后有多少子系统
launcher := NewGameLauncher()
launcher.StartGame()
// 模拟游戏过程
fmt.Println("\n... 玩家正在冒险 ...")
// 一键退出
launcher.ExitGame()
}
运行结果
🎮 === 游戏启动中 ===
[UI系统] 显示加载界面... ✅
[英雄系统] 加载英雄数据... ✅
[地图系统] 加载场景地图... ✅
[网络系统] 连接游戏服务器... ✅
[网络系统] 同步玩家数据... ✅
[音效系统] 播放背景音乐... ✅
[UI系统] 隐藏加载界面... ✅
[UI系统] 显示主菜单... ✅
🎮 === 游戏启动完成,欢迎冒险! ===
... 玩家正在冒险 ...
🛑 === 游戏退出中 ===
[音效系统] 播放音效: 退出音效... ✅
[网络系统] 同步玩家数据... ✅
🛑 === 游戏已安全退出 ===
关键点
-
外观模式不隐藏子系统,只是提供简化的入口
-
客户端仍然可以直接访问子系统(如果需要精细控制),但通常不需要
-
降低了客户端与子系统的耦合度
7. 享元模式(Flyweight Pattern)
是什么?
运用共享技术有效地支持大量细粒度对象的复用。
为什么需要?
你的游戏要渲染 1000 个小兵(Soldier)。每个小兵都有:
-
模型数据(3D 模型、贴图、动画)------ 占内存 10MB
-
位置坐标 (x, y) ------ 占内存 8 字节
-
生命值 ------ 占内存 4 字节
如果创建 1000 个实例,仅模型数据就要 10GB!但实际上所有小兵共享同一个模型,只有位置和生命值不同。
享元模式把对象分成:
-
内部状态(Intrinsic):共享的、不变的(如模型、贴图)
-
外部状态(Extrinsic):独立的、变化的(如位置、生命值)
适用场景
-
大量相似对象,造成内存压力
-
对象的大部分状态可以外部化
-
需要缓存共享对象
-
例如:围棋棋子(只有黑白两种,位置是外部状态)、文字渲染(相同字共享字形)、游戏粒子系统
代码案例:游戏小兵系统(1000 个小兵共享模型)
package main
import (
"fmt"
"math/rand"
)
// ===== 享元对象:小兵模型(共享部分)=====
type SoldierModel struct {
ModelName string
Texture string
Animation string
}
func (s *SoldierModel) ShowModel() string {
return fmt.Sprintf("模型:%s 贴图:%s 动画:%s", s.ModelName, s.Texture, s.Animation)
}
// ===== 享元工厂:管理和复用模型 =====
type SoldierModelFactory struct {
models map[string]*SoldierModel
}
func NewSoldierModelFactory() *SoldierModelFactory {
return &SoldierModelFactory{
models: make(map[string]*SoldierModel),
}
}
func (f *SoldierModelFactory) GetModel(modelType string) *SoldierModel {
if model, ok := f.models[modelType]; ok {
fmt.Printf(" [享元复用] 复用已有模型: %s\n", modelType)
return model
}
// 创建新模型(模拟加载资源)
fmt.Printf(" [享元创建] 首次加载模型: %s\n", modelType)
var model *SoldierModel
switch modelType {
case "步兵":
model = &SoldierModel{ModelName: "步兵模型", Texture: "步兵贴图", Animation: "步兵动画"}
case "骑兵":
model = &SoldierModel{ModelName: "骑兵模型", Texture: "骑兵贴图", Animation: "骑兵动画"}
case "弓箭手":
model = &SoldierModel{ModelName: "弓箭手模型", Texture: "弓箭手贴图", Animation: "弓箭手动画"}
}
f.models[modelType] = model
return model
}
// ===== 小兵对象(包含享元引用 + 外部状态)=====
type Soldier struct {
ID int
X, Y int
HP int
Model *SoldierModel // 享元引用(共享)
}
func (s *Soldier) ShowInfo() {
fmt.Printf(" 小兵[%d] 位置(%d,%d) 血量:%d | %s\n",
s.ID, s.X, s.Y, s.HP, s.Model.ShowModel())
}
// ===== 游戏场景:生成大量小兵 =====
type GameScene struct {
factory *SoldierModelFactory
soldiers []*Soldier
}
func NewGameScene() *GameScene {
return &GameScene{
factory: NewSoldierModelFactory(),
soldiers: []*Soldier{},
}
}
func (g *GameScene) SpawnSoldier(modelType string, id int) {
model := g.factory.GetModel(modelType)
soldier := &Soldier{
ID: id,
X: rand.Intn(100),
Y: rand.Intn(100),
HP: 100,
Model: model, // 共享同一个模型!
}
g.soldiers = append(g.soldiers, soldier)
}
func (g *GameScene) ShowAllSoldiers() {
fmt.Println("\n=== 当前场景所有小兵 ===")
for _, s := range g.soldiers {
s.ShowInfo()
}
}
func main() {
scene := NewGameScene()
// 生成10个步兵(共享同一个步兵模型)
fmt.Println("=== 生成步兵 ===")
for i := 1; i <= 5; i++ {
scene.SpawnSoldier("步兵", i)
}
// 生成5个骑兵
fmt.Println("\n=== 生成骑兵 ===")
for i := 6; i <= 10; i++ {
scene.SpawnSoldier("骑兵", i)
}
// 再生成3个步兵(这次应该复用已有模型)
fmt.Println("\n=== 再生成步兵(复用) ===")
for i := 11; i <= 13; i++ {
scene.SpawnSoldier("步兵", i)
}
scene.ShowAllSoldiers()
// 统计模型数量
fmt.Printf("\n=== 内存优化效果 ===\n")
fmt.Printf("小兵总数: %d\n", len(scene.soldiers))
fmt.Printf("实际加载的模型数: %d (步兵+骑兵)\n", len(scene.factory.models))
fmt.Printf("节省的模型内存: 避免了加载 %d 个重复模型\n", len(scene.soldiers)-len(scene.factory.models))
}
运行结果
=== 生成步兵 ===
[享元创建] 首次加载模型: 步兵
[享元复用] 复用已有模型: 步兵
[享元复用] 复用已有模型: 步兵
[享元复用] 复用已有模型: 步兵
[享元复用] 复用已有模型: 步兵
=== 生成骑兵 ===
[享元创建] 首次加载模型: 骑兵
[享元复用] 复用已有模型: 骑兵
[享元复用] 复用已有模型: 骑兵
[享元复用] 复用已有模型: 骑兵
[享元复用] 复用已有模型: 骑兵
=== 再生成步兵(复用) ===
[享元复用] 复用已有模型: 步兵
[享元复用] 复用已有模型: 步兵
[享元复用] 复用已有模型: 步兵
=== 当前场景所有小兵 ===
小兵[1] 位置(81,87) 血量:100 | 模型:步兵模型 贴图:步兵贴图 动画:步兵动画
小兵[2] 位置(47,59) 血量:100 | 模型:步兵模型 贴图:步兵贴图 动画:步兵动画
...
小兵[11] 位置(62,28) 血量:100 | 模型:步兵模型 贴图:步兵贴图 动画:步兵动画
=== 内存优化效果 ===
小兵总数: 13
实际加载的模型数: 2 (步兵+骑兵)
节省的模型内存: 避免了加载 11 个重复模型
关键点
-
享元工厂 负责创建和复用共享对象
-
每个小兵只保存外部状态 (ID、位置、血量),内部状态 (模型)通过指针共享
-
13 个小兵只有 2 个模型实例,内存大幅降低
结构型模式总结
| 模式 | 核心思想 | 一句话记忆 | 游戏案例 |
|---|---|---|---|
| 代理 | 控制对象访问 | 找个替身挡在前面 | 缓存代理查询英雄信息 |
| 桥接 | 抽象与实现分离 | 两个维度独立变化 | 展示方式 × 渲染引擎 |
| 组合 | 树形结构,统一处理 | 整体和部分一视同仁 | 英雄/小队/军团统一算战力 |
| 装饰器 | 动态叠加职责 | 像穿衣服一样叠加 | 火焰剑 + 暴击戒指 + 力量药水 |
| 适配器 | 接口转换 | 电源转换插头 | 第三方英雄库接入我方系统 |
| 外观 | 简化复杂系统入口 | 一键启动 | 一键开始游戏(背后涉及6个子系统) |
| 享元 | 共享细粒度对象 | 共享模型,各自状态 | 1000 个小兵共享 2 个模型 |
Go 语言设计模式入门(三):行为型模式
行为型模式的核心问题是:对象之间如何交互?如何分配职责? 如果说创建型模式解决"怎么造对象",结构型模式解决"怎么组合对象",那行为型模式就是解决**"对象之间怎么说话、怎么配合"**。
我们继续用 RPG 游戏英雄系统 作为统一场景。
1. 责任链模式(Chain of Responsibility)
是什么?
让多个对象都有机会处理请求,将这些对象连成一条链,请求沿着链传递,直到有对象处理它为止。
为什么需要?
玩家在游戏中发起一个"使用道具"请求,这个请求可能需要经过多层处理:
-
先检查背包里有没有这个道具
-
再检查当前状态是否允许使用(比如眩晕状态不能用)
-
再检查冷却时间是否结束
-
最后才真正使用
如果把这些判断写在一个巨大的 if-else 里,代码会臃肿不堪。责任链模式把每个检查做成一个"处理器",像流水线一样依次处理。
适用场景
-
多个对象可以处理同一请求,但具体由谁处理在运行时才确定
-
想在不明确指定接收者的情况下,向多个对象中的一个提交请求
-
例如:审批流程、日志级别过滤、游戏指令处理
代码案例:游戏指令处理链
package main
import (
"fmt"
)
// ===== 请求对象 =====
type GameRequest struct {
Command string // 指令,如 "attack", "heal", "flee"
Target string // 目标
}
// ===== 处理器接口 =====
type Handler interface {
SetNext(handler Handler) Handler
Handle(request *GameRequest) string
}
// ===== 基础处理器(抽象基类)=====
type BaseHandler struct {
next Handler
}
func (b *BaseHandler) SetNext(handler Handler) Handler {
b.next = handler
return handler
}
func (b *BaseHandler) HandleNext(request *GameRequest) string {
if b.next != nil {
return b.next.Handle(request)
}
return "请求未被处理"
}
// ===== 具体处理器1:状态检查 =====
type StateCheckHandler struct {
BaseHandler
}
func (s *StateCheckHandler) Handle(request *GameRequest) string {
fmt.Printf("[状态检查] 检查玩家状态...\n")
// 模拟:如果指令是 "attack" 且玩家处于异常状态
if request.Command == "attack" {
fmt.Printf("[状态检查] 玩家状态正常,放行 ✅\n")
return s.HandleNext(request)
}
fmt.Printf("[状态检查] 无需状态检查,放行 ✅\n")
return s.HandleNext(request)
}
// ===== 具体处理器2:冷却检查 =====
type CooldownHandler struct {
BaseHandler
}
func (c *CooldownHandler) Handle(request *GameRequest) string {
fmt.Printf("[冷却检查] 检查技能冷却...\n")
// 模拟:所有指令都通过冷却检查
fmt.Printf("[冷却检查] 冷却已结束,放行 ✅\n")
return c.HandleNext(request)
}
// ===== 具体处理器3:权限检查 =====
type PermissionHandler struct {
BaseHandler
}
func (p *PermissionHandler) Handle(request *GameRequest) string {
fmt.Printf("[权限检查] 检查操作权限...\n")
if request.Command == "flee" {
fmt.Printf("[权限检查] 逃跑权限不足,拒绝 ❌\n")
return "拒绝:当前场景不允许逃跑!"
}
fmt.Printf("[权限检查] 权限通过,放行 ✅\n")
return p.HandleNext(request)
}
// ===== 具体处理器4:执行指令 =====
type ExecuteHandler struct {
BaseHandler
}
func (e *ExecuteHandler) Handle(request *GameRequest) string {
fmt.Printf("[执行器] 正在执行指令: %s\n", request.Command)
switch request.Command {
case "attack":
return fmt.Sprintf("执行成功:对 %s 发起攻击!", request.Target)
case "heal":
return fmt.Sprintf("执行成功:使用治疗药水,恢复生命值!")
default:
return "执行成功:指令已处理"
}
}
func main() {
// 组装责任链:状态检查 -> 冷却检查 -> 权限检查 -> 执行
state := &StateCheckHandler{}
cooldown := &CooldownHandler{}
permission := &PermissionHandler{}
execute := &ExecuteHandler{}
state.SetNext(cooldown).SetNext(permission).SetNext(execute)
// 测试1:正常攻击请求
fmt.Println("=== 测试1:发起攻击 ===")
req1 := &GameRequest{Command: "attack", Target: "史莱姆"}
result1 := state.Handle(req1)
fmt.Printf("结果: %s\n\n", result1)
// 测试2:逃跑请求(会被权限检查拦截)
fmt.Println("=== 测试2:尝试逃跑 ===")
req2 := &GameRequest{Command: "flee", Target: ""}
result2 := state.Handle(req2)
fmt.Printf("结果: %s\n\n", result2)
// 测试3:治疗请求
fmt.Println("=== 测试3:使用治疗 ===")
req3 := &GameRequest{Command: "heal", Target: ""}
result3 := state.Handle(req3)
fmt.Printf("结果: %s\n", result3)
}
运行结果
=== 测试1:发起攻击 ===
[状态检查] 检查玩家状态...
[状态检查] 玩家状态正常,放行 ✅
[冷却检查] 检查技能冷却...
[冷却检查] 冷却已结束,放行 ✅
[权限检查] 检查操作权限...
[权限检查] 权限通过,放行 ✅
[执行器] 正在执行指令: attack
结果: 执行成功:对 史莱姆 发起攻击!
=== 测试2:尝试逃跑 ===
[状态检查] 检查玩家状态...
[状态检查] 无需状态检查,放行 ✅
[冷却检查] 检查技能冷却...
[冷却检查] 冷却已结束,放行 ✅
[权限检查] 检查操作权限...
[权限检查] 逃跑权限不足,拒绝 ❌
结果: 拒绝:当前场景不允许逃跑!
=== 测试3:使用治疗 ===
[状态检查] 检查玩家状态...
[状态检查] 无需状态检查,放行 ✅
[冷却检查] 检查技能冷却...
[冷却检查] 冷却已结束,放行 ✅
[权限检查] 检查操作权限...
[权限检查] 权限通过,放行 ✅
[执行器] 正在执行指令: heal
结果: 执行成功:使用治疗药水,恢复生命值!
关键点
-
每个处理器只关心自己的逻辑,通过或拒绝
-
链条可以动态组装,运行时决定处理顺序
-
解耦了发送者和接收者,发送者不知道谁会处理请求
2. 命令模式(Command Pattern)
是什么?
将请求封装为一个对象,从而可以用不同的请求、队列或日志来参数化其他对象。支持撤销 和重做。
为什么需要?
玩家按下了"攻击"按钮,这背后:
-
要记录操作日志
-
要支持"撤销上一步"(比如误操作)
-
要把多个操作组合成"宏命令"(一键连招)
命令模式把"攻击"这个动作封装成一个对象,这个对象可以存储、传递、撤销。
适用场景
-
需要支持撤销/重做
-
需要将操作排队、记录日志
-
需要组合多个操作(宏)
-
例如:编辑器撤销、游戏回放系统、事务系统
代码案例:游戏操作与撤销系统
package main
import (
"fmt"
)
// ===== 命令接口 =====
type Command interface {
Execute() string
Undo() string
GetName() string
}
// ===== 接收者:英雄(真正执行动作的对象)=====
type Hero struct {
Name string
X, Y int
HP int
}
func NewHero(name string) *Hero {
return &Hero{Name: name, X: 0, Y: 0, HP: 100}
}
func (h *Hero) MoveTo(x, y int) {
h.X, h.Y = x, y
}
func (h *Hero) TakeDamage(damage int) {
h.HP -= damage
if h.HP < 0 {
h.HP = 0
}
}
func (h *Hero) Heal(amount int) {
h.HP += amount
if h.HP > 100 {
h.HP = 100
}
}
func (h *Hero) Status() string {
return fmt.Sprintf("%s 位置(%d,%d) 血量:%d", h.Name, h.X, h.Y, h.HP)
}
// ===== 具体命令:移动命令 =====
type MoveCommand struct {
hero *Hero
oldX, oldY int
newX, newY int
}
func NewMoveCommand(hero *Hero, x, y int) *MoveCommand {
return &MoveCommand{
hero: hero,
oldX: hero.X, oldY: hero.Y,
newX: x, newY: y,
}
}
func (c *MoveCommand) Execute() string {
c.hero.MoveTo(c.newX, c.newY)
return fmt.Sprintf("移动至 (%d,%d)", c.newX, c.newY)
}
func (c *MoveCommand) Undo() string {
c.hero.MoveTo(c.oldX, c.oldY)
return fmt.Sprintf("撤销移动,回到 (%d,%d)", c.oldX, c.oldY)
}
func (c *MoveCommand) GetName() string {
return "移动"
}
// ===== 具体命令:攻击命令 =====
type AttackCommand struct {
hero *Hero
target string
}
func NewAttackCommand(hero *Hero, target string) *AttackCommand {
return &AttackCommand{hero: hero, target: target}
}
func (c *AttackCommand) Execute() string {
return fmt.Sprintf("%s 攻击了 %s!", c.hero.Name, c.target)
}
func (c *AttackCommand) Undo() string {
return fmt.Sprintf("撤销攻击 %s(时光倒流)", c.target)
}
func (c *AttackCommand) GetName() string {
return "攻击"
}
// ===== 具体命令:治疗命令 =====
type HealCommand struct {
hero *Hero
amount int
}
func NewHealCommand(hero *Hero, amount int) *HealCommand {
return &HealCommand{hero: hero, amount: amount}
}
func (c *HealCommand) Execute() string {
c.hero.Heal(c.amount)
return fmt.Sprintf("治疗 +%d HP", c.amount)
}
func (c *HealCommand) Undo() string {
c.hero.TakeDamage(c.amount)
return fmt.Sprintf("撤销治疗,扣除 %d HP", c.amount)
}
func (c *HealCommand) GetName() string {
return "治疗"
}
// ===== 宏命令:组合多个命令 =====
type MacroCommand struct {
name string
commands []Command
}
func NewMacroCommand(name string) *MacroCommand {
return &MacroCommand{name: name, commands: []Command{}}
}
func (m *MacroCommand) Add(cmd Command) {
m.commands = append(m.commands, cmd)
}
func (m *MacroCommand) Execute() string {
result := fmt.Sprintf("【宏:%s】开始执行:\n", m.name)
for _, cmd := range m.commands {
result += " - " + cmd.Execute() + "\n"
}
return result
}
func (m *MacroCommand) Undo() string {
result := fmt.Sprintf("【宏:%s】撤销:\n", m.name)
// 撤销要逆序执行
for i := len(m.commands) - 1; i >= 0; i-- {
result += " - " + m.commands[i].Undo() + "\n"
}
return result
}
func (m *MacroCommand) GetName() string {
return m.name
}
// ===== 命令历史(用于撤销)=====
type CommandHistory struct {
history []Command
}
func (ch *CommandHistory) Push(cmd Command) {
ch.history = append(ch.history, cmd)
}
func (ch *CommandHistory) Undo() string {
if len(ch.history) == 0 {
return "没有可撤销的操作"
}
// 取出最后一个命令
last := ch.history[len(ch.history)-1]
ch.history = ch.history[:len(ch.history)-1]
return last.Undo()
}
func main() {
hero := NewHero("亚瑟")
history := &CommandHistory{}
fmt.Println("初始状态:", hero.Status())
// 执行移动
cmd1 := NewMoveCommand(hero, 10, 20)
fmt.Println("执行:", cmd1.Execute())
history.Push(cmd1)
fmt.Println("当前状态:", hero.Status())
// 执行治疗
cmd2 := NewHealCommand(hero, 30)
fmt.Println("执行:", cmd2.Execute())
history.Push(cmd2)
fmt.Println("当前状态:", hero.Status())
// 执行攻击
cmd3 := NewAttackCommand(hero, "史莱姆")
fmt.Println("执行:", cmd3.Execute())
history.Push(cmd3)
// 撤销攻击
fmt.Println("\n撤销:", history.Undo())
// 撤销治疗
fmt.Println("撤销:", history.Undo())
fmt.Println("当前状态:", hero.Status())
// 撤销移动
fmt.Println("撤销:", history.Undo())
fmt.Println("当前状态:", hero.Status())
// 宏命令演示:一键连招
fmt.Println("\n=== 宏命令演示 ===")
hero2 := NewHero("甘道夫")
macro := NewMacroCommand("终极连招")
macro.Add(NewMoveCommand(hero2, 5, 5))
macro.Add(NewAttackCommand(hero2, "Boss"))
macro.Add(NewHealCommand(hero2, 50))
fmt.Println(macro.Execute())
fmt.Println("宏后状态:", hero2.Status())
fmt.Println(macro.Undo())
fmt.Println("撤销后状态:", hero2.Status())
}
运行结果
初始状态: 亚瑟 位置(0,0) 血量:100
执行: 移动至 (10,20)
当前状态: 亚瑟 位置(10,20) 血量:100
执行: 治疗 +30 HP
当前状态: 亚瑟 位置(10,20) 血量:100
执行: 亚瑟 攻击了 史莱姆!
撤销: 撤销攻击 史莱姆(时光倒流)
撤销: 撤销治疗,扣除 30 HP
当前状态: 亚瑟 位置(10,20) 血量:100
撤销: 撤销移动,回到 (0,0)
当前状态: 亚瑟 位置(0,0) 血量:100
=== 宏命令演示 ===
【宏:终极连招】开始执行:
- 移动至 (5,5)
- 甘道夫 攻击了 Boss!
- 治疗 +50 HP
宏后状态: 甘道夫 位置(5,5) 血量:100
【宏:终极连招】撤销:
- 撤销治疗,扣除 50 HP
- 撤销攻击 Boss(时光倒流)
- 撤销移动,回到 (0,0)
撤销后状态: 甘道夫 位置(0,0) 血量:100
关键点
-
命令对象封装了"做什么"和"怎么撤销"
-
MacroCommand把多个命令组合,执行顺序执行,撤销逆序执行 -
这是游戏回放系统 和撤销功能的基础
3. 解释器模式(Interpreter Pattern)
是什么?
给定一个语言,定义它的文法表示,并定义一个解释器来解释语言中的句子。
为什么需要?
你的游戏想支持简单的脚本或聊天指令,比如:
-
/attack 史莱姆 -
if hp < 50 then heal -
move 10 20
解释器模式把这些字符串解析成抽象语法树(AST),然后执行。
适用场景
-
需要解释执行简单的语法规则
-
文法简单且相对固定
-
例如:正则表达式引擎、简单的规则引擎、游戏脚本语言
代码案例:简单游戏指令解释器
package main
import (
"fmt"
"strconv"
"strings"
)
// ===== 表达式接口(抽象表达式)=====
type Expression interface {
Interpret(context *GameContext) string
}
// ===== 上下文:游戏环境 =====
type GameContext struct {
HeroName string
HP int
MP int
X, Y int
}
// ===== 终结符表达式:移动指令 "move x y" =====
type MoveExpression struct {
x, y int
}
func NewMoveExpression(args []string) (*MoveExpression, error) {
if len(args) != 2 {
return nil, fmt.Errorf("move 需要2个参数")
}
x, _ := strconv.Atoi(args[0])
y, _ := strconv.Atoi(args[1])
return &MoveExpression{x: x, y: y}, nil
}
func (m *MoveExpression) Interpret(ctx *GameContext) string {
ctx.X = m.x
ctx.Y = m.y
return fmt.Sprintf("移动至坐标 (%d, %d)", m.x, m.y)
}
// ===== 终结符表达式:攻击指令 "attack target" =====
type AttackExpression struct {
target string
}
func NewAttackExpression(args []string) (*AttackExpression, error) {
if len(args) != 1 {
return nil, fmt.Errorf("attack 需要1个参数")
}
return &AttackExpression{target: args[0]}, nil
}
func (a *AttackExpression) Interpret(ctx *GameContext) string {
return fmt.Sprintf("%s 对 %s 发起攻击!", ctx.HeroName, a.target)
}
// ===== 终结符表达式:状态查询 "status" =====
type StatusExpression struct{}
func (s *StatusExpression) Interpret(ctx *GameContext) string {
return fmt.Sprintf("状态: HP=%d MP=%d 位置=(%d,%d)", ctx.HP, ctx.MP, ctx.X, ctx.Y)
}
// ===== 终结符表达式:治疗指令 "heal amount" =====
type HealExpression struct {
amount int
}
func NewHealExpression(args []string) (*HealExpression, error) {
if len(args) != 1 {
return nil, fmt.Errorf("heal 需要1个参数")
}
amount, _ := strconv.Atoi(args[0])
return &HealExpression{amount: amount}, nil
}
func (h *HealExpression) Interpret(ctx *GameContext) string {
ctx.HP += h.amount
return fmt.Sprintf("治疗 %d 点生命值,当前 HP=%d", h.amount, ctx.HP)
}
// ===== 非终结符表达式:复合指令(顺序执行多个指令)=====
type CompositeExpression struct {
expressions []Expression
}
func (c *CompositeExpression) Interpret(ctx *GameContext) string {
var results []string
for _, expr := range c.expressions {
results = append(results, expr.Interpret(ctx))
}
return strings.Join(results, "\n")
}
// ===== 解析器:将字符串解析为表达式 =====
type CommandParser struct{}
func (p *CommandParser) Parse(input string) (Expression, error) {
parts := strings.Fields(input)
if len(parts) == 0 {
return nil, fmt.Errorf("空指令")
}
cmd := parts[0]
args := parts[1:]
switch cmd {
case "move":
return NewMoveExpression(args)
case "attack":
return NewAttackExpression(args)
case "status":
return &StatusExpression{}, nil
case "heal":
return NewHealExpression(args)
default:
return nil, fmt.Errorf("未知指令: %s", cmd)
}
}
// 解析多行指令(复合)
func (p *CommandParser) ParseMulti(inputs []string) Expression {
composite := &CompositeExpression{}
for _, input := range inputs {
expr, err := p.Parse(input)
if err != nil {
fmt.Printf("解析错误: %v\n", err)
continue
}
composite.expressions = append(composite.expressions, expr)
}
return composite
}
func main() {
ctx := &GameContext{
HeroName: "亚瑟",
HP: 80,
MP: 50,
X: 0,
Y: 0,
}
parser := &CommandParser{}
// 单条指令解析
fmt.Println("=== 单条指令解析 ===")
cmds := []string{
"status",
"move 10 20",
"attack 史莱姆",
"heal 20",
"status",
}
for _, cmd := range cmds {
expr, err := parser.Parse(cmd)
if err != nil {
fmt.Printf("错误: %v\n", err)
continue
}
result := expr.Interpret(ctx)
fmt.Printf("> %s\n%s\n\n", cmd, result)
}
// 复合指令解析
fmt.Println("=== 复合指令(脚本)===")
script := []string{
"move 5 5",
"attack Boss",
"heal 10",
"status",
}
composite := parser.ParseMulti(script)
fmt.Println(composite.Interpret(ctx))
}
运行结果
=== 单条指令解析 ===
> status
状态: HP=80 MP=50 位置=(0,0)
> move 10 20
移动至坐标 (10, 20)
> attack 史莱姆
亚瑟 对 史莱姆 发起攻击!
> heal 20
治疗 20 点生命值,当前 HP=100
> status
状态: HP=100 MP=50 位置=(10,20)
=== 复合指令(脚本)===
移动至坐标 (5, 5)
亚瑟 对 Boss 发起攻击!
治疗 10 点生命值,当前 HP=100
状态: HP=100 MP=50 位置=(5,5)
关键点
-
每个指令是一个表达式,实现
Interpret方法 -
CompositeExpression可以组合多个表达式,形成脚本 -
适合简单的领域特定语言(DSL),复杂语法建议用专业解析器(如 yacc)
4. 迭代器模式(Iterator Pattern)
是什么?
提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
为什么需要?
你的英雄有一个背包,里面装着各种物品。你想遍历背包,但:
-
背包内部可能是数组、链表、map,甚至数据库
-
你不想暴露背包的内部结构
-
你可能需要正序遍历 、倒序遍历 、按品质过滤遍历
迭代器模式封装遍历逻辑,客户端只需说"给我下一个"。
适用场景
-
需要遍历集合,但不想暴露内部结构
-
需要多种遍历方式
-
为不同集合提供统一的遍历接口
-
例如:数据库结果集遍历、树形结构遍历
代码案例:英雄背包迭代器
package main
import (
"fmt"
)
// ===== 物品 =====
type Item struct {
Name string
Quality int // 1=普通, 2=稀有, 3=传说
Quantity int
}
// ===== 迭代器接口 =====
type Iterator interface {
HasNext() bool
Next() *Item
Reset()
}
// ===== 聚合接口 =====
type Container interface {
GetIterator() Iterator
}
// ===== 背包(具体聚合)=====
type Backpack struct {
items []*Item
}
func NewBackpack() *Backpack {
return &Backpack{items: []*Item{}}
}
func (b *Backpack) AddItem(item *Item) {
b.items = append(b.items, item)
}
// ===== 正向迭代器 =====
type ForwardIterator struct {
backpack *Backpack
index int
}
func (f *ForwardIterator) HasNext() bool {
return f.index < len(f.backpack.items)
}
func (f *ForwardIterator) Next() *Item {
if f.HasNext() {
item := f.backpack.items[f.index]
f.index++
return item
}
return nil
}
func (f *ForwardIterator) Reset() {
f.index = 0
}
// ===== 反向迭代器 =====
type ReverseIterator struct {
backpack *Backpack
index int
}
func NewReverseIterator(b *Backpack) *ReverseIterator {
return &ReverseIterator{
backpack: b,
index: len(b.items) - 1,
}
}
func (r *ReverseIterator) HasNext() bool {
return r.index >= 0
}
func (r *ReverseIterator) Next() *Item {
if r.HasNext() {
item := r.backpack.items[r.index]
r.index--
return item
}
return nil
}
func (r *ReverseIterator) Reset() {
r.index = len(r.backpack.items) - 1
}
// ===== 品质过滤迭代器 =====
type QualityFilterIterator struct {
backpack *Backpack
index int
minQuality int
}
func NewQualityFilterIterator(b *Backpack, minQ int) *QualityFilterIterator {
return &QualityFilterIterator{
backpack: b,
index: 0,
minQuality: minQ,
}
}
func (q *QualityFilterIterator) HasNext() bool {
// 跳过不符合条件的
for q.index < len(q.backpack.items) {
if q.backpack.items[q.index].Quality >= q.minQuality {
return true
}
q.index++
}
return false
}
func (q *QualityFilterIterator) Next() *Item {
if q.HasNext() {
item := q.backpack.items[q.index]
q.index++
return item
}
return nil
}
func (q *QualityFilterIterator) Reset() {
q.index = 0
}
// 让 Backpack 实现 Container 接口
func (b *Backpack) GetIterator() Iterator {
return &ForwardIterator{backpack: b, index: 0}
}
func main() {
// 初始化背包
backpack := NewBackpack()
backpack.AddItem(&Item{Name: "木剑", Quality: 1, Quantity: 1})
backpack.AddItem(&Item{Name: "生命药水", Quality: 1, Quantity: 5})
backpack.AddItem(&Item{Name: "屠龙刀", Quality: 3, Quantity: 1})
backpack.AddItem(&Item{Name: "魔法披风", Quality: 2, Quantity: 1})
backpack.AddItem(&Item{Name: "铁盾", Quality: 1, Quantity: 1})
// 1. 正向遍历
fmt.Println("=== 正向遍历背包 ===")
it := backpack.GetIterator()
for it.HasNext() {
item := it.Next()
fmt.Printf(" %s (品质:%d) x%d\n", item.Name, item.Quality, item.Quantity)
}
// 2. 反向遍历
fmt.Println("\n=== 反向遍历背包 ===")
rit := NewReverseIterator(backpack)
for rit.HasNext() {
item := rit.Next()
fmt.Printf(" %s (品质:%d) x%d\n", item.Name, item.Quality, item.Quantity)
}
// 3. 只遍历稀有及以上物品
fmt.Println("\n=== 只查看稀有/传说物品 ===")
qit := NewQualityFilterIterator(backpack, 2)
for qit.HasNext() {
item := qit.Next()
fmt.Printf(" ✨ %s (品质:%d) x%d\n", item.Name, item.Quality, item.Quantity)
}
}
运行结果
=== 正向遍历背包 ===
木剑 (品质:1) x1
生命药水 (品质:1) x5
屠龙刀 (品质:3) x1
魔法披风 (品质:2) x1
铁盾 (品质:1) x1
=== 反向遍历背包 ===
铁盾 (品质:1) x1
魔法披风 (品质:2) x1
屠龙刀 (品质:3) x1
生命药水 (品质:1) x5
木剑 (品质:1) x1
=== 只查看稀有/传说物品 ===
✨ 屠龙刀 (品质:3) x1
✨ 魔法披风 (品质:2) x1
关键点
-
Go 语言有
range,但迭代器模式在需要多种遍历策略 或复杂过滤时更有优势 -
迭代器封装了遍历状态(
index),多个迭代器可以独立遍历同一个背包 -
新增遍历方式只需新增迭代器,不改背包代码
5. 中介者模式(Mediator Pattern)
是什么?
定义一个对象来封装一组对象的交互方式。中介者使各对象不需要显式地相互引用,从而使其耦合松散。
为什么需要?
你的游戏里有 4 个英雄组队打副本。如果每个英雄都要直接和其他 3 个英雄通信:
-
亚瑟要和甘道夫、Legolas、赵云通信
-
甘道夫要和亚瑟、Legolas、赵云通信
-
... 共 6 条通信线
如果再加一个英雄,要新增 4 条通信线!这叫做网状依赖。
中介者模式引入一个"队长"(中介者),所有英雄只和队长通信,由队长转发消息。
适用场景
-
一组对象以复杂方式通信,产生网状依赖
-
想定制一个分布在多个类中的行为,又不想生成太多子类
-
例如:聊天室、组队系统、MVC 控制器、航空管制系统
代码案例:英雄组队通信系统
package main
import (
"fmt"
)
// ===== 中介者接口 =====
type PartyMediator interface {
SendMessage(sender *Hero, msg string)
AddMember(hero *Hero)
}
// ===== 同事接口 =====
type Colleague interface {
SetMediator(mediator PartyMediator)
ReceiveMessage(from string, msg string)
GetName() string
}
// ===== 具体中介者:队伍频道 =====
type PartyChat struct {
members map[string]*Hero
}
func NewPartyChat() *PartyMediator {
p := &PartyChat{members: make(map[string]*Hero)}
var pm PartyMediator = p
return &pm
}
func (p *PartyChat) AddMember(hero *Hero) {
p.members[hero.Name] = hero
hero.SetMediator(p)
fmt.Printf("系统: %s 加入了队伍\n", hero.Name)
}
func (p *PartyChat) SendMessage(sender *Hero, msg string) {
fmt.Printf("\n📢 [%s] 发送: %s\n", sender.Name, msg)
for name, member := range p.members {
if name != sender.Name {
member.ReceiveMessage(sender.Name, msg)
}
}
}
// ===== 具体同事:英雄 =====
type Hero struct {
Name string
mediator PartyMediator
}
func NewHero(name string) *Hero {
return &Hero{Name: name}
}
func (h *Hero) SetMediator(mediator PartyMediator) {
h.mediator = mediator
}
func (h *Hero) Send(msg string) {
if h.mediator != nil {
h.mediator.SendMessage(h, msg)
}
}
func (h *Hero) ReceiveMessage(from string, msg string) {
fmt.Printf(" <%s> 收到来自 %s 的消息: %s\n", h.Name, from, msg)
}
func (h *Hero) GetName() string {
return h.Name
}
// ===== 扩展:私密频道(只有队长能看到)=====
type LeaderOnlyChat struct {
leader *Hero
members map[string]*Hero
}
func NewLeaderOnlyChat(leader *Hero) *PartyMediator {
l := &LeaderOnlyChat{
leader: leader,
members: make(map[string]*Hero),
}
var pm PartyMediator = l
return &pm
}
func (l *LeaderOnlyChat) AddMember(hero *Hero) {
l.members[hero.Name] = hero
hero.SetMediator(l)
fmt.Printf("系统: %s 加入了队长频道\n", hero.Name)
}
func (l *LeaderOnlyChat) SendMessage(sender *Hero, msg string) {
// 只有队长能发消息,且只有队长能看到
if sender.Name == l.leader.Name {
fmt.Printf("\n👑 [队长频道] %s: %s\n", sender.Name, msg)
l.leader.ReceiveMessage("系统", msg)
} else {
fmt.Printf("\n❌ %s 没有权限在队长频道发言\n", sender.Name)
}
}
func main() {
// 创建英雄
亚瑟 := NewHero("亚瑟")
甘道夫 := NewHero("甘道夫")
legolas := NewHero("Legolas")
赵云 := NewHero("赵云")
// 创建普通队伍频道(中介者)
fmt.Println("=== 创建普通队伍 ===")
// 注意:这里需要解引用
party := *NewPartyChat()
party.AddMember(亚瑟)
party.AddMember(甘道夫)
party.AddMember(legolas)
party.AddMember(赵云)
// 亚瑟发送队伍消息
亚瑟.Send("准备打Boss,大家集合!")
// 甘道夫回复
甘道夫.Send("收到,马上到!")
// 创建队长频道(只有队长能用)
fmt.Println("\n=== 创建队长私密频道 ===")
leaderChat := *NewLeaderOnlyChat(亚瑟)
leaderChat.AddMember(亚瑟)
leaderChat.AddMember(甘道夫) // 甘道夫加入但只能看不能发
亚瑟.Send("队长私密:等会儿我抗伤害,你们输出")
甘道夫.Send("我能发言吗?")
}
运行结果
=== 创建普通队伍 ===
系统: 亚瑟 加入了队伍
系统: 甘道夫 加入了队伍
系统: Legolas 加入了队伍
系统: 赵云 加入了队伍
📢 [亚瑟] 发送: 准备打Boss,大家集合!
<甘道夫> 收到来自 亚瑟 的消息: 准备打Boss,大家集合!
<Legolas> 收到来自 亚瑟 的消息: 准备打Boss,大家集合!
<赵云> 收到来自 亚瑟 的消息: 准备打Boss,大家集合!
📢 [甘道夫] 发送: 收到,马上到!
<亚瑟> 收到来自 甘道夫 的消息: 收到,马上到!
<Legolas> 收到来自 甘道夫 的消息: 收到,马上到!
<赵云> 收到来自 甘道夫 的消息: 收到,马上到!
=== 创建队长私密频道 ===
系统: 亚瑟 加入了队长频道
系统: 甘道夫 加入了队长频道
👑 [队长频道] 亚瑟: 队长私密:等会儿我抗伤害,你们输出
<亚瑟> 收到来自 系统的消息: 队长私密:等会儿我抗伤害,你们输出
❌ 甘道夫 没有权限在队长频道发言
关键点
-
没有中介者:4 个英雄需要 6 条通信线(n*(n-1)/2)
-
有中介者:4 个英雄只需要 4 条线(每个只连中介者)
-
新增英雄只需连到中介者,不影响其他英雄
6. 备忘录模式(Memento Pattern)
是什么?
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后恢复。
为什么需要?
玩家打到第 50 关,想存档 。之后如果死了,可以读档回到第 50 关的状态。
备忘录模式让英雄自己生成一个"存档快照"(备忘录),交给存档管理器保管。英雄的内部细节(如私有字段)不会暴露。
适用场景
-
需要保存和恢复对象状态
-
需要实现撤销功能
-
不想暴露对象内部细节
-
例如:游戏存档、编辑器撤销、事务回滚
代码案例:游戏存档系统
package main
import (
"fmt"
"time"
)
// ===== 备忘录:存档数据(只允许 Originator 访问内部)=====
type HeroMemento struct {
hp, mp, x, y int
level int
timestamp string
}
func (m *HeroMemento) GetInfo() string {
return fmt.Sprintf("存档[%s] Lv.%d HP:%d MP:%d 位置(%d,%d)",
m.timestamp, m.level, m.hp, m.mp, m.x, m.y)
}
// ===== 发起者:英雄(可以创建和恢复备忘录)=====
type Hero struct {
Name string
HP int
MP int
X, Y int
Level int
}
func NewHero(name string) *Hero {
return &Hero{
Name: name,
HP: 100,
MP: 50,
X: 0,
Y: 0,
Level: 1,
}
}
func (h *Hero) Save() *HeroMemento {
return &HeroMemento{
hp: h.HP,
mp: h.MP,
x: h.X,
y: h.Y,
level: h.Level,
timestamp: time.Now().Format("15:04:05"),
}
}
func (h *Hero) Restore(m *HeroMemento) {
h.HP = m.hp
h.MP = m.mp
h.X = m.x
h.Y = m.y
h.Level = m.level
fmt.Printf("🔄 %s 已恢复到存档状态: %s\n", h.Name, m.GetInfo())
}
func (h *Hero) Status() string {
return fmt.Sprintf("%s Lv.%d HP:%d MP:%d 位置(%d,%d)",
h.Name, h.Level, h.HP, h.MP, h.X, h.Y)
}
func (h *Hero) Fight() {
fmt.Printf("⚔️ %s 经历了一场恶战...\n", h.Name)
h.HP -= 50
h.MP -= 20
h.X += 10
h.Level += 1
}
func (h *Hero) Die() {
fmt.Printf("💀 %s 阵亡了!\n", h.Name)
h.HP = 0
}
// ===== 管理者:存档管理器(只负责存储,不访问备忘录内部)=====
type SaveManager struct {
saves map[string]*HeroMemento
}
func NewSaveManager() *SaveManager {
return &SaveManager{saves: make(map[string]*HeroMemento)}
}
func (s *SaveManager) SaveSlot(slot string, m *HeroMemento) {
s.saves[slot] = m
fmt.Printf("💾 已保存到存档槽 [%s]: %s\n", slot, m.GetInfo())
}
func (s *SaveManager) LoadSlot(slot string) *HeroMemento {
if m, ok := s.saves[slot]; ok {
fmt.Printf("📂 读取存档槽 [%s]: %s\n", slot, m.GetInfo())
return m
}
fmt.Printf("❌ 存档槽 [%s] 为空\n", slot)
return nil
}
func main() {
hero := NewHero("亚瑟")
manager := NewSaveManager()
// 初始状态
fmt.Println("初始状态:", hero.Status())
// 保存初始存档
manager.SaveSlot("auto", hero.Save())
// 推进游戏
fmt.Println("\n=== 游戏进行中 ===")
hero.Fight()
fmt.Println("战后状态:", hero.Status())
manager.SaveSlot("manual", hero.Save())
// 再打一场
hero.Fight()
fmt.Println("再战后状态:", hero.Status())
// 不幸阵亡
hero.Die()
fmt.Println("阵亡状态:", hero.Status())
// 读档回到手动存档
fmt.Println("\n=== 读档恢复 ===")
save := manager.LoadSlot("manual")
if save != nil {
hero.Restore(save)
}
fmt.Println("恢复后状态:", hero.Status())
// 再读档回到初始存档
fmt.Println("\n=== 再次读档 ===")
save2 := manager.LoadSlot("auto")
if save2 != nil {
hero.Restore(save2)
}
fmt.Println("恢复后状态:", hero.Status())
}
运行结果
初始状态: 亚瑟 Lv.1 HP:100 MP:50 位置(0,0)
💾 已保存到存档槽 [auto]: 存档[20:46:30] Lv.1 HP:100 MP:50 位置(0,0)
=== 游戏进行中 ===
⚔️ 亚瑟 经历了一场恶战...
战后状态: 亚瑟 Lv.2 HP:50 MP:30 位置(10,0)
💾 已保存到存档槽 [manual]: 存档[20:46:30] Lv.2 HP:50 MP:30 位置(10,0)
⚔️ 亚瑟 经历了一场恶战...
再战后状态: 亚瑟 Lv.3 HP:0 MP:10 位置(20,0)
💀 亚瑟 阵亡了!
阵亡状态: 亚瑟 Lv.3 HP:0 MP:10 位置(20,0)
=== 读档恢复 ===
📂 读取存档槽 [manual]: 存档[20:46:30] Lv.2 HP:50 MP:30 位置(10,0)
🔄 亚瑟 已恢复到存档状态: 存档[20:46:30] Lv.2 HP:50 MP:30 位置(10,0)
恢复后状态: 亚瑟 Lv.2 HP:50 MP:30 位置(10,0)
=== 再次读档 ===
📂 读取存档槽 [auto]: 存档[20:46:30] Lv.1 HP:100 MP:50 位置(0,0)
🔄 亚瑟 已恢复到存档状态: 存档[20:46:30] Lv.1 HP:100 MP:50 位置(0,0)
恢复后状态: 亚瑟 Lv.1 HP:100 MP:50 位置(0,0)
关键点
-
封装性 :
SaveManager只存HeroMemento,看不到Hero的内部字段 -
单一职责 :
Hero负责创建/恢复备忘录,SaveManager负责存储 -
备忘录可以存到文件、数据库,实现真正的游戏存档
7. 观察者模式(Observer Pattern)
是什么?
定义对象间的一对多依赖,当一个对象状态改变时,所有依赖它的对象都会收到通知并自动更新。
为什么需要?
你的英雄升级了,需要通知:
-
UI 系统:显示升级特效
-
成就系统:检查是否触发"首次升级"成就
-
队友系统:同步等级信息
-
任务系统:检查是否有"升到10级"的任务
如果英雄直接调用这 4 个系统,耦合太紧。观察者模式让英雄广播一个事件,谁关心谁订阅。
适用场景
-
一个对象的状态变化需要通知多个其他对象
-
需要事件订阅/发布机制
-
解耦事件发送者和接收者
-
例如:消息队列、事件总线、React/Vue 的响应式系统
代码案例:英雄升级事件系统
package main
import (
"fmt"
)
// ===== 观察者接口 =====
type Observer interface {
Update(heroName string, event string, data map[string]interface{})
GetName() string
}
// ===== 主题接口 =====
type Subject interface {
Attach(observer Observer)
Detach(observer Observer)
Notify(event string, data map[string]interface{})
}
// ===== 具体主题:英雄 =====
type GameHero struct {
Name string
Level int
HP int
observers []Observer
}
func NewGameHero(name string) *GameHero {
return &GameHero{
Name: name,
Level: 1,
HP: 100,
observers: []Observer{},
}
}
func (h *GameHero) Attach(o Observer) {
h.observers = append(h.observers, o)
fmt.Printf("📌 %s 订阅了 %s 的事件\n", o.GetName(), h.Name)
}
func (h *GameHero) Detach(o Observer) {
// 简化:实际应查找并删除
}
func (h *GameHero) Notify(event string, data map[string]interface{}) {
fmt.Printf("\n🔔 %s 触发事件: %s\n", h.Name, event)
for _, o := range h.observers {
o.Update(h.Name, event, data)
}
}
func (h *GameHero) LevelUp() {
h.Level++
h.HP = 100 // 升级回满血
fmt.Printf("\n⬆️ %s 升到了 %d 级!\n", h.Name, h.Level)
h.Notify("level_up", map[string]interface{}{
"level": h.Level,
"hp": h.HP,
})
}
func (h *GameHero) TakeDamage(damage int) {
h.HP -= damage
if h.HP <= 0 {
h.HP = 0
h.Notify("die", map[string]interface{}{"hp": 0})
} else {
h.Notify("damaged", map[string]interface{}{"hp": h.HP})
}
}
// ===== 具体观察者1:UI系统 =====
type UISystem struct{}
func (u *UISystem) GetName() string { return "UI系统" }
func (u *UISystem) Update(heroName, event string, data map[string]interface{}) {
switch event {
case "level_up":
fmt.Printf(" 🎆 [%s] 播放升级特效!当前等级: %d\n", u.GetName(), data["level"])
case "die":
fmt.Printf(" 💀 [%s] 屏幕变灰,显示复活按钮\n", u.GetName())
}
}
// ===== 具体观察者2:成就系统 =====
type AchievementSystem struct{}
func (a *AchievementSystem) GetName() string { return "成就系统" }
func (a *AchievementSystem) Update(heroName, event string, data map[string]interface{}) {
if event == "level_up" && data["level"] == 10 {
fmt.Printf(" 🏆 [%s] 解锁成就:初出茅庐!\n", a.GetName())
}
if event == "die" {
fmt.Printf(" 💔 [%s] 记录首次阵亡\n", a.GetName())
}
}
// ===== 具体观察者3:任务系统 =====
type QuestSystem struct{}
func (q *QuestSystem) GetName() string { return "任务系统" }
func (q *QuestSystem) Update(heroName, event string, data map[string]interface{}) {
if event == "level_up" {
fmt.Printf(" 📜 [%s] 检查任务进度:升到10级 (%d/10)\n", q.GetName(), data["level"])
}
}
// ===== 具体观察者4:队友系统 =====
type TeammateSystem struct {
Teammates []string
}
func (t *TeammateSystem) GetName() string { return "队友系统" }
func (t *TeammateSystem) Update(heroName, event string, data map[string]interface{}) {
if event == "level_up" {
for _, mate := range t.Teammates {
fmt.Printf(" 👥 [%s] 通知队友 %s: %s 已升级!\n", t.GetName(), mate, heroName)
}
}
}
func main() {
hero := NewGameHero("亚瑟")
// 创建观察者
ui := &UISystem{}
achieve := &AchievementSystem{}
quest := &QuestSystem{}
team := &TeammateSystem{Teammates: []string{"甘道夫", "Legolas"}}
// 订阅事件
hero.Attach(ui)
hero.Attach(achieve)
hero.Attach(quest)
hero.Attach(team)
// 英雄升级
hero.LevelUp()
// 再升几级
for i := 0; i < 9; i++ {
hero.LevelUp()
}
// 英雄阵亡
fmt.Println("\n=== 遭遇强敌 ===")
hero.TakeDamage(200)
}
运行结果
📌 UI系统 订阅了 亚瑟 的事件
📌 成就系统 订阅了 亚瑟 的事件
📌 任务系统 订阅了 亚瑟 的事件
📌 队友系统 订阅了 亚瑟 的事件
⬆️ 亚瑟 升到了 2 级!
🔔 亚瑟 触发事件: level_up
🎆 [UI系统] 播放升级特效!当前等级: 2
📜 [任务系统] 检查任务进度:升到10级 (2/10)
👥 [队友系统] 通知队友 甘道夫: 亚瑟 已升级!
👥 [队友系统] 通知队友 Legolas: 亚瑟 已升级!
⬆️ 亚瑟 升到了 3 级!
...
⬆️ 亚瑟 升到了 10 级!
🔔 亚瑟 触发事件: level_up
🎆 [UI系统] 播放升级特效!当前等级: 10
🏆 [成就系统] 解锁成就:初出茅庐!
📜 [任务系统] 检查任务进度:升到10级 (10/10)
👥 [队友系统] 通知队友 甘道夫: 亚瑟 已升级!
👥 [队友系统] 通知队友 Legolas: 亚瑟 已升级!
=== 遭遇强敌 ===
🔔 亚瑟 触发事件: die
💀 [UI系统] 屏幕变灰,显示复活按钮
💔 [成就系统] 记录首次阵亡
关键点
-
英雄(主题)不知道有哪些观察者,只管发通知
-
观察者可以动态订阅/取消订阅
-
这是现代框架(React、Vue、Spring Event)的核心机制
8. 状态模式(State Pattern)
是什么?
允许对象在内部状态改变时改变它的行为。对象看起来好像修改了它的类。
为什么需要?
你的英雄在不同状态下行为完全不同:
-
正常状态:可以攻击、移动、使用技能
-
眩晕状态:不能攻击,不能移动
-
死亡状态:什么都不能做,只能等待复活
-
隐身状态:可以移动,但攻击会显形
如果用一个巨大的 switch 来判断状态,代码会非常臃肿。状态模式把每个状态封装成一个类,英雄把行为委托给当前状态对象。
适用场景
-
对象行为依赖于状态,且状态经常变化
-
需要避免大量的条件判断
-
例如:订单状态机、游戏角色状态、TCP 连接状态
代码案例:英雄状态机
package main
import (
"fmt"
)
// ===== 状态接口 =====
type HeroState interface {
Enter()
Exit()
Attack(hero *Hero) string
Move(hero *Hero, x, y int) string
UseSkill(hero *Hero) string
}
// ===== 具体状态1:正常状态 =====
type NormalState struct{}
func (n *NormalState) Enter() {
fmt.Println(" [状态] 进入正常状态")
}
func (n *NormalState) Exit() {
fmt.Println(" [状态] 退出正常状态")
}
func (n *NormalState) Attack(hero *Hero) string {
return fmt.Sprintf("%s 发起攻击!", hero.Name)
}
func (n *NormalState) Move(hero *Hero, x, y int) string {
hero.X, hero.Y = x, y
return fmt.Sprintf("%s 移动到 (%d,%d)", hero.Name, x, y)
}
func (n *NormalState) UseSkill(hero *Hero) string {
return fmt.Sprintf("%s 使用技能!", hero.Name)
}
// ===== 具体状态2:眩晕状态 =====
type StunnedState struct{}
func (s *StunnedState) Enter() {
fmt.Println(" [状态] 进入眩晕状态(无法行动)")
}
func (s *StunnedState) Exit() {
fmt.Println(" [状态] 眩晕解除")
}
func (s *StunnedState) Attack(hero *Hero) string {
return fmt.Sprintf("%s 处于眩晕状态,无法攻击!", hero.Name)
}
func (s *StunnedState) Move(hero *Hero, x, y int) string {
return fmt.Sprintf("%s 处于眩晕状态,无法移动!", hero.Name)
}
func (s *StunnedState) UseSkill(hero *Hero) string {
return fmt.Sprintf("%s 处于眩晕状态,无法使用技能!", hero.Name)
}
// ===== 具体状态3:死亡状态 =====
type DeadState struct{}
func (d *DeadState) Enter() {
fmt.Println(" [状态] 英雄阵亡")
}
func (d *DeadState) Exit() {
fmt.Println(" [状态] 复活!")
}
func (d *DeadState) Attack(hero *Hero) string {
return fmt.Sprintf("%s 已阵亡,无法攻击(请先复活)", hero.Name)
}
func (d *DeadState) Move(hero *Hero, x, y int) string {
return fmt.Sprintf("%s 已阵亡,无法移动", hero.Name)
}
func (d *DeadState) UseSkill(hero *Hero) string {
return fmt.Sprintf("%s 已阵亡,无法使用技能", hero.Name)
}
// ===== 具体状态4:隐身状态 =====
type StealthState struct{}
func (s *StealthState) Enter() {
fmt.Println(" [状态] 进入隐身状态")
}
func (s *StealthState) Exit() {
fmt.Println(" [状态] 隐身解除")
}
func (s *StealthState) Attack(hero *Hero) string {
// 攻击会解除隐身
hero.ChangeState(&NormalState{})
return fmt.Sprintf("%s 发起偷袭!隐身解除,进入正常状态", hero.Name)
}
func (s *StealthState) Move(hero *Hero, x, y int) string {
hero.X, hero.Y = x, y
return fmt.Sprintf("%s 悄无声息地移动到 (%d,%d)", hero.Name, x, y)
}
func (s *StealthState) UseSkill(hero *Hero) string {
return fmt.Sprintf("%s 在隐身状态下使用技能(不会显形)", hero.Name)
}
// ===== 英雄(上下文)=====
type Hero struct {
Name string
X, Y int
State HeroState
}
func NewHero(name string) *Hero {
hero := &Hero{Name: name}
hero.ChangeState(&NormalState{}) // 初始正常状态
return hero
}
func (h *Hero) ChangeState(newState HeroState) {
if h.State != nil {
h.State.Exit()
}
h.State = newState
h.State.Enter()
}
func (h *Hero) Attack() string {
return h.State.Attack(h)
}
func (h *Hero) Move(x, y int) string {
return h.State.Move(h, x, y)
}
func (h *Hero) UseSkill() string {
return h.State.UseSkill(h)
}
func main() {
hero := NewHero("亚瑟")
// 正常状态
fmt.Println("=== 正常状态 ===")
fmt.Println(hero.Attack())
fmt.Println(hero.Move(10, 20))
fmt.Println(hero.UseSkill())
// 被眩晕
fmt.Println("\n=== 被眩晕 ===")
hero.ChangeState(&StunnedState{})
fmt.Println(hero.Attack())
fmt.Println(hero.Move(5, 5))
fmt.Println(hero.UseSkill())
// 眩晕解除,进入隐身
fmt.Println("\n=== 隐身 ===")
hero.ChangeState(&StealthState{})
fmt.Println(hero.Move(15, 25))
fmt.Println(hero.UseSkill())
fmt.Println(hero.Attack()) // 攻击会解除隐身
// 阵亡
fmt.Println("\n=== 阵亡 ===")
hero.ChangeState(&DeadState{})
fmt.Println(hero.Attack())
fmt.Println(hero.Move(0, 0))
}
运行结果
[状态] 进入正常状态
=== 正常状态 ===
亚瑟 发起攻击!
亚瑟 移动到 (10,20)
亚瑟 使用技能!
=== 被眩晕 ===
[状态] 退出正常状态
[状态] 进入眩晕状态(无法行动)
亚瑟 处于眩晕状态,无法攻击!
亚瑟 处于眩晕状态,无法移动!
亚瑟 处于眩晕状态,无法使用技能!
=== 隐身 ===
[状态] 眩晕解除
[状态] 进入隐身状态
亚瑟 悄无声息地移动到 (15,25)
亚瑟 在隐身状态下使用技能(不会显形)
[状态] 退出隐身状态
[状态] 进入正常状态
亚瑟 发起偷袭!隐身解除,进入正常状态
=== 阵亡 ===
[状态] 退出正常状态
[状态] 英雄阵亡
亚瑟 已阵亡,无法攻击(请先复活)
亚瑟 已阵亡,无法移动
关键点
-
每个状态是一个独立的类,封装了该状态下的所有行为
-
状态转换由状态自己控制(如隐身状态攻击后自动切到正常)或由外部触发
-
彻底消除了
if state == "stunned" { ... }这样的条件判断
9. 策略模式(Strategy Pattern)
是什么?
定义一系列算法,把它们一个个封装起来,并且使它们可以互相替换。策略模式让算法的变化独立于使用算法的客户。
为什么需要?
你的英雄攻击时,可以选择不同的攻击策略:
-
近战策略:靠近敌人,造成物理伤害
-
远程策略:保持距离,造成远程伤害
-
魔法策略:消耗 MP,造成魔法伤害
如果把这些策略都写在英雄类里,英雄类会变得臃肿。策略模式把每种策略封装成独立的类,英雄在运行时切换策略。
适用场景
-
需要定义一组算法,且它们可以互换
-
避免使用大量的条件语句
-
算法需要独立于客户端变化
-
例如:排序策略、支付策略、压缩算法、游戏 AI
代码案例:英雄攻击策略
package main
import (
"fmt"
)
// ===== 策略接口 =====
type AttackStrategy interface {
Execute(hero *Hero, target string) string
GetName() string
}
// ===== 具体策略1:近战 =====
type MeleeStrategy struct{}
func (m *MeleeStrategy) GetName() string { return "近战" }
func (m *MeleeStrategy) Execute(hero *Hero, target string) string {
return fmt.Sprintf("%s 冲上前去,对 %s 使出【重斩】,造成物理伤害!",
hero.Name, target)
}
// ===== 具体策略2:远程 =====
type RangedStrategy struct{}
func (r *RangedStrategy) GetName() string { return "远程" }
func (r *RangedStrategy) Execute(hero *Hero, target string) string {
return fmt.Sprintf("%s 拉开距离,对 %s 射出【穿云箭】,造成远程伤害!",
hero.Name, target)
}
// ===== 具体策略3:魔法 =====
type MagicStrategy struct{}
func (m *MagicStrategy) GetName() string { return "魔法" }
func (m *MagicStrategy) Execute(hero *Hero, target string) string {
if hero.MP >= 20 {
hero.MP -= 20
return fmt.Sprintf("%s 吟唱咒语,对 %s 释放【火球术】,造成魔法伤害!剩余MP:%d",
hero.Name, target, hero.MP)
}
return fmt.Sprintf("%s MP不足,无法使用魔法攻击!", hero.Name)
}
// ===== 具体策略4:暴击(装饰策略)=====
type CriticalStrategy struct {
baseStrategy AttackStrategy
}
func (c *CriticalStrategy) GetName() string {
return "暴击" + c.baseStrategy.GetName()
}
func (c *CriticalStrategy) Execute(hero *Hero, target string) string {
base := c.baseStrategy.Execute(hero, target)
return base + " 💥触发暴击!伤害翻倍!"
}
// ===== 上下文:英雄 =====
type Hero struct {
Name string
MP int
Strategy AttackStrategy
}
func NewHero(name string) *Hero {
return &Hero{
Name: name,
MP: 100,
Strategy: &MeleeStrategy{}, // 默认近战
}
}
func (h *Hero) SetStrategy(s AttackStrategy) {
h.Strategy = s
fmt.Printf("🔄 %s 切换攻击策略为: %s\n", h.Name, s.GetName())
}
func (h *Hero) Attack(target string) string {
return h.Strategy.Execute(h, target)
}
func main() {
hero := NewHero("亚瑟")
// 默认近战
fmt.Println("=== 默认近战 ===")
fmt.Println(hero.Attack("史莱姆"))
// 切换远程
fmt.Println("\n=== 切换远程 ===")
hero.SetStrategy(&RangedStrategy{})
fmt.Println(hero.Attack("哥布林"))
// 切换魔法
fmt.Println("\n=== 切换魔法 ===")
hero.SetStrategy(&MagicStrategy{})
fmt.Println(hero.Attack("Boss"))
fmt.Println(hero.Attack("Boss"))
fmt.Println(hero.Attack("Boss"))
fmt.Println(hero.Attack("Boss"))
fmt.Println(hero.Attack("Boss")) // MP应该不够了
// 组合策略:暴击 + 近战
fmt.Println("\n=== 组合策略:暴击近战 ===")
hero.MP = 100
hero.SetStrategy(&CriticalStrategy{baseStrategy: &MeleeStrategy{}})
fmt.Println(hero.Attack("巨龙"))
}
运行结果
=== 默认近战 ===
亚瑟 冲上前去,对 史莱姆 使出【重斩】,造成物理伤害!
=== 切换远程 ===
🔄 亚瑟 切换攻击策略为: 远程
亚瑟 拉开距离,对 哥布林 射出【穿云箭】,造成远程伤害!
=== 切换魔法 ===
🔄 亚瑟 切换攻击策略为: 魔法
亚瑟 吟唱咒语,对 Boss 释放【火球术】,造成魔法伤害!剩余MP:80
亚瑟 吟唱咒语,对 Boss 释放【火球术】,造成魔法伤害!剩余MP:60
亚瑟 吟唱咒语,对 Boss 释放【火球术】,造成魔法伤害!剩余MP:40
亚瑟 吟唱咒语,对 Boss 释放【火球术】,造成魔法伤害!剩余MP:20
亚瑟 吟唱咒语,对 Boss 释放【火球术】,造成魔法伤害!剩余MP:0
亚瑟 MP不足,无法使用魔法攻击!
=== 组合策略:暴击近战 ===
🔄 亚瑟 切换攻击策略为: 暴击近战
亚瑟 冲上前去,对 巨龙 使出【重斩】,造成物理伤害! 💥触发暴击!伤害翻倍!
关键点
-
策略模式 vs 状态模式:策略模式是主动选择 算法(玩家选攻击方式),状态模式是被动进入某种状态(被眩晕)
-
策略可以组合 (如暴击 + 近战),也可以运行时切换
10. 模板方法模式(Template Method Pattern)
是什么?
定义一个操作中的算法骨架,而将一些步骤延迟到子类中。模板方法使子类可以不改变算法结构即可重定义该算法的某些步骤。
为什么需要?
游戏中的战斗流程通常是固定的:
-
战斗开始(播报开始)
-
选择目标
-
执行攻击(不同职业实现不同)
-
计算伤害
-
战斗结束(播报结果)
步骤 1、2、4、5 是通用的,但步骤 3(执行攻击)每个职业不同。模板方法模式把通用流程写在父类,把变化的部分留给子类实现。
适用场景
-
算法骨架固定,但某些步骤由子类实现
-
避免代码重复,提取公共流程
-
例如:数据处理流程、游戏战斗回合、HTTP 请求处理中间件
代码案例:战斗回合模板
package main
import (
"fmt"
"math/rand"
"time"
)
// ===== 抽象类:战斗模板 =====
type BattleTemplate interface {
SelectTarget() string
ExecuteAttack(target string) string
}
// ===== 模板方法(基类)=====
type BaseBattle struct {
HeroName string
}
// 模板方法:定义战斗流程骨架(不能重写)
func (b *BaseBattle) BattleRound(template BattleTemplate) string {
result := ""
result += b.onBattleStart()
target := template.SelectTarget()
result += fmt.Sprintf(" 选择目标: %s\n", target)
result += template.ExecuteAttack(target)
result += b.calculateDamage()
result += b.onBattleEnd()
return result
}
// 通用步骤1:战斗开始
func (b *BaseBattle) onBattleStart() string {
return fmt.Sprintf("=== %s 的战斗回合开始 ===\n", b.HeroName)
}
// 通用步骤4:计算伤害
func (b *BaseBattle) calculateDamage() string {
damage := rand.Intn(50) + 50
return fmt.Sprintf(" 造成 %d 点伤害\n", damage)
}
// 通用步骤5:战斗结束
func (b *BaseBattle) onBattleEnd() string {
return fmt.Sprintf("=== %s 的战斗回合结束 ===\n", b.HeroName)
}
// ===== 具体子类1:战士 =====
type WarriorBattle struct {
BaseBattle
}
func NewWarriorBattle(name string) *WarriorBattle {
return &WarriorBattle{BaseBattle: BaseBattle{HeroName: name}}
}
func (w *WarriorBattle) SelectTarget() string {
return "最近的敌人"
}
func (w *WarriorBattle) ExecuteAttack(target string) string {
return fmt.Sprintf(" %s 怒吼一声,对 %s 使出【旋风斩】!\n", w.HeroName, target)
}
// ===== 具体子类2:法师 =====
type MageBattle struct {
BaseBattle
}
func NewMageBattle(name string) *MageBattle {
return &MageBattle{BaseBattle: BaseBattle{HeroName: name}}
}
func (m *MageBattle) SelectTarget() string {
return "血量最多的敌人"
}
func (m *MageBattle) ExecuteAttack(target string) string {
return fmt.Sprintf(" %s 念动咒语,对 %s 释放【陨石术】!\n", m.HeroName, target)
}
// ===== 具体子类3:弓箭手 =====
type ArcherBattle struct {
BaseBattle
}
func NewArcherBattle(name string) *ArcherBattle {
return &ArcherBattle{BaseBattle: BaseBattle{HeroName: name}}
}
func (a *ArcherBattle) SelectTarget() string {
return "距离最远的敌人"
}
func (a *ArcherBattle) ExecuteAttack(target string) string {
return fmt.Sprintf(" %s 拉满弓弦,对 %s 射出【追踪箭】!\n", a.HeroName, target)
}
func main() {
rand.Seed(time.Now().UnixNano())
// 战士战斗
warrior := NewWarriorBattle("亚瑟")
fmt.Println(warrior.BattleRound(warrior))
// 法师战斗
mage := NewMageBattle("甘道夫")
fmt.Println(mage.BattleRound(mage))
// 弓箭手战斗
archer := NewArcherBattle("Legolas")
fmt.Println(archer.BattleRound(archer))
}
运行结果
=== 亚瑟 的战斗回合开始 ===
选择目标: 最近的敌人
亚瑟 怒吼一声,对 最近的敌人 使出【旋风斩】!
造成 87 点伤害
=== 亚瑟 的战斗回合结束 ===
=== 甘道夫 的战斗回合开始 ===
选择目标: 血量最多的敌人
甘道夫 念动咒语,对 血量最多的敌人 释放【陨石术】!
造成 62 点伤害
=== 甘道夫 的战斗回合结束 ===
=== Legolas 的战斗回合开始 ===
选择目标: 距离最远的敌人
Legolas 拉满弓弦,对 距离最远的敌人 射出【追踪箭】!
造成 95 点伤害
=== Legolas 的战斗回合结束 ===
关键点
-
BattleRound是模板方法,定义了不可变的流程骨架 -
SelectTarget和ExecuteAttack是钩子方法,由子类实现 -
子类不能改变战斗流程,只能改变流程中的具体实现
11. 访问者模式(Visitor Pattern)
是什么?
表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。
为什么需要?
你的游戏中有多种英雄(战士、法师、弓箭手),你想对它们做各种统计/计算:
-
计算总战力
-
计算总金币价值
-
导出数据到 JSON
如果把这些方法写在每个英雄类里,每新增一种统计就要改所有英雄类。访问者模式说:把操作分离出来,让"战力计算器"访问每个英雄,英雄只负责"接受访问"。
适用场景
-
需要对对象结构中的对象做多种不同操作
-
对象结构很少变化,但操作经常变化
-
例如:编译器 AST 遍历、报表系统、游戏中的属性统计
代码案例:英雄属性统计系统
package main
import (
"fmt"
)
// ===== 访问者接口 =====
type HeroVisitor interface {
VisitWarrior(w *Warrior)
VisitMage(m *Mage)
VisitArcher(a *Archer)
}
// ===== 元素接口 =====
type HeroElement interface {
Accept(visitor HeroVisitor)
}
// ===== 具体元素1:战士 =====
type Warrior struct {
Name string
AttackPower int
Defense int
}
func (w *Warrior) Accept(visitor HeroVisitor) {
visitor.VisitWarrior(w)
}
// ===== 具体元素2:法师 =====
type Mage struct {
Name string
MagicPower int
Mana int
}
func (m *Mage) Accept(visitor HeroVisitor) {
visitor.VisitMage(m)
}
// ===== 具体元素3:弓箭手 =====
type Archer struct {
Name string
RangePower int
Agility int
}
func (a *Archer) Accept(visitor HeroVisitor) {
visitor.VisitArcher(a)
}
// ===== 具体访问者1:战力计算器 =====
type PowerCalculator struct {
TotalPower int
}
func (p *PowerCalculator) VisitWarrior(w *Warrior) {
power := w.AttackPower*2 + w.Defense*3
fmt.Printf(" %s(战士) 战力: %d\n", w.Name, power)
p.TotalPower += power
}
func (p *PowerCalculator) VisitMage(m *Mage) {
power := m.MagicPower*3 + m.Mana
fmt.Printf(" %s(法师) 战力: %d\n", m.Name, power)
p.TotalPower += power
}
func (p *PowerCalculator) VisitArcher(a *Archer) {
power := a.RangePower*2 + a.Agility*2
fmt.Printf(" %s(弓箭手) 战力: %d\n", a.Name, power)
p.TotalPower += power
}
// ===== 具体访问者2:JSON 导出器 =====
type JSONExporter struct {
JSON string
}
func (j *JSONExporter) VisitWarrior(w *Warrior) {
j.JSON += fmt.Sprintf(`{"type":"warrior","name":"%s","atk":%d,"def":%d},`,
w.Name, w.AttackPower, w.Defense)
}
func (j *JSONExporter) VisitMage(m *Mage) {
j.JSON += fmt.Sprintf(`{"type":"mage","name":"%s","magic":%d,"mana":%d},`,
m.Name, m.MagicPower, m.Mana)
}
func (j *JSONExporter) VisitArcher(a *Archer) {
j.JSON += fmt.Sprintf(`{"type":"archer","name":"%s","range":%d,"agi":%d},`,
a.Name, a.RangePower, a.Agility)
}
// ===== 对象结构:英雄队伍 =====
type HeroTeam struct {
heroes []HeroElement
}
func (t *HeroTeam) Add(hero HeroElement) {
t.heroes = append(t.heroes, hero)
}
func (t *HeroTeam) Accept(visitor HeroVisitor) {
for _, hero := range t.heroes {
hero.Accept(visitor)
}
}
func main() {
team := &HeroTeam{}
team.Add(&Warrior{Name: "亚瑟", AttackPower: 100, Defense: 80})
team.Add(&Mage{Name: "甘道夫", MagicPower: 120, Mana: 200})
team.Add(&Archer{Name: "Legolas", RangePower: 90, Agility: 100})
team.Add(&Warrior{Name: "赵云", AttackPower: 110, Defense: 70})
// 访问者1:计算总战力
fmt.Println("=== 战力统计 ===")
calc := &PowerCalculator{}
team.Accept(calc)
fmt.Printf("队伍总战力: %d\n", calc.TotalPower)
// 访问者2:导出 JSON
fmt.Println("\n=== JSON 导出 ===")
exporter := &JSONExporter{}
team.Accept(exporter)
fmt.Printf("[%s]\n", exporter.JSON[:len(exporter.JSON)-1]) // 去掉末尾逗号
// 新增访问者3:金币估值(无需修改任何英雄类)
fmt.Println("\n=== 金币估值 ===")
// 这里可以新增一个 GoldEvaluator 访问者,完全不影响原有代码
}
运行结果
=== 战力统计 ===
亚瑟(战士) 战力: 440
甘道夫(法师) 战力: 560
Legolas(弓箭手) 战力: 380
赵云(战士) 战力: 430
队伍总战力: 1810
=== JSON 导出 ===
[{"type":"warrior","name":"亚瑟","atk":100,"def":80},{"type":"mage","name":"甘道夫","magic":120,"mana":200},{"type":"archer","name":"Legolas","range":90,"agi":100},{"type":"warrior","name":"赵云","atk":110,"def":70}]
关键点
-
双分派 :
team.Accept(visitor)→hero.Accept(visitor)→visitor.VisitWarrior(w) -
新增统计方式只需新增访问者,完全不改英雄类
-
代价:如果新增英雄类型(如刺客),所有访问者都要新增方法
行为型模式总结
| 模式 | 核心思想 | 一句话记忆 | 游戏案例 |
|---|---|---|---|
| 责任链 | 请求沿链传递 | 流水线审批 | 指令检查链(状态→冷却→权限→执行) |
| 命令 | 封装请求为对象 | 操作可存档可撤销 | 移动/攻击/治疗命令 + 撤销系统 |
| 解释器 | 定义文法并解释 | 脚本语言解析 | /attack 史莱姆 指令解析 |
| 迭代器 | 顺序访问集合 | 多种遍历方式 | 背包正向/反向/品质过滤遍历 |
| 中介者 | 统一协调通信 | 队长转发消息 | 队伍频道,避免网状依赖 |
| 备忘录 | 保存恢复状态 | 游戏存档读档 | 英雄状态快照 + 存档管理器 |
| 观察者 | 一对多通知 | 广播事件 | 升级通知 UI/成就/任务/队友 |
| 状态 | 状态改变行为 | 不同状态不同反应 | 正常/眩晕/死亡/隐身状态机 |
| 策略 | 算法可互换 | 自由切换打法 | 近战/远程/魔法攻击策略 |
| 模板方法 | 骨架固定,步骤可变 | 固定流程,灵活实现 | 战斗回合:开始→选目标→攻击→结算→结束 |
| 访问者 | 分离操作与结构 | 外来统计员 | 战力计算/JSON导出,不修改英雄类 |
全系列设计模式终极总结
创建型:怎么造对象
| 模式 | 核心 |
|---|---|
| 单例 | 全局唯一 |
| 简单工厂 | 一个函数造多种 |
| 工厂方法 | 每种产品有自己的工厂 |
| 抽象工厂 | 造配套产品族 |
| 建造者 | 一步步组装 |
| 原型 | 克隆复制 |
结构型:怎么组合对象
| 模式 | 核心 |
|---|---|
| 代理 | 替身控制访问 |
| 桥接 | 两个维度独立 |
| 组合 | 树形统一处理 |
| 装饰器 | 动态叠加 |
| 适配器 | 接口转换 |
| 外观 | 简化入口 |
| 享元 | 共享复用 |
行为型:对象怎么交互
| 模式 | 核心 |
|---|---|
| 责任链 | 流水线处理 |
| 命令 | 请求对象化 |
| 解释器 | 文法解析 |
| 迭代器 | 遍历封装 |
| 中介者 | 集中协调 |
| 备忘录 | 状态存档 |
| 观察者 | 事件订阅 |
| 状态 | 状态驱动行为 |
| 策略 | 算法互换 |
| 模板方法 | 流程骨架 |
| 访问者 | 操作分离 |