你應該至少看過一次這種程式碼:所有內容都塞在單一個 800 行的檢視檔案裡。
繪製畫面的檢視程式碼裡有 API 呼叫,旁邊是折扣率計算,下面是日期格式化函式,中間還夾著分析事件傳送。想修改任何一項,都得讀完全部 800 行。
這類檔案違反的原則就是關注點分離(Separation of Concerns)。開發原則系列的最後一篇,談的就是這項原則。
關注點分離(SoC)是指將程式分成不同的關注點單位,讓每個部分只處理一個關注點。
「關注點」是程式需要處理的工作類型。如何呈現畫面、從哪裡取得資料,以及如何套用商業規則,都是不同的關注點。
這個術語由電腦科學家 Dijkstra 創造,他在 1974 年的文章中如此描述。
一次只專注思考一個面向。這是我所知道唯一有效的思考技巧。
人的大腦無法一次處理多個關注點。因此,程式碼也應該讓每個片段只包含一個關注點。
從熟悉的案例開始:HTML、CSS、JS
如果你做過網頁開發,就代表你早已每天使用關注點分離。
| 技術 | 關注點 |
|---|---|
| HTML | 結構 — 有什麼 |
| CSS | 呈現 — 看起來如何 |
| JavaScript | 行為 — 如何回應 |
如果像以前一樣在 HTML 標籤中堆滿 style 屬性和 onclick,連修改按鈕顏色都得翻遍 HTML。拆成三個檔案後,設計師只需看 CSS,負責標記的人只需看 HTML。
後端的控制器-服務-儲存庫結構,以及 iOS 將檢視、檢視模型和資料層分開,也都是相同的原理。MVC 和分層架構,歸根結柢都是將關注點分離形式化的模式。
來解剖剛才那個 800 行檢視
我們來按照關注點拆分前面看到的 800 行檢視。
// Before: 所有關注點都在同一個檔案中
struct ProductPage: View {
// 關注點 1: 取得資料(網路呼叫、載入、錯誤處理 150 行)
// 關注點 2: 商業規則(折扣率・庫存計算 200 行)
// 關注點 3: 分析事件(點選追蹤 100 行)
// 關注點 4: 繪製畫面 (body 350 行)
}
// After: 讓每個關注點都有自己的位置
ProductStore(id:) // 取得資料
calcDiscount(product, user) // 商業規則(純函式)
Tracker.track("product") // 分析事件
struct ProductPage: View { // 只繪製畫面
@State private var store: ProductStore
var body: some View {
let price = calcDiscount(store.product, user)
...
}
}
拆開後,優點很明顯。折扣政策變更時,只要看 calcDiscount;而且這個函式與畫面無關,是純函式,因此也容易測試。API 規格變更時,只要修改 ProductStore。和必須讀完 800 行相比,修改範圍縮小為十分之一。
應該拆分到什麼程度?
關注點分離最難的是「那麼,邊界要畫在哪裡?」拆得太細,就會變成必須在數十個檔案間來回閱讀的迷宮。
我的判斷基準是變更的原因。這和 SOLID 篇中討論 SRP 時的問題完全相同。
這兩段程式碼會因為相同原因、在相同時間變更嗎?
折扣政策和畫面版面配置會因不同原因變更,因此要分開。相反地,按鈕和它的點選處理器幾乎總是一起變更,所以放在一起。從這個角度看,SwiftUI 以檢視為單位,將結構、樣式和行為集中在同一個檔案中,並不矛盾。它是按照會一起變更的單位,而不是技術種類,重新劃分關注點。
系列完結
關注點分離最重要的核心是這個。
- 讓一段程式碼只處理一個關注點。人的大腦無法多工。
- 實務上,應該以「一起變更的單位」而不是技術種類來劃定邊界。
- 過度分離會製造迷宮。拆分本身不能成為目的。
開發原則系列到此告一段落:KISS(保持簡單)、DRY(知識集中在一處)、YAGNI(只做現在需要的事)、SOLID(縮小變更的影響範圍)、最小驚訝(讓行為符合預期),以及關注點分離(一次只處理一件事)。
回頭看,六項原則全都指向同一件事:程式碼是給人讀的,不是給電腦讀的;好的程式碼,就是容易修改的程式碼。即使忘了原則名稱,也請只留下這句話。

