Swift 一个小型游戏对象模型渐进式设计(二)——协议与默认实现:如何写出不用继承的多态

用 protocol + extension 把上一篇的 BOSS 战例彻底重构,让代码轻量、可测试、易扩展

为什么"不用继承"

上一篇我们用 class Entity → Monster / Boss 的经典继承树完成了需求,但留下几个隐痛:

  1. 值类型无法参与:Swift 的 struct 不能继承 class。
  2. 多继承死路:一个 BOSS 既要"可攻击"又要"可飞行"还要"可分裂",Swift 不支持多类继承。
  3. 隐式共享状态:父类新增的存储属性,所有子类被迫买单,造成"胖基类"。
  4. 单元测试困难:想单独测"狂暴逻辑"必须把整个 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 关键字------因为协议不涉继承链。

拆成"能力插件"

  1. 可攻击
swift 复制代码
protocol Attackable {
    func attack() -> Double
}
extension Attackable {
    func attack() -> Double { 10.0 }
}
  1. 可定位
swift 复制代码
protocol Locatable {
    var x: Double { get set }
    var y: Double { get set }
}
  1. 可受伤
swift 复制代码
protocol Woundable {
    var hp: Double { get set }
    var maxHp: Double { get }
}
extension Woundable {
    var isRage: Bool { hp < maxHp * 0.2 }   // 狂暴判断
}
  1. 可随机伤害
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 可以符合多个协议,享受所有默认实现,零继承。

  1. 普通小怪
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)
    }
}
  1. 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
    }
}
  1. 飞行小怪(新增能力,无需改旧代码)
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 继承

  1. 需要 Objective-C 运行时动态替换(KVO、Swizzle)。
  2. 需要析构器 deinit 做资源清理。
  3. 需要共享引用语义(多个指针指向同一对象)。
  4. 需要互斥锁、原子操作等"引用计数"场景。

其余场景,优先 struct + 协议。

小结:一条决策流程图

相关推荐
HarderCoder3 小时前
Swift 一个小型游戏对象模型渐进式设计(一)——继承机制解读:从基础类到防止重写
swift
HarderCoder4 小时前
Swift 中的迭代机制:Sequence、Collection 与 Iterator 完全拆解
swift
HarderCoder9 小时前
告别并发警告:Swift 6 线程安全通知 MainActorMessage & AsyncMessage 实战指南
swift
HarderCoder9 小时前
【SwiftUI 任务身份】task(id:) 如何正确响应依赖变化
swift
非专业程序员10 小时前
精读GitHub - swift-markdown-ui
ios·swiftui·swift
5***79001 天前
Swift进阶
开发语言·ios·swift
大炮走火1 天前
iOS在制作framework时,oc与swift混编的流程及坑点!
开发语言·ios·swift
0***142 天前
Swift资源
开发语言·ios·swift
z***I3942 天前
Swift Tips
开发语言·ios·swift