一、先明確:Clean Architecture 的核心目標

在講具體邏輯前,先搞懂它解決什麼問題:

  • 傳統架構(比如混亂的 MVC)中,業務邏輯會和 UI、網絡、第三方庫深度耦合(比如 Controller 裏既寫登錄邏輯,又調 Alamofire,還處理 UI 刷新),導致:
  1. 改 UI 框架(比如從 UIKit 轉 SwiftUI)要動業務邏輯;
  2. 換網絡庫(比如從 Alamofire 換 URLSession)要改核心代碼;
  3. 單元測試難寫(必須啓動 App、依賴網絡 / 本地存儲);
  4. 多人協作時,改 A 的代碼容易影響 B 的功能。

Clean Architecture 的核心目標:讓業務邏輯(核心)不依賴任何技術細節(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 協議,包含 “獲取新聞、登錄、下單、支付” 所有方法;
  • 正確做法:拆分為 NewsRepositoryAuthRepositoryOrderRepository 等小協議,每個用例只依賴自己需要的協議。

三、Clean Architecture 的分層結構(從內到外)

Clean Architecture 最經典的是 “洋葱架構”(分層像洋葱,核心在內層),iOS 中通常簡化為 4 層,內層絕對不依賴外層,外層只能依賴內層的抽象

層級(從內到外)

名稱

核心職責

依賴關係

iOS 中常見文件 / 模塊

核心層

Entity(實體層)

業務模型(和技術無關)

無依賴(最內層)

Core/Entity/News.swiftUser.swift

核心層

Use Case(用例層)

業務邏輯(“做什麼”,比如 “登錄”“獲取新聞”)

只依賴 Entity

Core/UseCase/FetchNewsUseCase.swift

適配層

Interface Adapter

適配轉換(核心層 ↔ 外部層)

依賴 Core 層的抽象

Data/Repository/NewsRepository.swiftMapper/NewsMapper.swift

外部層

External

技術細節(UI、網絡、存儲、第三方庫)

依賴 Adapter 層的抽象

Presentation/View/NewsVC.swiftInfrastructure/Network/NetworkManager.swift

分層詳解(結合 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(接口適配層)—— “翻譯官”
  • 核心邏輯:把核心層的 “業務語言” 和外部層的 “技術語言” 互相翻譯,是核心層和外部層的 “橋樑”。
  • 核心職責
  1. 數據轉換:比如把網絡返回的 NewsDTO(DTO:數據傳輸對象)轉成核心層的 News 實體;
  2. 實現抽象:核心層的 NewsRepository 協議,由這一層的 RemoteNewsRepository/LocalNewsRepository 實現;
  3. 隔離技術:核心層完全不知道 “網絡”“存儲” 是什麼,所有技術細節都被這一層包裹。
  • 代碼示例
    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 的核心依賴規則(最關鍵)

整個架構的 “靈魂” 是單向依賴—— 所有依賴都必須從 “外層” 指向 “內層”,內層絕對不能依賴外層,總結為 3 條規則:

  1. 內層不感知外層:Core 層(Entity+UseCase)不知道有 UI、網絡、存儲這些東西,只關心業務;
  2. 依賴抽象,而非具體:外層的具體實現(比如 RemoteNewsRepository)依賴內層的抽象(NewsRepository 協議),而非反過來;
  3. 技術細節是 “插件”:UI、網絡、存儲等技術細節,都是可以隨時替換的 “插件”,不影響核心業務邏輯。

依賴規則的可視化(洋葱圖)

plaintext

┌─────────────────────────────────┐
│           External Layer         │ ← UI、網絡、存儲(依賴內層抽象)
│ (UIKit/SwiftUI、Alamofire、CoreData)│
└─────────────────┬─────────────────┘
                  │
┌─────────────────▼─────────────────┐
│    Interface Adapter Layer       │ ← 適配層(依賴Core層抽象)
│ (Repository、Mapper、DTO)       │
└─────────────────┬─────────────────┘
                  │
┌─────────────────▼─────────────────┐
│           Core Layer             │ ← 核心層(無依賴)
│ (Entity:業務模型;UseCase:業務邏輯) │
└─────────────────────────────────┘

五、Clean Architecture 在 iOS 中的落地價值

  1. 極致可維護性:比如要把 UIKit 換成 SwiftUI,只需要改 Presentation 層,Core 層一行不用動;
  2. 高可測試性:Core 層的 UseCase 可以脱離 App 單獨寫單元測試(比如模擬 Repository 返回假數據,測試業務邏輯是否正確);
  3. 團隊協作高效
  • 業務工程師:只寫 Core 層的 Entity 和 UseCase,不用關心技術細節;
  • 技術工程師:只寫適配層和外部層的實現,不用懂業務邏輯;
  • UI 工程師:只寫 Presentation 層的 UI,不用關心數據從哪來;
  1. 抗技術迭代:網絡庫從 Alamofire 換成 URLSession、存儲從 CoreData 換成 Realm,只改適配層的實現類,核心業務邏輯不受影響。

總結

Clean Architecture 的核心邏輯可以濃縮為 3 句話:

  1. 核心不變:業務邏輯(Entity+UseCase)是架構的核心,不依賴任何技術細節;
  2. 依賴單向:所有依賴從外層指向內層,內層不感知外層;
  3. 抽象隔離:技術細節通過抽象協議被核心層調用,實現 “業務和技術解耦”。

對 iOS 開發者來説,不用一開始就追求 “完美的 Clean Architecture”,可以先從 “MVVM+Repository 模式” 入手(把網絡 / 存儲封裝成 Repository,ViewModel 依賴 Repository 抽象),再逐步過渡到完整的 Clean Architecture—— 核心是理解 “業務和技術分離” 的思想,而非生搬硬套分層。