普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月29日首页

iOS 语音房(拍卖房)开发实践

作者 KangJX
2025年11月29日 17:49

本文基于一个真实的iOS语音房项目案例,详细讲解如何使用状态模式来管理复杂的业务流程,以及如何与权限中心协同工作,因为在拍卖房间中不只有不同的房间阶段变化(状态)还有不同角色拥有不同的权限(权限中心)。

业务场景

拍拍房是一个实时拍卖房间系统,类似于语音房间+拍卖的结合体。用户可以在房间内:

  • 作为房主主持拍卖
  • 作为拍卖人上传物品并介绍
  • 作为竞拍者出价竞拍
  • 作为观众观看拍卖过程

核心业务流程

一个完整的拍卖流程需要经历4个明确的阶段:

准备阶段 → 上拍 → 拍卖中 → 定拍 → (循环)准备阶段

每个阶段都有:

  • 不同的允许操作(如只能在准备阶段上传物品)
  • 不同的状态转换规则(如只能从拍卖中进入定拍)
  • 不同的业务逻辑(如只有拍卖中才能出价)

技术挑战

  1. 状态多:4个主要状态,每个状态行为差异大
  2. 转换复杂:状态之间的转换有严格的规则
  3. 权限交织:每个操作还需要考虑用户角色权限
  4. 易扩展性:未来可能增加新的拍卖模式

为什么选择状态模式

❌ 不使用状态模式的问题

如果使用传统的 if-elseswitch-case 来处理:

// 反例:所有逻辑堆砌在一起
func placeBid(amount: Decimal) {
    if currentState == .preparing {
        print("拍卖还未开始")
        return
    } else if currentState == .listing {
        print("拍卖还未正式开始")
        return
    } else if currentState == .auctioning {
        // 执行出价逻辑
        if user.role == .viewer {
            print("观众不能出价")
            return
        }
        if user.id == auctioneer.id {
            print("拍卖人不能给自己出价")
            return
        }
        if amount < currentPrice + incrementStep {
            print("出价金额不足")
            return
        }
        // 终于可以出价了...
    } else if currentState == .closed {
        print("拍卖已结束")
        return
    }
}

问题显而易见

  1. 🔴 代码臃肿:所有状态的逻辑混在一起
  2. 🔴 难以维护:修改一个状态可能影响其他状态
  3. 🔴 不易扩展:增加新状态需要修改多处代码
  4. 🔴 权限混乱:业务逻辑和权限判断纠缠在一起
  5. 🔴 测试困难:无法单独测试某个状态的逻辑

✅ 使用状态模式的优势

// 状态模式:每个状态独立处理
class AuctioningState: RoomStateProtocol {
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        // 只关注拍卖中状态的出价逻辑
        let bid = Bid(...)
        room.addBid(bid)
        return true
    }
}

class PreparingState: RoomStateProtocol {
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        // 准备阶段直接拒绝
        print("拍卖还未开始")
        return false
    }
}

优势明显

  1. 职责单一:每个状态类只关注自己的逻辑
  2. 易于维护:修改某个状态不影响其他状态
  3. 开闭原则:新增状态只需添加新类,不修改现有代码
  4. 清晰直观:状态转换一目了然
  5. 便于测试:可以单独测试每个状态

状态模式设计

整体架构

┌─────────────────────────────────────────┐
│           Room(房间上下文)             │
│  - currentState: RoomStateProtocol      │
│  - changeState(to: RoomState)           │
└──────────────┬──────────────────────────┘
               │ 持有
               ↓
┌─────────────────────────────────────────┐
│      RoomStateProtocol(状态协议)       │
│  + startAuction(room: Room) -> Bool     │
│  + placeBid(room: Room, ...) -> Bool    │
│  + endAuction(room: Room) -> Bool       │
│  + uploadItem(room: Room, ...) -> Bool  │
└─────────────┬───────────────────────────┘
              │ 实现
    ┌─────────┼─────────┬─────────┐
    ↓         ↓         ↓         ↓
