Woody Liu
6 min readAug 25, 2022

MVVM & Redux _ Page 1.

在開始 Review 程式碼 之前,先來討論一下架構,也就是今天的主題。
在了解架構時不得就不先提一下 設計模式

設計模式
一種解決不同情況的方案
在物件導向中就是不同物件之間的相互關係的設計

所以不同的設計模式有可能物件之間的相互關係設計是相同的,差別在於所解決的問題。(註一)

架構
為了解決所有情況的問題產生的一種方案。
為了解決 所有情況 , 由複數的設計模式組合成的。

  • 一個DB系統的架構
  • 前端的 MVC、MVP、MVVM
  • AVPlayer、UITableView 都有應用不同的架構
flowchart LR

DeP(Design Pattern)
Solution-->DeP
flowchart 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

下一章:引入 Environment && Action Composable