iOS 语音房(拍卖房)开发实践
2025年11月29日 17:49
本文基于一个真实的iOS语音房项目案例,详细讲解如何使用状态模式来管理复杂的业务流程,以及如何与权限中心协同工作,因为在拍卖房间中不只有不同的房间阶段变化(状态)还有不同角色拥有不同的权限(权限中心)。
业务场景
拍拍房是一个实时拍卖房间系统,类似于语音房间+拍卖的结合体。用户可以在房间内:
- 作为房主主持拍卖
- 作为拍卖人上传物品并介绍
- 作为竞拍者出价竞拍
- 作为观众观看拍卖过程
核心业务流程
一个完整的拍卖流程需要经历4个明确的阶段:
准备阶段 → 上拍 → 拍卖中 → 定拍 → (循环)准备阶段
每个阶段都有:
- 不同的允许操作(如只能在准备阶段上传物品)
- 不同的状态转换规则(如只能从拍卖中进入定拍)
- 不同的业务逻辑(如只有拍卖中才能出价)
技术挑战
- 状态多:4个主要状态,每个状态行为差异大
- 转换复杂:状态之间的转换有严格的规则
- 权限交织:每个操作还需要考虑用户角色权限
- 易扩展性:未来可能增加新的拍卖模式
为什么选择状态模式
❌ 不使用状态模式的问题
如果使用传统的 if-else 或 switch-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
}
}
问题显而易见:
- 🔴 代码臃肿:所有状态的逻辑混在一起
- 🔴 难以维护:修改一个状态可能影响其他状态
- 🔴 不易扩展:增加新状态需要修改多处代码
- 🔴 权限混乱:业务逻辑和权限判断纠缠在一起
- 🔴 测试困难:无法单独测试某个状态的逻辑
✅ 使用状态模式的优势
// 状态模式:每个状态独立处理
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
}
}
优势明显:
- ✅ 职责单一:每个状态类只关注自己的逻辑
- ✅ 易于维护:修改某个状态不影响其他状态
- ✅ 开闭原则:新增状态只需添加新类,不修改现有代码
- ✅ 清晰直观:状态转换一目了然
- ✅ 便于测试:可以单独测试每个状态
状态模式设计
整体架构
┌─────────────────────────────────────────┐
│ 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:完整拍卖流程
// 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)
}
}
总结
何时使用状态模式
✅ 适合使用的场景:
- 对象行为随状态改变而改变
- 有明确的状态转换规则
- 状态相关的代码较多
- 需要避免大量的条件判断
❌ 不适合使用的场景:
- 状态很少(2-3个)
- 状态间没有明确的转换规则
- 状态逻辑非常简单
- 性能要求极高的场景
状态模式的价值
在拍拍房项目中,状态模式:
- 将复杂的业务流程结构化
-
- 4个状态,4个类,清晰明了
- 每个状态独立,互不干扰
- 提高代码质量
-
- 避免了数百行的switch语句
- 符合单一职责原则
- 符合开闭原则
- 增强可维护性
-
- 修改某个状态不影响其他状态
- 新增状态只需添加新类
- 状态转换一目了然
- 改善团队协作
-
- 不同开发者可以独立开发不同状态
- 减少Git冲突
- 代码审查更容易
- 与权限中心完美配合
-
- 状态负责"怎么做"
- 权限负责"能不能做"
- 职责清晰,耦合度低
最后的建议
- 不要过度设计:如果只有2-3个简单状态,可能不需要状态模式
- 绘制状态图:在实现之前先画出状态转换图
- 编写测试:为每个状态编写单元测试
- 文档化:记录每个状态的职责和转换规则
- 逐步重构:可以先用简单方式实现,再重构为状态模式
参考资源
设计模式相关
- 《设计模式:可复用面向对象软件的基础》- GoF
- 《Head First 设计模式》