┌──────┐ ┌────────┐ ┌────────┐ ┌────────┐
│准备  │ │上拍    │ │拍卖中  │ │定拍    │
│State │ │State   │ │State   │ │State   │
└──────┘ └────────┘ └────────┘ └────────┘

核心组件

1. 状态枚举

enum RoomState: String {
    case preparing      // 准备阶段
    case listing        // 上拍
    case auctioning     // 拍卖中
    case closed         // 定拍
}

2. 状态协议

protocol RoomStateProtocol {
    var stateName: RoomState { get }
    
    // 状态转换
    func startAuction(room: Room) -> Bool
    func endAuction(room: Room) -> Bool
    
    // 业务操作
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool
    func uploadItem(room: Room, item: AuctionItem, rules: AuctionRules) -> Bool
    
    // 状态描述
    func getStateDescription() -> String
}

状态转换图

┌─────────────┐
│  准备阶段    │ 拍卖人上传物品、设置规则
│  Preparing  │ 房主可以开始拍卖
└──────┬──────┘
       │ startAuction()
       ↓
┌─────────────┐
│    上拍     │ 展示物品信息
│   Listing   │ 倒计时准备(3秒)
└──────┬──────┘
       │ 自动转换 / 房主提前开始
       ↓
┌─────────────┐
│   拍卖中    │ 用户可以出价
│ Auctioning  │ 倒计时重置机制
└──────┬──────┘
       │ endAuction() / 倒计时归零
       ↓
┌─────────────┐
│    定拍     │ 展示成交结果
│   Closed    │ 可以开启下一轮
└──────┬──────┘
       │ startAuction() (开启下一轮)
       ↓
┌─────────────┐
│  准备阶段    │ 回到初始状态
│  Preparing  │
└─────────────┘

具体实现

1. 准备阶段(Preparing)

class PreparingState: RoomStateProtocol {
    var stateName: RoomState { return .preparing }
    
    // ✅ 允许:开始拍卖
    func startAuction(room: Room) -> Bool {
        guard room.currentItem != nil else {
            print("⚠️ 没有拍卖物品,无法开始")
            return false
        }
        
        // 状态转换:准备 → 上拍
        room.changeState(to: .listing)
        
        // 3秒后自动进入拍卖中
        DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
            room.changeState(to: .auctioning)
        }
        
        return true
    }
    
    // ❌ 不允许:出价
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        print("⚠️ 拍卖还未开始,无法出价")
        return false
    }
    
    // ✅ 允许:上传物品
    func uploadItem(room: Room, item: AuctionItem, rules: AuctionRules) -> Bool {
        room.setAuctionItem(item, rules: rules)
        return true
    }
    
    // ❌ 不允许:结束拍卖
    func endAuction(room: Room) -> Bool {
        print("⚠️ 拍卖还未开始")
        return false
    }
    
    func getStateDescription() -> String {
        return "准备阶段:拍卖人可以上传物品并设置规则"
    }
}

关键点

  • ✅ 只允许上传物品和开始拍卖
  • ✅ 自动触发状态转换(准备 → 上拍 → 拍卖中)
  • ✅ 逻辑清晰,职责单一

2. 上拍阶段(Listing)

class ListingState: RoomStateProtocol {
    var stateName: RoomState { return .listing }
    
    // ✅ 允许:房主提前开始
    func startAuction(room: Room) -> Bool {
        room.changeState(to: .auctioning)
        return true
    }
    
    // ❌ 不允许:出价
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        print("⚠️ 拍卖还未正式开始,无法出价")
        return false
    }
    
    // ❌ 不允许:修改物品
    func uploadItem(room: Room, item: AuctionItem, rules: AuctionRules) -> Bool {
        print("⚠️ 上拍阶段无法修改物品")
        return false
    }
    
    // ❌ 不允许:结束拍卖
    func endAuction(room: Room) -> Bool {
        print("⚠️ 拍卖还未正式开始")
        return false
    }
    
    func getStateDescription() -> String {
        return "上拍中:展示拍卖物品,倒计时后自动开始"
    }
}

