用 protocol + extension 把上一篇的 BOSS 战例彻底重构,让代码轻量、可测试、易扩展
为什么"不用继承"
上一篇我们用 class Entity → Monster / Boss 的经典继承树完成了需求,但留下几个隐痛:
- 值类型无法参与:Swift 的 struct 不能继承 class。
- 多继承死路:一个 BOSS 既要"可攻击"又要"可飞行"还要"可分裂",Swift 不支持多类继承。
- 隐式共享状态:父类新增的存储属性,所有子类被迫买单,造成"胖基类"。
- 单元测试困难:想单独测"狂暴逻辑"必须把整个 Boss 实例 new 出来,还要喂血量。
协议(protocol)是什么
一句话:协议只定义"契约",不关心"怎么存"。
swift
protocol Attackable {
func attack() -> Double
}
任何类型(class / struct / enum / actor)只要实现 attack(),就自动"符合" Attackable,从而获得多态能力。
协议本身不能存属性,但可以通过"关联属性"或"协议扩展"给出默认实现,达到"代码复用"而"不强制继承"。
协议扩展:给协议加"默认实现"
语法:
swift
extension Attackable {
func attack() -> Double { 10.0 } // 默认伤害
}
现在任何符合者如果不自己写 attack(),就自动拿到 10 点伤害。
想定制?在自己的类型里重新实现即可,不需要 override 关键字------因为协议不涉继承链。
拆成"能力插件"
- 可攻击
swift
protocol Attackable {
func attack() -> Double
}
extension Attackable {
func attack() -> Double { 10.0 }
}
- 可定位
swift
protocol Locatable {
var x: Double { get set }
var y: Double { get set }
}
- 可受伤
swift
protocol Woundable {
var hp: Double { get set }
var maxHp: Double { get }
}
extension Woundable {
var isRage: Bool { hp < maxHp * 0.2 } // 狂暴判断
}
- 可随机伤害
swift
protocol RandomDamage {
func randomDamage(base: Int, range: Int) -> Double
}
extension RandomDamage {
func randomDamage(base: Int, range: Int) -> Double {
Double.random(in: 0.0..<Double(range)) + Double(base)
}
}
用 struct 组装各种实体
Swift 的 struct 可以符合多个协议,享受所有默认实现,零继承。
- 普通小怪
swift
struct Monster: Attackable, Locatable, Woundable, RandomDamage {
var hp: Double
let maxHp: Double
var x: Double
var y: Double
// 自己定制伤害
func attack() -> Double {
randomDamage(base: 5, range: 6)
}
}
- BOSS
swift
struct Boss: Attackable, Locatable, Woundable {
var hp: Double
let maxHp: Double
var x: Double
var y: Double
// 狂暴机制
func attack() -> Double {
let base: Double = 10
return isRage ? base * 2 : base
}
}
- 飞行小怪(新增能力,无需改旧代码)
swift
protocol Flyable {
var altitude: Double { get set }
}
struct FlyingMonster: Attackable, Locatable, Woundable, Flyable, RandomDamage {
var hp: Double
let maxHp: Double
var x: Double
var y: Double
var altitude: Double
func attack() -> Double {
randomDamage(base: 4, range: 5) + 2 // 空对地加 2
}
}
多态依旧可用:协议作为类型
swift
let army: [any Attackable & Woundable] = [
Monster(hp: 30, maxHp: 30, x: 0, y: 0),
Boss(hp: 100, maxHp: 100, x: 1, y: 1),
FlyingMonster(hp: 20, maxHp: 20, x: 2, y: 2, altitude: 10)
]
for unit in army {
print("伤害=\(unit.attack()), 狂暴=\(unit.isRage)")
}
打印示例:
arduino
伤害=8.857546603881572, 狂暴=false
伤害=10.0, 狂暴=false
伤害=9.333377580674401, 狂暴=false
把 Boss 的血量打到 19 再跑一次,就能看到伤害翻倍,逻辑与继承版完全一致。
单元测试变得多简单?
想测"狂暴判断"只要 new 一个符合 Woundable 的伪对象即可,完全不用构造整个 Boss:
swift
struct Mock: Woundable {
var hp: Double
let maxHp: Double = 100
}
let mock = Mock(hp: 19)
XCTAssertTrue(mock.isRage)
协议组合(Protocol Composition)的语法糖
swift
typealias GameUnit = Attackable & Woundable & Locatable
func move(_ unit: inout GameUnit, toX x: Double, y: Double) {
unit.x = x
unit.y = y
}
一个类型别名即可把"能力包"当成一个整体使用,比继承树清爽得多。
什么时候仍需要 class 继承
- 需要 Objective-C 运行时动态替换(KVO、Swizzle)。
- 需要析构器 deinit 做资源清理。
- 需要共享引用语义(多个指针指向同一对象)。
- 需要互斥锁、原子操作等"引用计数"场景。
其余场景,优先 struct + 协议。
小结:一条决策流程图
