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!

相关推荐
ii_best15 小时前
安卓/ios脚本开发辅助工具按键精灵横纵坐标转换教程
android·开发语言·ios·安卓
先飞的笨鸟17 小时前
2026 年 Expo + React Native 项目接入微信分享完整指南
前端·ios·app
初级代码游戏18 小时前
iOS开发 SwiftUI 5 : 文本输入 密码输入 多行输入
ios·swiftui·swift
iosTiov18 小时前
ios生态的分发密钥:企业签、V3签、TF签深度解析与选型指南
安全·ios·团队开发·苹果签名·稳定
00后程序员张19 小时前
在 iPhone 上进行 iOS 网络抓包的实践经验
android·ios·小程序·https·uni-app·iphone·webview
2501_9159184119 小时前
介绍如何在电脑上查看 iPhone 和 iPad 的完整设备信息
android·ios·小程序·uni-app·电脑·iphone·ipad
晨枫阳19 小时前
iOS Universal Links配置
ios
2501_9160088919 小时前
没有 Mac 如何在 Windows 上创建 iOS 应用描述文件
android·macos·ios·小程序·uni-app·iphone·webview
北京自在科技1 天前
苹果iOS 26.3实现跨安卓数据无缝迁移
android·ios·findmy
郑梓斌1 天前
Luban 2 Flutter:一行代码在 Flutter 开发中实现图片压缩功能
flutter·ios