当MVVM和MVC不再奏效,测试变成噩梦时
您是否曾经编写过一个Swift应用,半年后添加一个新功能却破坏了其他三个功能,而测试因为数据库从Core Data换成Realm而不得不完全重写?如果是这样,您并不孤单。经典的分层架构(MVC、MVVM)在项目初期运行良好,但随着项目增长会变成一团“乱麻”。
我将向您展示Hexagonal架构(也称为Ports & Adapters)——一种源自Java和DDD世界的方法,但非常适合Swift。我们将通过真实示例来解析它,而不是抽象的“User”和“Manager”。您将看到如何将业务逻辑与数据库、UI和网络分离,以便在不触及应用核心的情况下更换它们。
这不是又一个理论——这是您今天就可以在自己的项目中应用的实践。
什么是Hexagonal架构?简单来说
想象一个六边形(因此得名)。中心是您纯净的业务逻辑(领域)。边缘是端口(接口),外部世界通过它们与核心通信。适配器是具体的实现:数据库、API、UI、文件系统。
主要规则是:核心对外部世界一无所知。它不知道UIKit、URLSession或Core Data。它只知道自己的接口(端口)。
生活中的例子:想象一个在线商店。核心知道有“购物车”和“下订单”。它不在乎数据来自哪里——来自WebSocket、本地JSON还是键盘。适配器连接到端口,将外部信号转换为领域命令。
为什么应该在Swift项目中使用它?
- 可测试性——您可以在没有真实数据库和网络的情况下测试领域。只需用模拟对象替换适配器。
- 灵活性——想从Firebase切换到自己的服务器?只需更换一个适配器,而不是半个应用。
- 可读性——业务逻辑不会分散在ViewController中。它集中在一个地方。
- 框架无关性——您的核心不导入UIKit、Combine或Alamofire。
Swift中Hexagonal架构的项目结构
让我们直接进入代码。假设我们正在编写一个任务管理应用(Todolist)。文件夹结构如下:
App/├── Domain/│ ├── Entity/│ │ └── Task.swift│ ├── UseCase/│ │ └── CreateTaskUseCase.swift│ └── Port/│ ├── TaskRepository.swift│ └── TaskOutputPort.swift├── Adapter/│ ├── Persistence/│ │ └── CoreDataTaskRepository.swift│ ├── Network/│ │ └── APITaskRepository.swift│ └── Presentation/│ └── TaskListViewModel.swift└── Main/ └── AppDelegate.swift请注意:Domain除了Foundation之外不导入任何东西。Adapter可以导入CoreData、UIKit、Alamofire——任何东西。
步骤1:领域实体和端口
我们从最重要的部分开始——领域模型。这是一个描述业务实体的struct或enum。没有NSManagedObject,没有Codable。
// Domain/Entity/Task.swiftstruct Task { let id: UUID var title: String var isCompleted: Bool let createdAt: Date}现在定义仓库端口(输出端口)。这是一个描述如何保存和加载任务的协议。
// Domain/Port/TaskRepository.swiftprotocol TaskRepository { func fetchAll() async throws -> [Task] func save(task: Task) async throws func delete(taskID: UUID) async throws}以及输入端口——外部世界(UI、API)通过它与核心交互。通常这是一个UseCase。
// Domain/Us