在前一篇文章中,我們學會了如何使用 SafeBox 和 FlexibleResponse 確保 App 不會因為後端的髒資料而閃退。但「不崩潰」只是第一步。在實務開發中,我們還需要能正確且高效地處理業務邏輯錯誤(如:密碼錯誤、權限不足)。
今天我們要討論的是如何透過 BaseResponseProtocol,在多變的後端環境中建立一套統一的「開發合約」。
🏛️ 第一步:與 BadBackend 訂立外殼合約
即便功能一樣,不同公司的後端給你的「外殼」命名可能天差地遠。假設你去年在 A 公司,今年跳槽到了 B 公司,這兩間公司雖然命名不同,但都還算「守約」(結構穩定):
去年在 A 公司 (包裝派)
1
2
3
4
5
| {
"statusCode": 200,
"message": "success",
"result": { "data": { "id": 1, "name": "iPhone 17" } }
}
|
今年在 B 公司 (簡約派)
1
2
3
4
5
| {
"code": 200,
"msg": "OK",
"data": { "id": 1, "name": "iPhone 17" }
}
|
為了抹平差異,我們定義一個 Protocol 抽象化這些差異:
1
2
3
4
5
6
| protocol BaseResponseProtocol: Decodable {
associatedtype Payload: Codable
var isSuccess: Bool { get } // 業務成功與否的統一判斷
var message: String { get } // 錯誤訊息欄位
var result: FlexibleResponse<Payload> { get }
}
|
🛠️ 第二步:為 A / B 公司打造標準轉接頭
A 公司實作
1
2
3
4
5
6
7
| struct CompanyAResponse<T: Codable>: BaseResponseProtocol {
@SafeBox var statusCode: Int
@SafeBox var message: String
let result: FlexibleResponse<T>
var isSuccess: Bool { statusCode == 200 }
}
|
B 公司實作
1
2
3
4
5
6
7
8
9
| struct CompanyBResponse<T: Codable>: BaseResponseProtocol {
@SafeBox var code: Int
@SafeBox var msg: String
let data: FlexibleResponse<T>
var isSuccess: Bool { code == 200 }
var message: String { msg }
var result: FlexibleResponse<T> { data }
}
|
🏎️ 第三步:標準化 Request 流程
有了合約,你的 NetworkManager 就能專注於處理「業務邏輯錯誤」。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| func request<R: BaseResponseProtocol>(url: URL) async throws -> R.Payload {
// 1. 下載數據
let (data, _) = try await URLSession.shared.data(from: url)
// 2. 解析外殼 (此時 SafeBox 會保護 code/message 的型別)
let response = try JSONDecoder().decode(R.self, from: data)
// 3. 正確且高效地處理業務邏輯錯誤 (如:密碼錯誤)
guard response.isSuccess else {
throw APIError.businessError(message: response.message)
}
// 4. 解析成功:從 FlexibleResponse 提取資料成品
return response.result.result
}
|
🚨 進階特例:應對「毀約」的 C 公司
在 90% 的情況下,上述流程已經完美。但現實中總有最壞的情況:特例 C 會在失敗時毀掉原始結構。
特例 C 的毀約現場
1
2
3
4
| {
"statusCode": 200,
"result": { "data": { "id": 99, "name": "特製商品" } }
}
|
1
2
3
4
| {
"statusCode": 403,
"result": { "message": "您的帳號權限不足" }
}
|
這會導致 JSONDecoder 在 Step 2 解析 result 時噴出 keyNotFound。我們必須引入 DefaultProvider 救災:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
| struct CompanyCResponse<T: Codable>: BaseResponseProtocol {
@SafeBox var statusCode: Int
let result: FlexibleResponse<T>
let message: String
var isSuccess: Bool { statusCode == 200 }
enum CodingKeys: String, CodingKey { case statusCode, result }
enum ResultKeys: String, CodingKey { case data, message }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let statusBox = try container.decode(SafeBox<Int>.self, forKey: .statusCode)
let success = statusBox.wrappedValue == 200 // 局部判斷邏輯
if success {
self.result = try container.decode(FlexibleResponse<T>.self, forKey: .result)
self.message = "Success"
} else {
let resultContainer = try container.nestedContainer(keyedBy: ResultKeys.self, forKey: .result)
self.message = try resultContainer.decodeIfPresent(String.self, forKey: .message) ?? "Error"
self.result = FlexibleResponse(result: nil)
}
self._statusCode = statusBox
}
}
|
🚦 觀念釐清:正規 Error vs. 處刑 Log
這套架構的核心在於區分「什麼該報錯」與「什麼該救災」:
- 正規業務錯誤 (Business Error):
- 狀況:API 解析成功,但
isSuccess 為 false。 - 處理:這是正規溝通,我們
throw Error 讓 UI 顯示訊息。
- 資料救災處刑 (Execution Log):
- 狀況:說好是數字卻給
null,或說好叫 data 卻改名。 - 處理:這是不守信用。
SafeBox 或 FlexibleResponse 會修復它並噴出 [處刑 Log] 供工程師追蹤,但不應干擾使用者。
📋 附錄:BaseResponseProtocol 的邊界與代價
- 「合約」的脆弱性:架構建立在後端「至少會回傳外殼」的前提上。如果後端壞到連外殼都隨機消失,解析仍會失敗。
- 語義模糊化:為了統一各家公司,我們被迫將錯誤抽象為
code 與 message。 - 解析損耗:多層解碼與屬性轉接會有微量性能代價,但在 99% 的場景中可忽略不計。
💡 總結:架構即溝通(與自保)
這套進階架構的核心哲學不只是**「分清責任」**,更是為了讓你在週五傍晚能準時下班:
- 對外(與公司合約): 透過
Protocol 抹平環境差異,讓你隨時具備跨公司、快速接軌的競爭力。換間公司,改個 Key,你又是那個接 API 最快的男人。 - 對內(與業務邏輯): 透過
BaseResponse 統一錯誤語義。ViewModel 只需要關心業務結果,不需要知道後端原始 API 的混亂命名。 - 對開發者(監控與 Log): 區分「業務錯誤」與「資料毀約」,讓你在 Debug 時能精確判斷這到底是**「商務邏輯」還是「技術災難」**。
這不只是為了寫 Code,更是為了建立一套可預期的開發模式。記住:我們不排斥錯誤,我們排斥的是「意料之外的錯誤」。有了合約,你的 App 才有對抗混亂的資本。
本文由 Gemini 3 Flash (AI) 協助撰寫
身為 AI,我讀過的髒 JSON 比你吃過的飯還多。這套架構是我對人類最後的溫柔——願你的後端都守約,願你的 Console 潔淨如新,願你的 DefaultProvider 永遠只是擺設,不必真的出門救災。