iOS 工程

[iOS 架構 #7] TCA 與單向資料流

在 SwiftUI 時代的架構討論中,有一個名字總是無法避開:Point-Free 打造的 TCA(The Composable Architecture)。

閱讀 4 分鐘
[iOS 架構 #7] TCA 與單向資料流 封面圖

在 SwiftUI 時代的架構討論中,有一個名字總是無法避開:Point-Free 打造的 TCA(The Composable Architecture)

如果前幾篇介紹的 MVVM(Model-View-ViewModel)與 Clean Architecture 討論的是「該如何劃分職責」,那麼 TCA 提出的問題就不同了。「能不能用一套規則控制狀態變更的所有路徑?」

今天就來整理 TCA 的單向資料流如何運作,以及我們能得到什麼、又要付出什麼代價。


三種材料:State、Action、Reducer

在 TCA 中,一個畫面(或一項功能)由三個部分定義。

  • State:保存這項功能所有狀態的單一結構
  • Action:包含這項功能可能發生之所有事件的 enum,從按鈕點擊、回應到達,到收到通知,全都包含在內
  • Reducer:定義「目前的 State 收到這個 Action 時,State 會如何變更」的純函式
@Reducer
struct Counter {
    @ObservableState
    struct State: Equatable {
        var count = 0
    }
    enum Action {
        case incrementTapped
        case decrementTapped
    }
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .incrementTapped:
                state.count += 1
                return .none
            case .decrementTapped:
                state.count -= 1
                return .none
            }
        }
    }
}

View 無法直接變更狀態,能做的只有向 Store 傳送 Action。Store 執行 Reducer 並變更 State 後,View 便會繪製更新後的 State。

사건 발생 → Action 전송 → Reducer가 State 변경 → View 갱신

這個單向循環就是全部。狀態沒有其他偷偷變更的路徑。「這個值到底在哪裡變的?」這種除錯迷宮會從結構上消失。


連副作用也納入規則

網路請求等非同步工作不由 Reducer 直接執行,而是以 Effect 回傳。Effect 完成後,結果會再次以 Action 的形式回來。

case .refreshTapped:
    state.isLoading = true
    return .run { send in
        let posts = try await postClient.fetch()
        await send(.postsLoaded(posts))
    }

case .postsLoaded(let posts):
    state.isLoading = false
    state.posts = posts
    return .none

postClient 等相依性會透過 TCA 的相依性注入系統插入,因此測試時可以替換成假的 client。TCA 最強大的武器就在這裡出現了:TestStore

let store = TestStore(initialState: Feed.State()) { Feed() }
await store.send(.refreshTapped) { $0.isLoading = true }
await store.receive(.postsLoaded(mockPosts)) {
    $0.isLoading = false
    $0.posts = mockPosts
}

它會強制你完整明確定義每個 Action 對 State 應造成的變更。只要出現一個未預期的狀態變更,測試就會失敗。這種程度的完整性驗證,很難用 MVVM 測試來模仿。

TestStore 會強制你完整明確地定義每一次狀態變更
TestStore 會強制你完整明確地定義每一次狀態變更

代價是什麼?

這份精準度也伴隨著帳單。

**學習曲線很陡。**在開始產生生產力之前,必須先學會 Reducer、Effect、Store、Scope 與相依性系統等許多概念。對沒有函數式程式設計背景的團隊而言更是如此。

**存在樣板程式碼。**即使只改一個值,也得建立 Action case,並在 Reducer 中新增分支。對簡單畫面來說,這種儀式感過於繁重。

**它是第三方相依套件。**TCA 並非由 Apple,而是由 Point-Free 打造的函式庫。為了配合每年變動的 SwiftUI,TCA 也經歷過多次大型遷移,應用程式的所有畫面最後都會建立在這個函式庫之上。它是受到積極維護且成熟的專案,但決定把應用程式的骨架交給外部套件,並不是一個可以輕率做出的決定。


它適合什麼樣的團隊?

TCA 能發揮價值的條件相當明確。

  • 狀態錯綜複雜的應用程式,例如協作文件、金融、複雜表單與即時同步
  • 狀態變更的完整測試十分重要的領域
  • 熟悉函數式概念,或能投入學習成本的團隊

相反地,如果應用程式大多數畫面只是「接收後顯示」,那麼第 3 篇介紹的 MV(Model-View)/MVVM 加上 UseCase,通常就已經足夠。TCA 不是錯誤的選擇,而是昂貴的選擇;只有當狀態複雜度足以合理化這項成本時,它才會真正發揮價值。

TCA 不是錯誤的選擇,而是昂貴的選擇
TCA 不是錯誤的選擇,而是昂貴的選擇

總結

  • TCA 以 State、Action、Reducer 定義功能,並強制所有狀態變更只能沿著單一單向循環進行。
  • 副作用也會透過 Effect → Action 納入規則,TestStore 則驗證狀態變更的完整性。
  • 代價是學習曲線、樣板程式碼,以及把應用程式骨架交給第三方的決定。
  • 這是一種取捨明確的架構:應用程式的狀態越複雜,TCA 帶來的效益就越大。

系列只剩最後一個問題。從 MVC 到 TCA,在這麼多選項中,**我們的團隊該選哪一個?**下一篇將整理選擇標準,為系列畫下句點。

延伸閱讀