一、先明確:Clean Architecture 的核心目標
- 傳統架構(比如混亂的 MVC)中,業務邏輯會和 UI、網絡、第三方庫深度耦合(比如 Controller 裏既寫登錄邏輯,又調 Alamofire,還處理 UI 刷新),導致:
二、Clean Architecture 的 4 大核心設計原則
1. 依賴反轉原則(DIP)
- 白話解釋:“高層模塊(業務邏輯)不依賴低層模塊(技術細節),兩者都依賴抽象;抽象不依賴具體實現,具體實現依賴抽象”。
- iOS 場景舉例:
- 錯誤做法:“獲取新聞” 的業務邏輯直接調用
Alamofire.request()(依賴具體網絡庫); - 正確做法:定義
NewsRepository抽象協議(只規定 “要能獲取新聞”),業務邏輯依賴這個協議,而Alamofire的具體實現去遵守這個協議。 - 好處:換網絡庫時,只改協議的實現類,業務邏輯一行不用動。
2. 單一職責原則(SRP)
- 白話解釋:每個類 / 模塊只做一件事,只對一個變化源負責。
- iOS 場景舉例:
- 錯誤做法:一個
NewsManager既處理網絡請求,又過濾過期新聞,還更新 UI; - 正確做法:
FetchNewsUseCase:只負責 “過濾過期新聞” 的業務邏輯;RemoteNewsRepository:只負責 “從網絡獲取新聞” 的技術實現;NewsViewModel:只負責 “把業務數據轉成 UI 可用格式”。
3. 開閉原則(OCP)
- 白話解釋:對擴展開放,對修改關閉 —— 新增功能時,只加新代碼,不改舊的核心代碼。
- iOS 場景舉例:
- 新增 “從本地緩存獲取新聞” 的功能,不用改
FetchNewsUseCase(核心業務邏輯),只需新增LocalNewsRepository遵守NewsRepository協議,然後把LocalNewsRepository傳給用例層即可。
4. 接口隔離原則(ISP)
- 白話解釋:不要給客户端暴露不需要的接口,每個接口只包含客户端需要的方法。
- iOS 場景舉例:
- 錯誤做法:定義一個超大的
Repository協議,包含 “獲取新聞、登錄、下單、支付” 所有方法; - 正確做法:拆分為
NewsRepository、AuthRepository、OrderRepository等小協議,每個用例只依賴自己需要的協議。
三、Clean Architecture 的分層結構(從內到外)
|
層級(從內到外)
|
名稱
|
核心職責
|
依賴關係
|
iOS 中常見文件 / 模塊
|
|
核心層
|
Entity(實體層)
|
業務模型(和技術無關)
|
無依賴(最內層)
|
|
|
核心層
|
Use Case(用例層)
|
業務邏輯(“做什麼”,比如 “登錄”“獲取新聞”)
|
只依賴 Entity
|
|
|
適配層
|
Interface Adapter
|
適配轉換(核心層 ↔ 外部層)
|
依賴 Core 層的抽象
|
|
|
外部層
|
External
|
技術細節(UI、網絡、存儲、第三方庫)
|
依賴 Adapter 層的抽象
|
|
分層詳解(結合 iOS 實戰)
1. Entity(實體層)—— 業務的 “最小單元”
- 核心邏輯:純業務模型,只描述 “是什麼”,不包含任何技術代碼(無 UIKit、無網絡、無存儲),是整個架構的 “核心中的核心”。
- 代碼示例:
swift
// Core/Entity/News.swift
struct News {
let id: String
let title: String
let content: String
let publishTime: Date
let author: String
// 只包含業務相關的邏輯,比如“是否是熱點新聞”
var isHot: Bool {
return Calendar.current.dateComponents([.hour], from: publishTime, to: Date()).hour! <= 24
}
}
- 關鍵點:這個類不管你是用 UIKit 還是 SwiftUI,不管用 Alamofire 還是 URLSession,都不會變 —— 因為它描述的是 “新聞” 這個業務概念,和技術無關。
2. Use Case(用例層)—— 業務的 “行為邏輯”
- 核心邏輯:定義 “業務要做什麼”,是實體層的 “操作器”,只關心 “做什麼”,不關心 “怎麼做”(比如只規定 “要獲取新聞”,不關心從網絡還是本地獲取)。
- 核心特徵:
- 依賴 Entity 層(因為操作的是業務實體);
- 依賴 “抽象協議”(比如
NewsRepository),不依賴具體實現; - 只包含純業務邏輯,無任何技術細節。
- 代碼示例:
swift
// Core/UseCase/FetchNewsUseCase.swift
// 第一步:定義抽象(規定“要做什麼”)
protocol FetchNewsUseCase {
// 異步獲取新聞,返回符合業務規則的新聞列表
func execute(category: String) async throws -> [News]
}
// 第二步:實現業務邏輯(只關心“做什麼”,不關心“怎麼做”)
class DefaultFetchNewsUseCase: FetchNewsUseCase {
// 依賴抽象,不是具體實現(依賴反轉)
private let newsRepository: NewsRepository
private let userRepository: UserRepository // 比如需要判斷用户是否有權限看某類新聞
// 通過構造函數注入依賴(方便測試時替換為模擬實現)
init(newsRepository: NewsRepository, userRepository: UserRepository) {
self.newsRepository = newsRepository
self.userRepository = userRepository
}
// 核心業務邏輯:
// 1. 檢查用户是否登錄;
// 2. 獲取對應分類的新聞;
// 3. 過濾掉過期且非熱點的新聞;
func execute(category: String) async throws -> [News] {
// 業務規則1:未登錄用户只能看免費新聞
let isLogin = try await userRepository.isUserLogin()
guard isLogin else {
throw BusinessError.notLogin
}
// 調用抽象的Repository,不關心是網絡還是本地獲取
let allNews = try await newsRepository.getNews(category: category)
// 業務規則2:只返回7天內的新聞,或24小時內的熱點新聞
let filteredNews = allNews.filter { news in
let daysSincePublish = Calendar.current.dateComponents([.day], from: news.publishTime, to: Date()).day!
return daysSincePublish <= 7 || news.isHot
}
return filteredNews
}
}
// 自定義業務錯誤(和技術無關)
enum BusinessError: Error {
case notLogin
case invalidCategory
}
3. Interface Adapter(接口適配層)—— “翻譯官”
- 核心邏輯:把核心層的 “業務語言” 和外部層的 “技術語言” 互相翻譯,是核心層和外部層的 “橋樑”。
- 核心職責:
- 代碼示例:
swift
// 第一步:定義抽象協議(核心層依賴的抽象,放在適配層的接口目錄)
// Data/Repository/NewsRepository.swift
protocol NewsRepository {
func getNews(category: String) async throws -> [News]
}
// 第二步:實現網絡版Repository(具體技術細節)
// Data/Repository/RemoteNewsRepository.swift
class RemoteNewsRepository: NewsRepository {
private let networkManager: NetworkManager // 外部層的網絡工具
init(networkManager: NetworkManager) {
self.networkManager = networkManager
}
// 實現抽象方法,處理技術細節+數據轉換
func getNews(category: String) async throws -> [News] {
// 1. 調用網絡工具(技術細節,核心層不知道)
let dtoList: [NewsDTO] = try await networkManager.request(
url: "https://api.news.com/\(category)",
method: .get
)
// 2. 把DTO轉成核心層的News實體(翻譯)
let newsList = dtoList.map { NewsMapper.map(from: $0) }
return newsList
}
}
// 第三步:DTO和Mapper(數據轉換工具)
// Data/DTO/NewsDTO.swift(網絡返回的格式)
struct NewsDTO: Codable {
let news_id: String
let news_title: String
let news_content: String
let publish_time: String // 字符串格式的時間
let author_name: String
}
// Data/Mapper/NewsMapper.swift(翻譯器)
enum NewsMapper {
static func map(from dto: NewsDTO) -> News {
// 把DTO的字段轉成實體的字段,處理格式轉換(比如時間字符串轉Date)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let publishTime = dateFormatter.date(from: dto.publish_time) ?? Date()
return News(
id: dto.news_id,
title: dto.news_title,
content: dto.news_content,
publishTime: publishTime,
author: dto.author_name
)
}
}
4. External(外部層)—— 技術細節層
- 核心邏輯:所有和 “具體技術” 相關的代碼都放在這裏,是架構的 “最外層”,依賴內層的抽象。
- 包含內容:
- UI 層:ViewController、View、ViewModel(MVVM 的 VM 屬於外部層,因為依賴 UIKit);
- 基礎設施層:網絡工具(NetworkManager)、本地存儲(CoreData/Realm)、第三方庫(Alamofire、Kingfisher);
- 組裝層:依賴注入(DI),把所有層的組件組裝起來(比如給 UseCase 傳入 Repository 的實現)。
- 代碼示例(iOS UI 層調用核心層):
swift
// Presentation/ViewModel/NewsViewModel.swift
class NewsViewModel {
// 對外暴露的UI數據(被View訂閲)
@Published var newsList: [News] = []
@Published var isLoading: Bool = false
@Published var error: String?
// 依賴核心層的抽象(不是具體實現)
private let fetchNewsUseCase: FetchNewsUseCase
init(fetchNewsUseCase: FetchNewsUseCase) {
self.fetchNewsUseCase = fetchNewsUseCase
}
// UI觸發的方法(比如用户點擊“獲取科技新聞”)
func loadNews(category: String) {
isLoading = true
error = nil
Task {
do {
// 調用核心層的用例,不關心底層是網絡還是本地
let news = try await fetchNewsUseCase.execute(category: category)
await MainActor.run {
self.newsList = news
self.isLoading = false
}
} catch let businessError as BusinessError {
await MainActor.run {
self.error = businessError.localizedDescription
self.isLoading = false
}
} catch {
await MainActor.run {
self.error = "網絡請求失敗"
self.isLoading = false
}
}
}
}
}
// Presentation/View/NewsVC.swift
class NewsVC: UIViewController {
private let tableView = UITableView()
private let viewModel: NewsViewModel
private var cancellables = Set<AnyCancellable>()
// 依賴注入ViewModel(外部組裝,VC不關心ViewModel的依賴)
init(viewModel: NewsViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
bindViewModel()
// 觸發加載新聞
viewModel.loadNews(category: "technology")
}
// 綁定ViewModel和UI(純UI邏輯)
private func bindViewModel() {
viewModel.$newsList
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.tableView.reloadData()
}
.store(in: &cancellables)
// 監聽加載狀態、錯誤提示(略)
}
private func setupUI() { /* 純UI佈局,略 */ }
}
// 組裝所有組件(DI層,比如AppDelegate/SwiftUI的App入口)
// App/DI/Assembler.swift
enum Assembler {
static func makeNewsVC() -> NewsVC {
// 1. 外部層:創建網絡工具
let networkManager = NetworkManager()
// 2. 適配層:創建Repository實現
let newsRepository = RemoteNewsRepository(networkManager: networkManager)
let userRepository = RemoteUserRepository(networkManager: networkManager)
// 3. 核心層:創建UseCase
let fetchNewsUseCase = DefaultFetchNewsUseCase(
newsRepository: newsRepository,
userRepository: userRepository
)
// 4. 外部層:創建ViewModel
let viewModel = NewsViewModel(fetchNewsUseCase: fetchNewsUseCase)
// 5. 外部層:創建VC
let vc = NewsVC(viewModel: viewModel)
return vc
}
}
四、Clean Architecture 的核心依賴規則(最關鍵)
依賴規則的可視化(洋葱圖)
plaintext
┌─────────────────────────────────┐
│ External Layer │ ← UI、網絡、存儲(依賴內層抽象)
│ (UIKit/SwiftUI、Alamofire、CoreData)│
└─────────────────┬─────────────────┘
│
┌─────────────────▼─────────────────┐
│ Interface Adapter Layer │ ← 適配層(依賴Core層抽象)
│ (Repository、Mapper、DTO) │
└─────────────────┬─────────────────┘
│
┌─────────────────▼─────────────────┐
│ Core Layer │ ← 核心層(無依賴)
│ (Entity:業務模型;UseCase:業務邏輯) │
└─────────────────────────────────┘
五、Clean Architecture 在 iOS 中的落地價值
- 業務工程師:只寫 Core 層的 Entity 和 UseCase,不用關心技術細節;
- 技術工程師:只寫適配層和外部層的實現,不用懂業務邏輯;
- UI 工程師:只寫 Presentation 層的 UI,不用關心數據從哪來;