关键点

  • 🎯 过渡状态:用于展示物品信息
  • ✅ 房主可以提前开始
  • ❌ 大部分操作被禁止,保证流程的严谨性

3. 拍卖中(Auctioning)⭐ 核心状态

class AuctioningState: RoomStateProtocol {
    var stateName: RoomState { return .auctioning }
    
    // ❌ 不允许:重复开始
    func startAuction(room: Room) -> Bool {
        print("⚠️ 拍卖已经在进行中")
        return false
    }
    
    // ✅ 允许:结束拍卖
    func endAuction(room: Room) -> Bool {
        room.changeState(to: .closed)
        
        if let winner = room.currentBid {
            room.addSystemMessage("🎉 成交!恭喜 (winner.bidderName) 以 ¥(winner.price) 拍得")
        } else {
            room.addSystemMessage("流拍:没有人出价")
        }
        
        return true
    }
    
    // ✅ 允许:出价(核心逻辑)
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        // 创建出价记录
        let bid = Bid(
            id: UUID().uuidString,
            price: amount,
            bidderId: user.id,
            bidderName: user.nickname,
            timestamp: Date()
        )
        
        // 记录出价
        room.addBid(bid)
        
        print("💰 (user.nickname) 出价 ¥(amount)")
        
        // 这里可以重置倒计时(简化版省略)
        // resetCountdown()
        
        return true
    }
    
    // ❌ 不允许:修改物品
    func uploadItem(room: Room, item: AuctionItem, rules: AuctionRules) -> Bool {
        print("⚠️ 拍卖进行中,无法修改物品")
        return false
    }
    
    func getStateDescription() -> String {
        return "拍卖中:竞拍者可以出价,倒计时结束后定拍"
    }
}

关键点

  • 💰 核心业务逻辑:处理出价
  • 📊 实时更新:记录每次出价
  • ⏱️ 倒计时机制:有出价时重置(可扩展)
  • 🔄 状态转换:可以结束进入定拍

4. 定拍阶段(Closed)

class ClosedState: RoomStateProtocol {
    var stateName: RoomState { return .closed }
    
    // ✅ 允许:开启下一轮
    func startAuction(room: Room) -> Bool {
        // 重置房间状态
        room.changeState(to: .preparing)
        room.currentItem = nil
        room.currentBid = nil
        room.addSystemMessage("🔄 准备下一轮拍卖")
        return true
    }
    
    // ❌ 不允许:出价
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        print("⚠️ 拍卖已经结束,无法出价")
        return false
    }
    
    // ❌ 不允许:重复结束
    func endAuction(room: Room) -> Bool {
        print("⚠️ 拍卖已经结束")
        return false
    }
    
    // ❌ 不允许:上传物品
    func uploadItem(room: Room, item: AuctionItem, rules: AuctionRules) -> Bool {
        print("⚠️ 拍卖已结束,请开启下一轮")
        return false
    }
    
    func getStateDescription() -> String {
        return "已定拍:拍卖结束,可以开启下一轮"
    }
}

关键点

  • 🎉 展示成交结果
  • 🔄 支持循环拍卖:可以开启下一轮
  • 🔒 所有拍卖操作被锁定

与权限中心协作

设计哲学:分离关注点

┌─────────────────────────────────────┐
│         用户发起操作                 │
└──────────────┬──────────────────────┘
               ↓
┌─────────────────────────────────────┐
│      RoomManager(协调层)           │
└──────────────┬──────────────────────┘
               ↓
        ┌──────┴──────┐
        ↓             ↓
┌──────────────┐ ┌──────────────┐
│ 权限中心      │ │ 状态对象      │
│"能不能做"    │ │"怎么做"      │
└──────────────┘ └──────────────┘

协作流程

