一、概念:延迟决策(Delay Decision)
延迟决策 = 在设计中尽量推迟对具体实现、技术选型或模块边界的硬性绑定,直到有足够信息支撑明确决策。
- 目标:避免早期猜测导致架构僵化
- 核心原则:面向抽象、保持可替换性
- 本质是“先定义接口 / 协议 /抽象,后选择实现”,而不是一开始就固定实现方案
二、为什么重要
1️⃣ 避免早期耦合 / 反向依赖
- 早期直接在模块内部使用具体实现 → 改动成本高
- 延迟决策 → 用协议 /事件/抽象接口隔离依赖
2️⃣ 保持演进能力
- 业务需求、技术栈、性能优化都会变化
- 提前固定某种实现会阻碍迭代
3️⃣ 提升测试和重用性
- 延迟绑定具体实现 → 可以在测试中 mock / stub
- 可以在不同上下文复用模块
三、实践策略
1️⃣ 面向协议 / 接口编程
- 逻辑层只依赖协议,不依赖具体实现
- 具体实现延迟到 DI / 组装器 / Factory 注入
protocol AuthService {
func login(username: String, password: String) async throws -> Bool
}
// 延迟到应用启动或测试时绑定具体实现
let authService: AuthService = NetworkAuthService()
2️⃣ 延迟注入 / 依赖反转
- 构造注入 / 环境注入 / Factory 延迟决定依赖
struct LoginViewModel {
let authService: AuthService
}
- LoginViewModel 不关心使用的是 Network / Mock / Local Storage
- 依赖决定可以推迟到 App 层
3️⃣ 使用 Effect / Publisher 封装副作用
- 副作用(网络 / DB / 文件)封装在 Effect / Publisher
- 模块内部只依赖抽象,具体执行逻辑可以后续替换
func loadUser() -> AnyPublisher<User, Error> { ... }
- 未来可以换成 async/await、缓存策略、GraphQL,不破坏依赖模块
4️⃣ 延迟界面 / 业务细节
- SwiftUI / MVVM:View 只依赖状态,具体渲染延迟到 View 层决定
- Presenter / ViewModel 不写具体动画 /控件 → UI 变化不会影响逻辑层
四、真实项目应用案例
- 网络层
- 早期不直接写 URLSession
- 定义协议
NetworkClient→ 延迟决定使用 URLSession / Alamofire / Mock
- Feature 模块
- 不提前决定模块如何交互
- 用事件 / Action / Publisher 定义接口 → 后期可换不同 UI / State 管理方案
- 数据库 / 持久化
- 定义接口
UserRepository - 延迟决定 CoreData / Realm / SQLite / FileStorage
五、工程直觉
| 延迟决策策略 | 典型实践 |
|---|---|
| 面向协议 | 逻辑层只依赖抽象接口 |
| 延迟注入 | Factory / DI / Environment 注入具体实现 |
| 延迟副作用 | Effect / Publisher 封装异步任务 |
| 延迟 UI / 渲染 | View 只观察 State,不操作逻辑层 |
核心思想:先定义“能做什么”,再决定“怎么做” 。
这样架构在项目中期或后期仍能灵活演进,不会因为早期实现决定而僵化。