你常會在徵才公告或技術部落格中看到「以 Clean Architecture 為基礎」這句話。但實際打開程式碼後,會發現每個團隊的樣貌都不一樣。有些地方有 UseCase,有些沒有,Repository 的職責也各不相同。
之所以混亂,原因很簡單:Clean Architecture 不是特定的資料夾結構或類別清單,而是一項原則。
今天就來整理這項原則的內容,以及在 iOS 中以 UseCase 和 Repository 形式實作時,各自負責什麼。
核心是「相依性只能向內」
Clean Architecture 是 Robert Martin(Uncle Bob)整理出的概念,將 App 視為同心圓式的分層結構。
- 最內層:網域。實體與商業規則。「這個 App 是做什麼的?」
- 中間層:UseCase。App 的行為情境
- 最外層:UI、資料庫、網路、框架
規則只有一條。相依性永遠只能由外向內。
內層的網域程式碼不應知道外層的 UIKit、URLSession 或 SwiftData。反過來,外層可以知道內層。如此一來,不論替換 UI 框架或變更伺服器 API,都不必修改 App 的核心規則。
你可能已經注意到,這是把 DIP(相依性反轉原則)擴展到整個 App 的規模,也就是以分層為單位套用「依賴抽象,而非具體實作」。
Repository:隱藏「資料來自哪裡」的邊界
Repository 是網域與資料來源之間的邊界。將協定放在網域側,實作放在外層。
// 網域層 — URLSession什麼都 SwiftData不知道
protocol PostRepository {
func fetchPosts(userID: String) async throws -> [Post]
}
// 資料層 — 實作放在外層
final class RemotePostRepository: PostRepository {
func fetchPosts(userID: String) async throws -> [Post] {
// URLSession 呼叫, DTO 解碼, Post轉換
}
}
重點在於協定的位置。介面由網域擁有,因此網域只知道「可以取得文章」,不知道資料來自伺服器、快取還是本機資料庫。伺服器回應格式變更時,只要修改 DTO(Data Transfer Object,承載伺服器回應的資料傳輸物件)與實作即可;測試時則可插入假的 Repository。
UseCase:承載「這個 App 要做的事」之一的容器
UseCase 是將一個 App 行為情境化為物件,例如「載入動態」或「發佈文章」。
final class LoadFeedUseCase {
private let postRepository: PostRepository
private let blockRepository: BlockRepository
func execute(userID: String) async throws -> [Post] {
let posts = try await postRepository.fetchPosts(userID: userID)
let blocked = try await blockRepository.blockedUserIDs()
return posts
.filter { !blocked.contains($0.authorID) }
.sorted { $0.createdAt > $1.createdAt }
}
}
「將封鎖使用者的文章從動態中排除」是放在 UseCase 中的商業規則。如果放進 ViewModel 會怎樣?動態、個人檔案和搜尋畫面都會各自實作封鎖篩選,最後一定會有一處出現偏差。抽到 UseCase 後,規則集中在一處,也能不依賴畫面進行測試。
因此,MVVM(Model-View-ViewModel)和 Clean Architecture 並非競爭關係,而是正交關係。MVVM 整理 View 端,Clean Architecture 整理其後的分層。上一篇提到「要避免 Massive ViewModel,就把邏輯往下移」,那個承接邏輯的位置就是 UseCase 和 Repository。
應該導入到什麼程度?
Clean Architecture 的陷阱是過度導入。兩個畫面的 App 若完整配置 UseCase、Repository、DTO、Mapper,傳遞一個值就得經過五個檔案,VIPER(View・Interactor・Presenter・Entity・Router)的樣板程式碼問題也會原樣重現。
實際上的判斷標準如下。
- **Repository 幾乎總是有益。**只要將網路與 DB 程式碼從畫面中分離,測試與變更就會更容易。
- **當多個畫面開始共用商業規則時,再導入 UseCase。**如果大多數 UseCase 只是原封不動轉呼叫 Repository,現在還太早。
- **依分層拆分模型(DTO/網域/畫面模型)應等規模變大後再做。**一開始就全部拆開,只會不斷累積 Mapper 程式碼。
總結
- Clean Architecture 不是資料夾結構,而是「相依性只能向內(網域)」的原則,也是 DIP 的 App 規模擴展版。
- Repository 是隱藏資料來源的邊界,核心在於協定由網域擁有。
- UseCase 是放置多個畫面共用商業規則的地方,與 MVVM 正交,因此可以一起使用。
- 目標不是全部備齊。從 Repository 開始,等規則累積後再加入 UseCase。
下一篇將轉換方向,介紹把狀態管理本身做成架構的 TCA(The Composable Architecture)。

![[iOS 架構 #6] Clean Architecture 核心 封面圖](/assets/images/posts/4a332cf7-d161-401b-ab7c-f15324e48609/1.jpg)