有一定的程式設計經驗之後,會愈來愈感受到程式架構的重要性,在 iOS app 開發的世界裡,最常見的莫過於 MVC 架構,因為它夠簡單而且是蘋果推薦的架構。但當你的程式越來越龐大,流程越來越複雜的時候,就會發現 MVC 架構已經無法滿足需求了。這幾年最為人所知的就是 MVP / MVVM / VIPER / Coordinator 這幾個模式。
我認為這些模式的著眼點都在於「UI」:它們假設你有一套辦法去存取或修改資料,然後它們提出的方案是關於如何處理「界面顯示 / 使用者互動 / 資料存取」之間的關係。
當程式越長越大,要儲存的狀態越來越多,不同畫面之間需要同步的資料也越來越多,我們該如何管理資料的存取、確保其正確性呢?Facebook 之前提出了 Flux 架構,後來有人提出改良版的 Redux 架構,不管是 Flux 還是 Redux,其重點都是在於「 資料的流動是單向的,資料只有一份,並且只有一個角色可以修改資料 」。
Flux / Redux 一開始提出是給網站使用的架構,後來有人把它套用到 iOS 開發,不過我查到的資料都是使用 Swift 實作。無可否認使用 Swift 來實作這套架構的確比較方便,只是我很好奇用 Objective-C 的話會有多困難,以下就是我的一些開發過程。
從上圖可以看到,Redux 架構很簡單,只有四個角色:
現在我們要來寫一個很簡單的 app,它唯一的功能就是跟伺服器要求最新的文章列表,然後一筆筆顯示處理。假設我們的網路功能跟 UI 都設計好了,那該怎麼套用 Redux 架構來處理資料的部分呢?
我會建議一開始由 Action 先規劃,Action 很單純,就是用一個 property 來記錄 action type,再用一個 property 來記錄 payload。因為有些 type 不需要附帶資料,所以 payload 是 nullable。這裡我規劃了兩個 type,第一個是取得文章列表之後我需要 SetPosts
來更新 State 裡頭的文章列表,第二個是 AppendPosts
,當我取得下一頁的文章列表之後我要把它附加到 State 原有的列表裡。
/// TLBAction.h typedef NS_ENUM (NSInteger, TLBActionType) { TLBActionTypeSetPosts, TLBActionTypeAppendPosts, }; @interface TLBAction : NSObject @property (nonatomic, assign, readonly) TLBActionType type; @property (nonatomic, strong, readonly, nullable) id payload; - (instancetype)initWithActionType:(TLBActionType)type payload:(nullable id)payload; @end /// TLBAction.m @interface TLBAction () @property (nonatomic, assign, readwrite) TLBActionType type; @property (nonatomic, strong, readwrite, nullable) id payload; @end @implementation TLBAction - (instancetype)initWithActionType:(TLBActionType)type payload:(id)payload { if (self = [super init]) { _type = type; _payload = payload; } return self; } @end
State 沒什麼好說的,就是一個單純的資料結構,用來儲存會用到的資料。值得一提的是,只要存原始資料就好,可以藉由原始資料推算出的資料不需要存起來。
@interface TLBState : NSObject <NSCopying> @property (nonatomic, strong) NSOrderedSet <NSString *> *posts; @end
Reducer 是唯一知道該怎麼修改 State 的地方,一個 Reducer 可能只會修改 State 的某一部分。當 Action 越來越多、State 越來越大的時候,也可以將多個 Reducer 合成一個更大的 Reducer。要注意的是,你不應該預期 Reducer 會以怎樣的順序被呼叫,它應該是一個 pure function。
/// TLBReducer.h typedef void (^TLBReduceBlock)(TLBState **, TLBAction *); @interface TLBReducer : NSObject + (NSArray *)availableReduceBlocks; @end /// TLBReducer.m @implementation TLBReducer + (NSArray *)availableReduceBlocks { return @[ [self postActionsReducer] ]; } + (TLBReduceBlock)postActionsReducer { TLBReduceBlock block = ^(TLBState **state, TLBAction *action) { if (state == NULL) { return; } TLBState *newState = *state; switch (action.type) { case TLBActionTypeSetPosts: { newState.posts = [NSOrderedSet orderedSetWithArray:action.payload]; break; } case TLBActionTypeAppendPosts: { NSMutableOrderedSet *set = [newState.posts mutableCopy]; [set addObjectsFromArray:action.payload]; newState.posts = [set copy]; break; } default: { break; } } }; return block; } @end
一個 app 只會有一個 Store,所以它會是一個 singleton。外界會要求它去 dispatch 一個 action,它就會讓全部的 Reducer 依序處理這個 action,並且為了確保一次只有一個 Action 被執行,所以我建立了一個 serial queue 來處理。最後把處理過的結果寫回 State,並通知感興趣的人 State 已更新。通知有很多種實作方式,在這裡我是用 ReactiveCocoa
的 RACSignal
。
/// TLBStore.h @interface TLBStore : NSObject @property (nonatomic, strong, readonly) RACSignal *stateObserver; + (instancetype)shardInstance; - (void)dispatchAction:(TLBAction *)action; @end /// TLBStore.m @interface TLBStore () @property (nonatomic, strong, readwrite) RACSignal *stateObserver; @property (nonatomic, strong) TLBState *state; @property (nonatomic, strong) NSArray <TLBReduceBlock> *reducers; @property (nonatomic, strong) dispatch_queue_t serialQueue; @end @implementation TLBStore + (instancetype)shardInstance { static TLBStore *_sharedInstance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _sharedInstance = [[TLBStore alloc] init]; }); return _sharedInstance; } - (instancetype)init { if (self = [super init]) { _serialQueue = dispatch_queue_create("Redux Store Action Queue", DISPATCH_QUEUE_SERIAL); } return self; } - (void)dispatchAction:(TLBAction *)action { dispatch_async(self.serialQueue, ^{ TLBState *newState = [self.state copy]; for (TLBReduceBlock block in self.reducers) { block(&newState, action); } self.state = newState; }); } - (RACSignal *)stateObserver { if (!_stateObserver) { _stateObserver = [RACObserve(self, state) replayLast]; } return _stateObserver; } - (TLBState *)state { if (!_state) { _state = [[TLBState alloc] init]; } return _state; } - (NSArray <TLBReduceBlock> *)reducers { if (!_reducers) { _reducers = [TLBReducer availableReduceBlocks]; } return _reducers; } @end
假如現在我有一個 UIViewController
,我要跟伺服器請求文章列表,取得列表之後就更新我的 tableView
,那使用 ReactiveCocoa
程式碼長得大概像這樣。
/// TLBPostListViewController.m @interface TLBPostListViewController () <UITableViewDataSource, UITableViewDelegate> @property (weak, nonatomic) IBOutlet UITableView *tableView; @property (nonatomic, strong) NSOrderedSet <NSString *> *posts; @property (nonatomic, strong) RACDisposable *stateObserver; @end @implementation TLBPostListViewController - (void)dealloc { [_stateObserver dispose]; _stateObserver = nil; } - (void)viewDidLoad { [super viewDidLoad]; [[[TLBNetworkManager shardManager] fetchPost] subscribeNext:^(NSArray *posts) { // 送出 action 之後就不理會它了,因為我們會監聽 state 的變化 TLBAction *action = [[TLBAction alloc] initWithActionType:TLBActionTypeSetPosts payload:posts]; [[TLBStore shardInstance] dispatchAction:action]; }]; @weakify(self); // 監聽 state 的變化 self.stateObserver = [[TLBStore shardInstance].stateObserver subscribeNext:^(TLBState *state) { @strongify(self); if (![self.posts isEqualToOrderedSet:state.posts]) { self.posts = [state.posts copy]; [self.tableView reloadData]; } }]; } @end
Redux 只是一個處理資料的方案,它可以跟 MVC / MVVM / VIPER / Coordinator 等架構相互配合,因為它們要處理的是不同問題。我覺得使用 Redux 有以下這些優點:
當然它也有缺點:
總結來說,每個架構有其適合的場景,你要先瞭解要解決的問題再來選擇要使用的架構,不要太早優化也不要過度設計了。