Domain 层完全指南(面向 iOS 开发者)


目录

  1. [为什么需要 Domain 层](#为什么需要 Domain 层)
  2. 清晰的三层架构
  3. [核心概念:Entity / Value Object / Use Case / Repository](#核心概念:Entity / Value Object / Use Case / Repository)
  4. [Swift 代码实战](#Swift 代码实战)
  5. 测试策略
  6. 在旧项目中落地的步骤
  7. 结语

1 为什么需要 Domain 层

在传统 MVC / MVVM 中,我们往往把业务规则 写进 ViewController 或 ViewModel。

问题随规模放大而爆发:

痛点 具体表现
可测试性差 单元测试必须启动 UIKit,跑真机或模拟器
业务难复用 同样的计费、权限逻辑被多处复制
维护成本高 UI 改版常常误伤业务代码

Domain 层 = 把"业务世界"的概念模型用例流程抽离出来,形成纯 Swift 代码;UI 与外部数据存取只依赖它,却不影响它。


2 三层架构速览

层级 依赖方向 关键词
Presentation ⬇︎ 调用 UseCase UIKit / SwiftUI / Combine / Bloc
Domain 纯 Swift Entity•ValueObject•UseCase•Repository协议
Data / Infrastructure ⬆︎ 实现 Repository URLSession / CoreData / Realm / BLE

依赖只允许由外向内,Domain 不感知任何框架。


3 关键概念

角色 职责 要点
Entity 有唯一标识 + 生命周期,如 Order 行为应遵守不变式
Value Object 无标识,靠值判等,如 Money 必须不可变
Use Case (Interactor) 满足用户故事的业务流程,如 PlaceOrder 只依赖协议
Repository 协议 Domain 访问数据的抽象 不关心具体存储方式

Place Order 意思是:下单 / 提交订单


4 Swift 代码实战

场景:展示并更新聊天未读数

4.1 Entity 与 Value Object

swift 复制代码
// Value Object
struct UnreadCount: Equatable {
    let value: Int
    init(_ raw: Int) {
        precondition(raw >= 0, "Unread cannot be negative")
        value = raw
    }
}

// Entity
struct Conversation: Identifiable, Equatable {
    let id: UUID
    private(set) var unread: UnreadCount

    mutating func markAllRead() {
        unread = .init(0)
    }
}

4.2 Repository 协议

swift 复制代码
protocol ConversationRepository {
    /// 从缓存或网络获取未读数
    func unreadCount() async throws -> UnreadCount
    /// 将未读数持久化
    func save(_ count: UnreadCount) async throws
}

4.3 Use Case

swift 复制代码
/// 单一职责:获取并缓存未读数
struct GetUnreadCountUseCase {
    private let repo: ConversationRepository
    init(repo: ConversationRepository) { self.repo = repo }

    func execute() async throws -> UnreadCount {
        let count = try await repo.unreadCount()
        try await repo.save(count)      // 读完即写缓存
        return count
    }
}

4.4 Data 层实现(摘录)

swift 复制代码
final class ConversationApiDataSource: ConversationRepository {
    private let api: URLSession
    private let cache: UserDefaults

    func unreadCount() async throws -> UnreadCount {
        let (data, _) = try await api.data(from: URL(string: "/unread")!)
        let json = try JSONDecoder().decode(UnreadDTO.self, from: data)
        return .init(json.total)
    }
    func save(_ count: UnreadCount) async throws {
        cache.set(count.value, forKey: "unread_total")
    }
}

4.5 Presentation 层集成

swift 复制代码
final class UnreadCubit: Cubit<UnreadState> {
    private let getCount: GetUnreadCountUseCase
    init(getCount: GetUnreadCountUseCase) {
        self.getCount = getCount
        super.init(Initial())
    }

    @MainActor
    func fetch() {
        Task {
            emit(Loading())
            do {
                let count = try await getCount.execute()
                emit(Loaded(count))
            } catch {
                emit(Failed(error))
            }
        }
    }
}
  • UI 只感知 UnreadState,不关心 Repository 具体实现。
  • 想改用 Realm 缓存?仅替换 ConversationApiDataSource,Domain 与 UI 零改动。

5 单元测试策略

swift 复制代码
final class FakeConversationRepo: ConversationRepository {
    var next: UnreadCount = .init(3)
    func unreadCount() async throws -> UnreadCount { next }
    func save(_ count: UnreadCount) async throws { /* no-op */ }
}

func testGetUnreadCount() async throws {
    let repo = FakeConversationRepo()
    let useCase = GetUnreadCountUseCase(repo: repo)
    let result = try await useCase.execute()
    XCTAssertEqual(result, .init(3))
}
  • 无需启动 App、无需网络;执行速度毫秒级。
  • Entity 的不变式可直接覆盖极端值(负数、溢出等)。

6 如何在旧项目落地

  1. 挑出最稳定的业务规则(如价格计算、权限判断)。
  2. 抽成纯 Swift 类型,斩断 UIKit / CoreData 依赖。
  3. 对 UI 暴露 Use Case 协议,用 DI 容器(例:Swinject)注入实现。
  4. 渐进式替换:新功能强制走 Domain;旧代码按需迁移。
  5. 持续加测试,确保迁移未破坏行为。

7 结语

Domain 层让 iOS 项目的业务核心 脱离平台细节,既提高可测试性 ,又带来长久可维护性

掌握它,你将在大型团队协作与多端共享逻辑(watchOS / visionOS / server Swift)时,享受显著的工程收益。

Happy refactoring!

相关推荐
他们都不看好你,偏偏你最不争气4 小时前
AutoLayout与Masonry:简化iOS布局
ios
2501_916008897 小时前
iOS 抓包工具有哪些?全面盘点主流工具与功能对比分析
android·ios·小程序·https·uni-app·iphone·webview
2501_915921437 小时前
iOS混淆工具实战 视频流媒体类 App 的版权与播放安全保护
android·ios·小程序·https·uni-app·iphone·webview
2501_9160088911 小时前
uni-app iOS 日志与崩溃分析全流程 多工具协作的实战指南
android·ios·小程序·https·uni-app·iphone·webview
2501_9159214312 小时前
iOS混淆工具实战 在线教育直播类 App 的课程与互动安全防护
android·安全·ios·小程序·uni-app·iphone·webview
Digitally14 小时前
没 iCloud, 如何数据从iPhone转移到iPhone
ios·iphone·icloud
笑尘pyrotechnic14 小时前
push pop 和 present dismiss
macos·ui·ios·objective-c·cocoa
Digitally15 小时前
如何将联系人从 iPhone 转移到 Redmi 手机
ios·智能手机·iphone
2501_9151063216 小时前
Charles抓包工具在接口性能优化与压力测试中的实用方法
ios·性能优化·小程序·https·uni-app·压力测试·webview
Winson℡1 天前
在 React Native 层禁止 iOS 左滑返回(手势返回/手势退出)
react native·react.js·ios