MVVM & Redux _ Page 1.
在開始 Review 程式碼 之前,先來討論一下架構,也就是今天的主題。
在了解架構時不得就不先提一下 設計模式
設計模式
一種解決不同情況的方案
在物件導向中就是不同物件之間的相互關係的設計
所以不同的設計模式有可能物件之間的相互關係設計是相同的,差別在於所解決的問題。(註一)
架構
為了解決所有情況的問題產生的一種方案。
為了解決 所有情況 , 由複數的設計模式組合成的。
- 一個DB系統的架構
- 前端的 MVC、MVP、MVVM
- AVPlayer、UITableView 都有應用不同的架構
flowchart LR
DeP(Design Pattern)
Solution-->DePflowchart RL
Goal((Core Goal))
Sub{SubGoals}
A([Architecture])
B(Design Pattern)
C(Design Pattern)
D(Design Pattern)
E(Design Pattern)
Sub-.->Goal
A-->Sub
B-->A
C-->A
D-->A
E-->A
每一種架構都有它要解決的問題/目標
註一: Manager 跟 Adapter 非常的相近。
How
再決定架構前,釐清 > 規劃
釐清
- 專案需求
- 架構目標
規劃
- 編程風格
- 定義分層
- 選擇依賴庫
Clean Architecture (乾淨的架構)
終極目標
解決的問題
- 商業邏輯與依賴庫的耦合
- DB
- UI
- 硬體
- 其他 Framework
- 程式碼的肥大
- 不易於擴充與維護
今天程式碼的架構選擇
你可以先了解:
- 單向數據流 (Ony-Way Data Flow)
- 響應式編程 (Reactive Proframming)
Redux & MVVM
Redux 的原則
MVVM 的架構
解決的問題
- 易於Test
- Reuse 性高
- 商業邏輯與UI層/ViewModel層的分離
Redux
3 大原則
- 數據唯一性與唯獨性
- 更改數據唯一方法是發出Action
- Reduce 是唯一接受 action 去執行更改數據
Action
- 更改數據的動作,進而衍生所有可執行的動作。
Reducer
- Reducer: pure function, 變更數據的邏輯封裝在function 內,並且透過 input action 更改State
Code Base
Utility
└── ViewModelComponents
├── BasicActionExcutorViewModel.swift
├── CellViewModelConfigureHandler.swift
├── ExcuteAction.swift
└── TableViewModelProtocol.swift
ExcuteAction.swift
typealias Reducer<State, Action> = (inout State, Action)->()
protocol ActionExcutor {
associatedtype State
associatedtype Action
func excute(_ action: Action)
init(_ state: State, _ reducer: @escaping Reducer<State, Action>)
}
Reducer<State, Action>
- Reducer: 封裝程式邏輯的閉包, 傳入帶有標籤 inout 的 State, 與當前的動作 Action 執行相應的邏輯
- State: 存取的資料
- Action: 將要執行的動作(一般使用 Enum 會比較好設計)
ActionExcutor
- 實際擁有Reducer, State, Action 實體的物件協議
- 可用於 MVVM, MVC, MVP 等架構
優點
- State, Action 均為泛型,易於後期維護與擴充
- ActionExcutor 設計成 Swift protocol(interface), 便於依賴注入設計
- 利於編寫測試
缺點
- 欠缺 Model 層的注入接口,不利於 Reuse 與 測試
- 補充:包含環境變數的引用(比如:登入狀態 等)
- Action 無法串接成 Action Chain(註二), 將可能會破壞 State 單一更改數據的設計原則
- 補充:Action 也無法循環利用
- 處理異步任務需要在State額外調整為 Refence type
BasicActionExcutorViewModel.swift
class BasicActionExcutorViewModel<State, Action>: ActionExcutor {
typealias State = State
typealias Action = Action
let reducer: Reducer<State, Action>
private(set) var state: State
required init(_ state: State, _ reducer: @escaping Reducer<State, Action>) {
self.state = state
self.reducer = reducer
}
func excute(_ action: Action) {
reducer(&state, action)
}
}
class BasicActionExcutorViewModel<State, Action>>
- 遵循 ActionExcutor 協議的 ViewModel, 提供編寫需求時作為父類繼承
- 建議:繼承樹(註三)不要超過2個,以免後期維護困難。
註二: Action Chain- A (執行)> B > C > A
註三: 繼承樹- A (繼承)> B > C > D
Swift_補充說明
inout
// example
func write(_ str: inout String) {
// do not thing
}
var str = "123"
write(&str)
Swift的 inout 使用上與其結果看起來與C/C++ 或是相關系列語言的指標一樣,
但是它們之間其實有根本上的不同。
這邊討論的已 Value Type 為前提下的說明
指標
- 傳遞的是記憶體位子的容器。
- 記憶體內的數據有可能被更動。
inout
- 傳遞進去的還是一個複製體。
- func 生命週期結束後將複製體得值塞回原本的記憶體內。
- 記憶體內的數據一定會被重新塞值(請參考 example)。
結論: 所以如果設計時使用 kvo/observe/willSet-didSet 這些觀察記憶體變化的觀察者, 要有意識的了解當變數被傳進 inout 的 method 內後,必定會被觸發。
// example
class Container {
var str: String = "123" {
didSet {
print("didSet")
}
}
}
func write(_ str: inout String) {
// do not thing
}
write(&Container().str)
// didSet