class RoomManager {
    func placeBid(user: User, room: Room, amount: Decimal) -> Result<Void, RoomError> {
        // 第一步:权限中心检查"能不能做"
        let result = permissionCenter.checkPermission(
            action: .placeBid,
            user: user,
            room: room,
            metadata: ["amount": amount]
        )
        
        guard result.isAllowed else {
            return .failure(.permissionDenied(result.deniedReason ?? "权限不足"))
        }
        
        // 第二步:状态对象执行"怎么做"
        let success = room.stateObject.placeBid(room: room, user: user, amount: amount)
        
        if success {
            return .success(())
        } else {
            return .failure(.operationFailed("出价失败"))
        }
    }
}

权限规则示例

// 权限中心:检查"能不能做"
PermissionRule(
    action: .placeBid,
    priority: 100,
    description: "只能在拍卖中状态出价"
) { context in
    guard context.room.state == .auctioning else {
        return .denied(reason: "❌ 当前不在拍卖阶段,无法出价")
    }
    return .allowed
}

PermissionRule(
    action: .placeBid,
    priority: 90,
    description: "拍卖人不能给自己出价"
) { context in
    if context.user.role == .auctioneer,
       context.user.id == context.room.currentItem?.auctioneerId {
        return .denied(reason: "❌ 您是拍卖人,不能对自己的物品出价")
    }
    return .allowed
}

为什么要分离?

如果不分离

// ❌ 反例:状态和权限混在一起
class AuctioningState {
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        // 权限判断
        if user.role == .viewer {
            return false
        }
        if user.role == .auctioneer && user.id == auctioneer.id {
            return false
        }
        if amount < currentPrice + increment {
            return false
        }
        
        // 业务逻辑
        room.addBid(...)
        return true
    }
}

分离后

// ✅ 状态对象:只关心业务逻辑
class AuctioningState {
    func placeBid(room: Room, user: User, amount: Decimal) -> Bool {
        room.addBid(...)  // 纯粹的业务逻辑
        return true
    }
}

// ✅ 权限中心:只关心权限验证
PermissionCenter.check(.placeBid, user, room)

优势

  1. 单一职责:状态对象不关心权限
  2. 易于扩展:新增权限规则不影响状态
  3. 易于测试:可以独立测试权限和状态
  4. 灵活配置:权限规则可以动态调整

实际应用场景

场景1:完整拍卖流程

// 1. 创建房间(自动进入准备阶段)
let room = Room(name: "今晚靓号专场", owner: host)
print("房间状态:(room.state.displayName)") // 准备中

// 2. 拍卖人上传物品
let item = AuctionItem(name: "手机号 13888888888", ...)
room.stateObject.uploadItem(room: room, item: item, rules: rules)
// ✅ 准备阶段允许上传物品

// 3. 房主开始拍卖
room.stateObject.startAuction(room: room)
// 状态转换:准备 → 上拍
print("房间状态:(room.state.displayName)") // 上拍中

// 4. 3秒后自动进入拍卖中
// 状态转换:上拍 → 拍卖中
print("房间状态:(room.state.displayName)") // 拍卖中

// 5. 竞拍者出价
room.stateObject.placeBid(room: room, user: bidder1, amount: 120)
// ✅ 拍卖中状态允许出价
print("当前价格:¥(room.currentPrice)") // ¥120

room.stateObject.placeBid(room: room, user: bidder2, amount: 150)
print("当前价格:¥(room.currentPrice)") // ¥150

// 6. 房主结束拍卖
room.stateObject.endAuction(room: room)
// 状态转换:拍卖中 → 定拍
print("房间状态:(room.state.displayName)") // 已定拍
print("成交:(room.currentLeader) - ¥(room.currentPrice)")

// 7. 开启下一轮
room.stateObject.startAuction(room: room)
// 状态转换:定拍 → 准备
print("房间状态:(room.state.displayName)") // 准备中

场景2:错误的操作被拒绝

// 尝试在准备阶段出价
let room = Room(...)
room.stateObject.placeBid(room: room, user: bidder, amount: 200)
// ❌ 输出:"拍卖还未开始,无法出价"
// 返回:false

