最近在讀App架構方面的書。對這個感興趣是因為我意識到:
- 如果只停留在一些簡單頁面開發,架構肯定作用不大,只需要關注這個頁面需要什麼技術細節來實現就可以;
- 但如果是涉及到一個功能多樣或者業務複雜的App,那麼有一個良好規範的架構絕對是有幫助的。
然後onevcat的 關於 MVC 的一個常見的誤用一文也啟發了我,解決了一直以來我對Model的困惑,所以想用自己的例子再記錄一下,正好實現一個OC的版本。
一、標準的Model使用
在onevcat的文章中,大神貼出了一個標準的MVC結構圖。這個圖源自史丹佛CS193p的iOS應用開發課程。我在自學入門iOS的時候也學習了這個課程,不過是很老的版本,還用的Objective-C作為教學語言。
當時我還是個超級新手,看到這個圖的時候,最先懂的是View和Controller的互動,畢竟iOS開發最先接觸學習的肯定是檢視的建立和互動。而看到Model和Controller的互動,只知道用通知來和Controller通訊,至於具體怎麼實現則是我一直的困惑。
別看這只是一個單純的類之間的通訊問題,我相信很多缺乏和周圍交流的新手開發者們對模型的使用很容易停留在“瘦Model”上。即便想實現上圖的標準用法,但一時半會兒還真不好找學習資料,反正不用實現圖片裡的標準,App一樣能開發,更沒動力找了。
二、現狀
因此很容易出現的情況:Massive ViewController,把邏輯都堆在ViewController這個檢視容器裡,形成了龐大的、難以維護的單個類。
這也是onevcat大神在他的文章中提出的兩個問題,Massive ViewController:
- 本質是Model 層“寄生”在ViewController 中
- 違反資料流動規則和單一職責規則
用我的例子舉例說明這兩個問題。現在我們實現了下圖的一個服務列表。
這個我的需求和我的服務這兩個列表共用一個模型:
@interface MyReleaseModel : JSONModel
// 公用
@property(nonatomic, copy) NSString<Optional> *type;
@property(nonatomic, copy) NSString<Optional> *ID;
@property(nonatomic, copy) NSString<Optional> *addtime;
@property(nonatomic, copy) NSString<Optional> *views;
// 我的需求
@property(nonatomic, copy) NSArray<Optional> *dem_img;
@property(nonatomic, copy) NSString<Optional> *dem_desc;
@property(nonatomic, copy) NSString<Optional> *dem_price;
// 我的服務
@property(nonatomic, copy) NSArray<Optional> *s_img;
@property(nonatomic, copy) NSString<Optional> *s_desc;
@property(nonatomic, copy) NSString<Optional> *s_price;
複製程式碼
然後在ViewController裡,有兩個當前列表的陣列,之後的刪除邏輯就需要操作它:
// 需求列表array
@property(nonatomic, strong) NSMutableArray *demandMutaArray;
// 服務列表array
@property(nonatomic, strong) NSMutableArray *serviceMutaArray;
複製程式碼
點選刪除按鈕的邏輯,需求和服務的刪除邏輯一樣,所以這裡列舉需求的刪除程式碼(OC的程式碼真的很不適合展示……):
// 點選了我的需求 刪除按鈕
// 由於在cell裡,所以獲取到當前cell
ReleasedServiceAndDemandTableViewCell *myDemandCell = (ReleasedServiceAndDemandTableViewCell *)[[[sender view] superview] superview];
// 再獲取當前行數
NSIndexPath *myDemandIndexPath = [weakSelf.demandTableView indexPathForCell:myDemandCell];
// 使用了JSONModel,所以陣列裡的每一項都是一個JSONModel型別的資料
MyReleaseModel *myDemandModel = weakSelf.demandMutaArray[myDemandIndexPath.row];
// 網路請求寫在Model類裡了,所以從Model發出刪除的網路請求(隱去具體的引數)
[myDemandModel deleteItemNetworkWithxxx:myDemandModel.xxx withxxx:myDemandModel.xxx];
// 在viewController類裡對陣列操作
[weakSelf.demandMutaArray removeObjectAtIndex:myDemandIndexPath.row];
// 呼叫系統框架裡列表的刪除API
[weakSelf.demandTableView deleteRowsAtIndexPaths:@[myDemandIndexPath] withRowAnimation:UITableViewRowAnimationLeft];
複製程式碼
三、闡述問題
現在就是這麼一個通過列表展示資料,然後能進行刪除操作的情況。那麼這有什麼問題呢?
首先,就是Model 層“寄生”在ViewController 中。
表面上看似有一個MyReleaseModel類,但它其實是“瘦Model”,只提供需要的屬性欄位,真正起到Model作用的則是上面的demandMutaArray
和serviceMutaArray
兩個陣列。
onevcat在他的文章中提出:
我們難以從外界維護或者同步 items(注:這裡是
demandMutaArray
和serviceMutaArray
兩個陣列) 的狀態,新增和刪除操作被“繫結”在了這個 View Controller 裡,如果你還想通過其他 View Controller 維護待辦列表的話,就不得不考慮資料同步的問題 (我們會在稍後看到幾個具體的這方面的例子);另外,這樣的設定導致 items 難以測試。你幾乎無法為新增/刪除/修改待辦列表進行 Model 層的測試。
其次,是違反資料流動規則和單一職責規則
如果點選刪除按鈕的話,會是這樣一個流程:
- 改變Model(
demandMutaArray
和serviceMutaArray
兩個陣列) - 改變tableView的Cell
這實質是操作UI,然後變更Model,但同時也變更了UI。但之前那個標準的MVC圖所倡導的資料流動應該是:
- UI 操作 -> 經由 View Controller 進行模型更新 -> 新的模型經由 View Controller 更新 UI -> 等待新的 UI 操作
而上面的例子則在經由 View Controller 進行模型更新
這一步變成經由 View Controller 進行模型更新以及 UI 操作
。onevcat大神的觀點是:“雖然看起來這是很不起眼的變更,但是會在專案複雜後帶來麻煩。”
在onevcat大神的文章裡,他列舉了兩個場景證明他的觀點,可以去看一下。
四、到底怎麼改進,更好地使用Model
建立真正的Model層
整個改進過程就是把ViewController裡運算元據的那部分邏輯遷移到Model層,然後Model層使用通知Notification把必要的資訊回傳給ViewController,後者根據資訊做相應動作。
Model層是app的內容,它不依賴於(像UIKit那樣的)任何app框架。也就是說,程式設計師對model層有完全的控制。Model層通常包括model物件(在錄音app中的例子是資料夾和錄音物件)和協調物件(比如我們的app例子中的負責在磁碟上儲存資料的Store型別)。被儲存在磁碟上的那部分model我們稱之為文件model(documentation model)。
如果model層能做到和應用框架分離,我們就可以完全在app的範圍之外使用它。我們可以很容易地在另外的測試套件中執行它,或者用一個完全不同的應用框架重寫新的view層。這個model層將能夠用於Android,macOS或者Windows版本的app中。
——《App架構——使用Swift進行iOS架構》
Model主要使用觀察者模式:
觀察者模式是在MVC中維持model和view分離的關鍵。
這種方式的優點在於,不論變更源自哪裡(比如,view事件、後臺任務或者網路),我們都可以確信UI是和model資料同步的。
而且在遇到變更請求時,model將有機會拒絕或者修改這個請求
——《App架構——使用Swift進行iOS架構》
把資料相關的屬性放到Model裡
@interface MyReleaseModel ()
@property(nonatomic, strong) NSMutableArray *demandMutaArray;
@property(nonatomic, strong) NSMutableArray *serviceMutaArray;
@end
複製程式碼
然後我們需要監視這兩個列表陣列的變化,Swift有值型別的陣列,有監視屬性,可以非常方便地監視屬性的變化。在OC裡我就先用KVO代替了。
#pragma mark - KVO method
// 觀察回撥
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSMutableArray *oldArray = (NSMutableArray *)[change valueForKey:@"old"];
NSMutableArray *newArray = (NSMutableArray *)[change valueForKey:@"new"];
if ([keyPath isEqualToString:@"demandMutaArray"]) {
// demandMutaArray
} else {
// serviceMutaArray
}
}
// 新增KVO
- (void)observePropertyChange {
[self.demandMutaArray addObserver:self forKeyPath:@"demandMutaArray" options:NSKeyValueObservingOptionNew context:nil];
[self.serviceMutaArray addObserver:self forKeyPath:@"serviceMutaArray" options:NSKeyValueObservingOptionNew context:nil];
}
// 移除KVO
- (void)removeObserverFromProperty {
[self.demandMutaArray removeObserver:self forKeyPath:@"demandMutaArray"];
[self.serviceMutaArray removeObserver:self forKeyPath:@"serviceMutaArray"];
}
複製程式碼
在適當的地方呼叫新增KVO和移除KVO的方法。然後在觀察回撥方法,也就是每次屬性變化的時候,我們做一個新值和舊值的對比,再定義一個enum,根據對比結果返回enum的狀態。
typedef enum : NSUInteger {
addItem,
removeItem,
reload,
} ChangeBehavior;
+ (ChangeBehavior)differenceBetweenOld:(NSMutableArray *)old andNew:(NSMutableArray *)new {
NSSet *oldSet = [NSSet setWithArray:old];
NSSet *newSet = [NSSet setWithArray:new];
if ([oldSet isSubsetOfSet:newSet]) {
// 新增
// ...
return addItem;
} else if ([newSet isSubsetOfSet:oldSet]) {
// 刪除
// ...
return removeItem;
} else {
// 既新增 也刪除
// ...
return reload;
}
}
// 觀察回撥
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
NSMutableArray *oldArray = (NSMutableArray *)[change valueForKey:@"old"];
NSMutableArray *newArray = (NSMutableArray *)[change valueForKey:@"new"];
if ([keyPath isEqualToString:@"demandMutaArray"]) {
// demandMutaArray
ChangeBehavior behavior = [self.class differenceBetweenOld:oldArray andNew:newArray];
[[NSNotificationCenter defaultCenter] postNotificationName:@"MyReleaseModelDemandDidChangedNotification" object:self userInfo:@{@"MyReleaseModelDemandDidChangedNotification": @(behavior)}];
} else {
// serviceMutaArray
// 同上
}
}
複製程式碼
在Model裡給外界開放“新增”“刪除”等運算元據的方法和一些資料相關的屬性
@property(nonatomic, assign) NSInteger demandPage;
@property(nonatomic, assign) NSInteger servicePage;
@property(nonatomic, assign) NSInteger demandCount;
@property(nonatomic, assign) NSInteger serviceCount;
- (void)addItem:(NSMutableArray *)itemArray;
- (void)removeAtIndex:(NSIndexPath *)indexPath;
- (MyReleaseModel *)itemAtIndex:(NSIndexPath *)indexPath;
複製程式碼
貼上介面定義,實現程式碼就不在此貼上了,在實現裡會改變demandMutaArray
或serviceMutaArray
,從而觸發KVO回撥,再通過通知Notification把相應的Enum狀態返回給訂閱通知的ViewController類。
在相應的ViewController類裡訂閱通知,檢視更新時,呼叫Model方法運算元據
先在相應的ViewController裡例項化Model,懶載入方式:
- (MyReleaseModel *)demandModel {
if (_demandModel == nil) {
_demandModel = [MyReleaseModel sharedInstance];
}
return _demandModel;
}
- (MyReleaseModel *)serviceModel {
if (_serviceModel == nil) {
_serviceModel = [MyReleaseModel sharedInstance];
}
return _serviceModel;
}
複製程式碼
訂閱通知,以及當Model改變時,Model通知ViewController來改變View:
- (void)viewDidLoad {
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(demandOrServiceDidChange:) name:@"MyReleaseModelDidChangedNotification" object:nil];
}
- (void)demandOrServiceDidChange:(NSNotification *)notification {
if ([notification.name isEqualToString:@"MyReleaseModelDemandDidChangedNotification"]) {
// 需求列表
ChangeBehavior behaivor = (ChangeBehavior)notification.userInfo[@"MyReleaseModelDemandDidChangedNotification"];
switch (behaivor) {
case addItem:
// 給table新增相應的cell
break;
case removeItem:
// 刪除table相應的cell
break;
case reload:
// 重新整理tableView
break;
default:
break;
}
} else {
// 服務列表
// ...
}
}
複製程式碼
或者當View改變時,View通過ViewController改變Model:
- (void)tapGestureAction:(UITapGestureRecognizer *)sender {
NSInteger index = sender.view.tag;
if (index == 1) {
NSLog(@"點選了我的需求 刪除按鈕");
ReleasedServiceAndDemandTableViewCell *myDemandCell = (ReleasedServiceAndDemandTableViewCell *)[[[sender view] superview] superview];
NSIndexPath *myDemandIndexPath = [weakSelf.demandTableView indexPathForCell:myDemandCell];
// 重點:改變Model
[self.demandModel removeAtIndex:myDemandIndexPath];
// ....
// ....
}
複製程式碼
這樣,我們就實現了MVC圖所倡導的這種單向資料流動:
- UI 操作 -> 經由 View Controller 進行模型更新 -> 新的模型經由 View Controller 更新 UI -> 等待新的 UI 操作
五、總結
這樣的方式寫Model,真正的把Model從ViewController獨立了出來,也實現了單一職責原則——Model全權負責資料,也達成了單向資料流,使整個流程不雜亂。