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!

相关推荐
2501_9160088914 分钟前
iOS 26 软件性能测试全流程,启动渲染资源压力对比与优化策略
android·macos·ios·小程序·uni-app·cocoa·iphone
00后程序员张1 小时前
iOS 26 兼容测试实战,机型兼容、SwiftUI 兼容性改动
android·ios·小程序·uni-app·swiftui·cocoa·iphone
2501_915106322 小时前
iOS 可分发是已经上架了吗?深入解析应用分发状态、ipa 文件上传、TestFlight 测试与 App Store 审核流程
android·ios·小程序·https·uni-app·iphone·webview
2501_9160074710 小时前
HTTPS 抓包乱码怎么办?原因剖析、排查步骤与实战工具对策(HTTPS 抓包乱码、gzipbrotli、TLS 解密、iOS 抓包)
android·ios·小程序·https·uni-app·iphone·webview
2501_9160088916 小时前
HTTPS 双向认证抓包实战,原理、难点、工具与可操作的排查流程
网络协议·http·ios·小程序·https·uni-app·iphone
2501_9151063216 小时前
HTTPS 能抓包吗?实战答案与逐步可行方案(HTTPS 抓包原理、证书Pinning双向认证应对、工具对比)
网络协议·http·ios·小程序·https·uni-app·iphone
游戏开发爱好者816 小时前
App HTTPS 抓包实战,原理、常见问题与可行工具路线(开发 测试 安全 角度)
网络协议·安全·ios·小程序·https·uni-app·iphone
2501_9151063216 小时前
App HTTPS 抓包实战指南,原理、常见阻碍、逐步排查与工具组合
网络协议·http·ios·小程序·https·uni-app·iphone
CocoaKier17 小时前
苹果海外老账号续费,踩了个大坑!
ios·apple