// 尝试在拍卖中上传物品
room.stateObject.startAuction(room: room)  // 进入拍卖中
room.stateObject.uploadItem(room: room, item: item, rules: rules)
// ❌ 输出:"拍卖进行中,无法修改物品"
// 返回:false

// 尝试在定拍后出价
room.stateObject.endAuction(room: room)  // 进入定拍
room.stateObject.placeBid(room: room, user: bidder, amount: 300)
// ❌ 输出:"拍卖已经结束,无法出价"
// 返回:false

场景3:状态转换的严格性

let room = Room(...)

// ✅ 正确的转换
room.state  // .preparing
room.stateObject.startAuction(room: room)
room.state  // .listing → .auctioning

// ❌ 不允许跳过状态
room.state  // .preparing
room.stateObject.endAuction(room: room)
// 输出:"拍卖还未开始"
// 状态不变,仍然是 .preparing

优势与挑战

✅ 优势

1. 代码组织清晰

对比

传统方式(500行的switch):

func handleOperation() {
    switch currentState {
    case .preparing:
        // 100行代码
    case .listing:
        // 100行代码
    case .auctioning:
        // 200行代码
    case .closed:
        // 100行代码
    }
}

状态模式(每个文件<100行):

PreparingState.swift    // 80行
ListingState.swift      // 60行
AuctioningState.swift   // 100行
ClosedState.swift       // 60行

2. 易于维护

修改"拍卖中"的逻辑:

  • ❌ 传统方式:在500行代码中找到对应的case,小心翼翼地修改
  • ✅ 状态模式:直接打开AuctioningState.swift,放心修改

3. 符合开闭原则

新增"暂停"状态:

  • ❌ 传统方式:修改所有的switch语句,增加新的case
  • ✅ 状态模式:创建PausedState.swift,不修改现有代码

4. 便于测试

// 可以单独测试某个状态
func testAuctioningState() {
    let state = AuctioningState()
    let room = MockRoom()
    let result = state.placeBid(room: room, user: mockUser, amount: 100)
    XCTAssertTrue(result)
}

5. 团队协作友好

多人开发时:

  • 小明负责 PreparingState
  • 小红负责 AuctioningState
  • 小刚负责 ClosedState

互不干扰,Git冲突少。

⚠️ 挑战

1. 类的数量增加

  • 4个状态 = 4个类文件
  • 如果有10个状态,就需要10个文件

应对:合理的文件组织和命名规范

2. 状态转换的复杂性

需要仔细设计状态转换图,避免:

  • 死锁状态
  • 循环转换
  • 无法到达的状态

应对

  • 绘制状态图
  • 编写状态转换测试
  • 文档化转换规则

3. 状态间的数据共享

状态对象是无状态的,数据存储在Room对象中:

class Room {
    var stateObject: RoomStateProtocol  // 当前状态对象
    var currentItem: AuctionItem?       // 状态间共享的数据
    var currentBid: Bid?                // 状态间共享的数据
}

应对

  • 明确哪些数据属于上下文(Room)
  • 哪些数据属于状态对象

4. 调试可能更困难

调用链变长:

ViewController → RoomManager → PermissionCenter → StateObject

应对

  • 添加详细的日志
  • 使用断点调试
  • 编写单元测试

最佳实践

1. 状态对象应该是无状态的

// ❌ 错误:状态对象持有数据
class AuctioningState {
    var currentPrice: Decimal = 0  // 不应该在这里
    var bidHistory: [Bid] = []     // 不应该在这里
}

// ✅ 正确:数据存储在上下文中
class Room {
    var currentPrice: Decimal
    var bidHistory: [Bid]
    var stateObject: RoomStateProtocol
}

2. 使用工厂方法创建状态

