iOS開發-去model化開發

文章轉載自: Sindri的小巢
原文鏈接:http://www.lxweimin.com/p/2b2290fa0beb

前言

去model化是一種框架設計上的做法,其中的model并不是指架構中的model層,套用Casa大神博客中的原文就是:

model化就是使用數據對象,去model化就是不使用數據對象。

常見的去model化做法是使用字典保存數據信息,然后提供一個reformer負責將這些字典數據轉換成View層可展示的信息,其流程圖如下:

更詳細的理論知識可以看Casa大神的去model化和數據對象。本文基于Casa大神的實踐基礎使用另外一種去model化的實現方式。

使用背景

在很早之前就看過大神的文章,不過一直沒有去嘗試這種做法。在筆者最近跳入新坑之后,總算是有了這么一次機會。需求是存在著三個非常相似的cell,但分別對應著不同的數據model

總結三個cell都需要的展示數據包括:

  • 產品名稱
  • 使用條件
  • 截止日期
  • 背景圖片*

此外,優惠信息屬于第一個和第二個獨有的。現在這一需求存在的問題主要有這么三點:

三種數據對象在服務器返回的屬性字段中命名差別大

這是大部分的應用都存在的一個問題,但是本文中的數據對象有一個顯著的特點是它們對應顯示的cell存在很大的相似度,可以被轉換成相似的展示數據

三種cell可以封裝成一種,卻分別對應著不同的數據對象

這里涉及cell和數據對象的對接問題,如果cell在以后發生改變了,那么原有的數據對象是否還能適用

控制器需要在數據源方法中調配不同的cellmodel,耦合過大

這個也是常見的問題之一,通常可以考慮適用工廠模式將調配的業務分離出去,但在本文中采用去model的方式實現

這些問題都有可能導致項目后期維護的過程中變得難以修改,小小的需求改動都會導致代碼的大改。筆者的解決方式是制定cellmodel之間對應的兩個協議,從而控制器無需理會兩者的具體類型。

實現

我在上一篇文章MVC架構雜談中提到過M層的業務邏輯放在model中,雖然本文要去model化,但只是去除屬性對象,自身的邏輯處理還保留著。下面是筆者去model化的協議圖以及協議聲明屬性:

@protocol LXDTicketModelProtocol <NSObject>

@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<NSObject>

- (void)configurateCellWithModel: 

(id<LXDTicketModelProtocol>)model;

@end

對于本文之中這種存在共同顯示效果的model,可以聲明一個包含多個readonly屬性的協議,讓這些模型對象在協議的getter方法中執行數據->展示這一過程的業務邏輯,而model自身只需簡單的持有字典數據即可:

字典數據 -->展示數據

LXDCouponTicketModel為例,協議的實現代碼如下:

// h文件
@interface LXDCouponTicketModel: NSObject<LXDTicketModelProtocol>

@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的具體類型是什么,只需實現針對單元格配置的協議方法獲取展示的數據即可:

// h文件
@interface LXDTicketCell: UITableViewCell<LXDTicketCellProtocol>
@end

// m實現
#define LXDCommonColor [UIColor colorWithRed: 253/255. green: 99/255. blue: 99/255. alpha: 1]
@implementation LXDTicketCell

- (void)configurateWithModel: (id<LXDTicketModelProtocol>)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也不會受限于傳入的參數類型,只需要簡單的調用協議方法獲取需要的數據即可。那么最后一個控制器的協調問題就變得簡單了:

// m實現
@interface LXDTicketViewController ()

@property (nonatomic, strong) NSMutableArray< id<LXDTicketModelProtocol> > * couponTickets;
@property (nonatomic, strong) NSMutableArray< id<LXDTicketModelProtocol> > * discountTickets;
@property (nonatomic, strong) NSMutableArray< id<LXDTicketModelProtocol> > * 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<LXDTicketCellProtocol>)cell configurateCellWithModel: [self modelWithIndexPath: indexPath]]; } 
      return cell;
}

#pragma mark - Data Generator
- (id<LXDTicketModelProtocol>)modelWithIndexPath: (NSIndexPath *)indexPath
{ 
      return self.currentModelSet[indexPath.row];
}
- (NSMutableArray< id<LXDTicketModelProtocol> > *)currentModelSet
{
      switch (_ticketType) { 
            case KMCTicketTypeCoupon: 
                  return _couponTickets; 
            case KMCTicketTypeDiscount: 
                  return _discountTickets; 
            case KMCTicketTypeConvert: 
                  return _convertTickets; 
      }
}

@end```
當`cell`和`model`共同通過協議的方式實現交流的時候,控制器存儲的數據源也就可以不關心這些對象的具體類型了。通過泛型聲明多個數據源,控制器此時的職責僅僅是根據狀態機的改變決定使用哪個數據源來展示而已。當然,雖然筆者統一了這三個數據源的類型,但是歸根到底總要根據服務器返回的json創建不同的數據對象存放到這些數據源中。如果把這個業務放在控制器中原本就達不到松耦合的作用,因此引入一個中間人`Helper`來完成這個業務:

// h文件
@interface LXDTicketDataHelper: NSObject

  • (void)anaylseJSON: (NSString *)JSON complete: (void(^)(NSMutableArray< id<LXDTicketModelProtocol> > *)models);

@end

// m實現

import "LXDCouponTicketModel.h"

import "LXDConvertTicketModel.h"

import "LXDDiscountTicketModel.h"

@implementation LXDTicketDataHelper

  • (void)anaylseJSON: (NSString *)JSON complete: (void(^)(NSMutableArray< id<LXDTicketModelProtocol> > *)models)
    {
    NSParameterAssert(JSON);
    NSParameterAssert(complete);
    [LXDQueue executeInGlobalQueue: ^{
    Class ModelCls = NULL;
    NSDictionary * jsonDict = [NSDictionary dictionaryWithJSON: JSON];
    NSMutableArray< id<LXDTicketModelProtocol> > * 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<LXDTicketModelProtocol>)[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化`之后整個項目的業務流程大致可以用下圖表示:

![](http://upload-images.jianshu.io/upload_images/1135052-d3a240701a54c749.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

這種方式最大的好處在于控制器和視圖不再依賴于`model`的具體類型,這樣在服務器返回的json中修改了模型對象字段的時候,修改ModelProtocol的對應實現即可。甚至在以后的版本再添加現金券各種其他票券的時候,只需要在Helper這一環節添加相應的工廠即可完成改動
####尾言
`去model化`是一種有效快捷的松耦合方式,但絕不是萬能藥。在本文的demo中不難看到筆者使用這一方式最大的原因在于多個`cell`之間有太多的共性而`model`的屬性字段全不相同。另一方面在這種設計中Helper可能會因為模型對象的增加變得臃腫,需要謹慎使用。一個好的項目框架總是隨著需求改變在不斷的調整的,沒有絕對最佳的設計方案。但是嘗試使用不同的思路去搭建項目可以提升我們的認知,培養對于開發框架設計的認識。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容