前言
去model化
是一種框架設計上的做法,其中的model
並不是指架構中的model
層,套用Casa大神部落格中的原文就是:
model化就是使用資料物件,去model化就是不使用資料物件。
常見的去model化
做法是使用字典儲存資料資訊,然後提供一個reformer
負責將這些字典資料轉換成View
層可展示的資訊,其流程圖如下:
更詳細的理論知識可以看Casa大神的去model化和資料物件。本文基於Casa大神的實踐基礎使用另外一種去model化
的實現方式。
使用背景
在很早之前就看過大神的文章,不過一直沒有去嘗試這種做法。在筆者最近跳入新坑之後,總算是有了這麼一次機會。需求是存在著三個非常相似的cell
,但分別對應著不同的資料model
:
總結三個cell
都需要的展示資料包括:
- 產品名稱
- 使用條件
- 截止日期
- 背景圖片
此外,優惠資訊
屬於第一個和第二個獨有的。現在這一需求存在的問題主要有這麼三點:
三種資料物件在伺服器返回的屬性欄位中命名差別大
這是大部分的應用都存在的一個問題,但是本文中的資料物件有一個顯著的特點是它們對應顯示的cell
存在很大的相似度,可以被轉換成相似的展示資料三種
cell
可以封裝成一種,卻分別對應著不同的資料物件
這裡涉及cell
和資料物件的對接問題,如果cell
在以後發生改變了,那麼原有的資料物件是否還能適用控制器需要在資料來源方法中調配不同的
cell
和model
,耦合過大
這個也是常見的問題之一,通常可以考慮適用工廠模式將調配的業務分離出去,但在本文中採用去model
的方式實現
這些問題都有可能導致專案後期維護的過程中變得難以修改,小小的需求改動都會導致程式碼的大改。筆者的解決方式是制定cell
和model
之間對應的兩個協議,從而控制器無需理會兩者的具體型別。
實現
我在上一篇文章MVC架構雜談中提到過M
層的業務邏輯放在model
中,雖然本文要去model化
,但只是去除屬性物件,自身的邏輯處理還保留著。下面是筆者去model化
的協議圖以及協議宣告屬性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
@protocol LXDTicketModelProtocol @optional @property (nonatomic, readonly) NSAttributedString * perferential; @required @property (nonatomic, readonly) NSString * backgroundImageName; @property (nonatomic, readonly) NSString * goodName; @property (nonatomic, readonly) NSString * effectCondition; @property (nonatomic, readonly) NSString * deadline; @property (nonatomic, readonly) LXDCellType type; - (instancetype)initWithDict: (NSDictionary *)dict; @end @protocol KMCTicketCellProtocol - (void)configurateCellWithModel: (id)model; @end |
對於本文之中這種存在共同顯示效果的model
,可以宣告一個包含多個readonly
屬性的協議,讓這些模型物件在協議的getter
方法中執行資料->展示
這一過程的業務邏輯,而model
自身只需簡單的持有字典資料即可:
以LXDCouponTicketModel
為例,協議的實現程式碼如下:
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
// h檔案 @interface LXDCouponTicketModel: NSObject @end // m實現 @implementation LXDCouponTicketModel { NSDictionary * _dict; } - (NSString *)backgroundImageName { return ([_dict[@"overdue"] boolValue] ? @"coupon_overdue" : @"coupon_common"); } - (NSAttributedString *)perferential { NSAttributedString * result = objc_getAssociatedObject(self, KMCPerferentialKey); if (result) { return result; } NSMutableAttributedString * attributedString = [[NSMutableAttributedString alloc] initWithString: @"¥" attributes: @{ NSFontAttributeName: [UIFont systemFontOfSize: 16] }]; [attributedString appendAttributedString: [[NSAttributedString alloc] initWithString: [NSString stringWithFormat: @"%g", [_dict[@"ticketMoney"] doubleValue]] attributes: @{ NSFontAttributeName: [UIFont boldSystemFontOfSize: 32] }]]; [attributedString addAttributes: @{ NSForegroundColorAttributeName: KMCCommonColor } range: NSMakeRange(0, attributedString.length)]; result = attributedString.copy; objc_setAssociatedObject(self, KMCPerferentialKey, result, OBJC_ASSOCIATION_RETAIN_NONATOMIC); return result; } - (NSString *)goodName { return [_dict[@"goodName"] stringValue]; } - (NSString *)effectCondition { return [NSString stringWithFormat: @"· 滿%lu元可用", [_dict[@"minLimitMoney"] unsignedIntegerValue]];; } - (NSString *)deadline { return [NSString stringWithFormat: @"· 兌換截止日期:%@", _dict[@"deadline"]]; } - (LXDCellType)type { return LXDCellTypeCoupon; } - (instancetype)initWithDict: (NSDictionary *)dict { if (self = [super init]) { _dict = dict; } return self; } |
通過讓三個資料物件實現這個協議,筆者將要展示的資料結果進行統一。在這種情況下,封裝成單個的cell
也無需關心model
的具體型別是什麼,只需實現針對單元格配置的協議方法獲取展示的資料即可:
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 28 29 30 31 32 33 34 |
// h檔案 @interface LXDTicketCell: UITableViewCell @end // m實現 #define LXDCommonColor [UIColor colorWithRed: 253/255. green: 99/255. blue: 99/255. alpha: 1] @implementation LXDTicketCell - (void)configurateWithModel: (id)model { UIView * goodInfoView = _goodNameLabel.superview; if ([model type] != KMCTicketTypeConvert) { [goodInfoView mas_updateConstraints: ^(MASConstraintMaker *make) { make.left.equalTo(_perferentialLabel.mas_right).offset(10); }]; } else { [goodInfoView mas_updateConstraints: ^(MASConstraintMaker *make) { make.left.equalTo(_backgroundImageView.mas_left).offset(18); }]; } [_use setTitleColor: LXDCommonColor forState: UIControlStateNormal]; _backgroundImageView.image = [UIImage imageNamed: [model backgroundImageName]]; _perferentialLabel.attributedText = [model perferential]; _effectConditionLabel.text = [model effectCondition]; _goodNameLabel.text = [model goodName]; _deadlineLabel.text = [model deadline]; [_effectConditionLabel sizeToFit]; [_goodNameLabel sizeToFit]; [_deadlineLabel sizeToFit]; } @end |
三個問題前兩個已經解決了:通過協議統一資料物件的展示效果,這時候並不需要model
儲存多個屬性物件,只需要在適當的時候直接從字典中獲取資料並執行資料視覺化這一邏輯即可。cell
也不會受限於傳入的引數型別,只需要簡單的呼叫協議方法獲取需要的資料即可。那麼最後一個控制器的協調問題就變得簡單了:
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
// m實現 @interface LXDTicketViewController () @property (nonatomic, strong) NSMutableArray > * couponTickets; @property (nonatomic, strong) NSMutableArray > * discountTickets; @property (nonatomic, strong) NSMutableArray > * convertTickets; @end @implementation LXDTicketViewController #pragma mark - UITableViewDataSource - (UITableViewCell *)tableView: (UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath { UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier: KMCTicketCommonCellIdentifier]; if ([cell conformsToProtocol: @protocol(LXDTicketCellProtocol)]) { [(id)cell configurateCellWithModel: [self modelWithIndexPath: indexPath]]; } return cell; } #pragma mark - Data Generator - (id)modelWithIndexPath: (NSIndexPath *)indexPath { return self.currentModelSet[indexPath.row]; } - (NSMutableArray > *)currentModelSet { switch (_ticketType) { case KMCTicketTypeCoupon: return _couponTickets; case KMCTicketTypeDiscount: return _discountTickets; case KMCTicketTypeConvert: return _convertTickets; } } @end |
當cell
和model
共同通過協議的方式實現交流的時候,控制器儲存的資料來源也就可以不關心這些物件的具體型別了。通過泛型宣告多個資料來源,控制器此時的職責僅僅是根據狀態機的改變決定使用哪個資料來源來展示而已。當然,雖然筆者統一了這三個資料來源的型別,但是歸根到底總要根據伺服器返回的json
建立不同的資料物件存放到這些資料來源中。如果把這個業務放在控制器中原本就達不到鬆耦合的作用,因此引入一箇中間人Helper
來完成這個業務:
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
// h檔案 @interface LXDTicketDataHelper: NSObject + (void)anaylseJSON: (NSString *)JSON complete: (void(^)(NSMutableArray > *)models); @end // m實現 #import "LXDCouponTicketModel.h" #import "LXDConvertTicketModel.h" #import "LXDDiscountTicketModel.h" @implementation LXDTicketDataHelper + (void)anaylseJSON: (NSString *)JSON complete: (void(^)(NSMutableArray > *)models) { NSParameterAssert(JSON); NSParameterAssert(complete); [LXDQueue executeInGlobalQueue: ^{ Class ModelCls = NULL; NSDictionary * jsonDict = [NSDictionary dictionaryWithJSON: JSON]; NSMutableArray > * results = @[].mutableCopy; // 使用switch簡單工廠,如果case太多時,使用繼承關係的工廠會更好 switch ((LXDModelType)[jsonDict[@"modelType"] integerValue]) { case LXDModelTypeCoupon: ModelCls = [KXDCouponTicketModel class]; break; case LXDModelTypeConvert: ModelCls = [LXDConvertTicketModel class]; break; case LXDModelTypeDiscount: ModelCls = [LXDDiscountTicketModel class]; break; } for (NSDictionary * dataDict in jsonDict[@"data"]) { id item = [(id)[ModelCls alloc] initWithDict: dataDict]; [result addObject: item]; } [LXDQueue executeInMainQueue: ^{ complete(result); }]; }]; } @end // m實現 #import "KMCNetworkHelper.h" @implementation LXDTicketViewController - (void)requestTickets { // get request parameters include 'url' and 'parameters' [LXDNetworkManager POST: PATH(url) parameters: parameters complete: ^(NSString * JSON, NSError * error) { // error check [LXDTicketDataHelper analyseJSON: JSON complete: ^(NSMutableArray * models) { [self.currentModelSet addObjectsFromArray: models]; }]; }]; } @end |
去model化
之後整個專案的業務流程大致可以用下圖表示:
這種方式最大的好處在於控制器和檢視不再依賴於model
的具體型別,這樣在伺服器返回的json
中修改了模型物件欄位的時候,修改ModelProtocol
的對應實現即可。甚至在以後的版本再新增現金券
各種其他票券的時候,只需要在Helper
這一環節新增相應的工廠即可完成改動
尾言
去model化
是一種有效快捷的鬆耦合方式,但絕不是萬能藥
。在本文的demo中不難看到筆者使用這一方式最大的原因在於多個cell
之間有太多的共性而model
的屬性欄位全不相同。另一方面在這種設計中Helper
可能會因為模型物件的增加變得臃腫,需要謹慎使用。
一個好的專案框架總是隨著需求改變在不斷的調整的,沒有絕對最佳的設計方案。但是嘗試使用不同的思路去搭建專案可以提升我們的認知,培養對於開發框架設計的認識。
關注我的文集iOS開發來獲取筆者文章動態(轉載請註明本文地址及作者)