class Room {
    func changeState(to newState: RoomState) {
        self.state = newState
        
        // 工厂方法
        switch newState {
        case .preparing:
            self.stateObject = PreparingState()
        case .listing:
            self.stateObject = ListingState()
        case .auctioning:
            self.stateObject = AuctioningState()
        case .closed:
            self.stateObject = ClosedState()
        }
        
        addSystemMessage("房间状态变更为:(newState.displayName)")
    }
}

3. 记录状态转换日志

func changeState(to newState: RoomState) {
    let oldState = self.state
    self.state = newState
    
    // 记录状态转换
    print("🔄 状态转换:(oldState.displayName) → (newState.displayName)")
    
    // 可以添加到数据库或分析系统
    Analytics.trackStateChange(from: oldState, to: newState)
}

4. 验证状态转换的合法性

func changeState(to newState: RoomState) {
    // 验证转换是否合法
    guard isValidTransition(from: self.state, to: newState) else {
        print("⚠️ 非法的状态转换:(self.state) → (newState)")
        return
    }
    
    // 执行转换
    self.state = newState
    self.stateObject = createState(newState)
}

private func isValidTransition(from: RoomState, to: RoomState) -> Bool {
    let validTransitions: [RoomState: [RoomState]] = [
        .preparing: [.listing],
        .listing: [.auctioning],
        .auctioning: [.closed],
        .closed: [.preparing]
    ]
    
    return validTransitions[from]?.contains(to) ?? false
}

5. 提供状态查询接口

extension Room {
    var canStartAuction: Bool {
        return stateObject.startAuction(room: self)
    }
    
    var canPlaceBid: Bool {
        return state == .auctioning
    }
    
    var canUploadItem: Bool {
        return state == .preparing
    }
}

// 使用
if room.canPlaceBid {
    room.stateObject.placeBid(...)
}

6. 编写完整的单元测试

class StatePatternTests: XCTestCase {
    func testStateTransitions() {
        let room = Room(...)
        
        // 测试初始状态
        XCTAssertEqual(room.state, .preparing)
        
        // 测试状态转换
        room.stateObject.startAuction(room: room)
        XCTAssertEqual(room.state, .listing)
        
        // 等待自动转换
        wait(for: 3)
        XCTAssertEqual(room.state, .auctioning)
    }
    
    func testInvalidOperations() {
        let room = Room(...)
        
        // 在准备阶段不能出价
        let result = room.stateObject.placeBid(...)
        XCTAssertFalse(result)
    }
}

总结

何时使用状态模式

适合使用的场景

  1. 对象行为随状态改变而改变
  2. 有明确的状态转换规则
  3. 状态相关的代码较多
  4. 需要避免大量的条件判断

不适合使用的场景

  1. 状态很少(2-3个)
  2. 状态间没有明确的转换规则
  3. 状态逻辑非常简单
  4. 性能要求极高的场景

状态模式的价值

在拍拍房项目中,状态模式:

  1. 将复杂的业务流程结构化
    • 4个状态,4个类,清晰明了
    • 每个状态独立,互不干扰
  1. 提高代码质量
    • 避免了数百行的switch语句
    • 符合单一职责原则
    • 符合开闭原则
  1. 增强可维护性
    • 修改某个状态不影响其他状态
    • 新增状态只需添加新类
    • 状态转换一目了然
  1. 改善团队协作
    • 不同开发者可以独立开发不同状态
    • 减少Git冲突
    • 代码审查更容易
  1. 与权限中心完美配合
    • 状态负责"怎么做"
    • 权限负责"能不能做"
    • 职责清晰,耦合度低

最后的建议

  1. 不要过度设计:如果只有2-3个简单状态,可能不需要状态模式
  2. 绘制状态图:在实现之前先画出状态转换图
  3. 编写测试:为每个状态编写单元测试
  4. 文档化:记录每个状态的职责和转换规则
  5. 逐步重构:可以先用简单方式实现,再重构为状态模式

参考资源

设计模式相关

  • 《设计模式:可复用面向对象软件的基础》- GoF
  • 《Head First 设计模式》

本项目相关

❌
❌