iOS開發(fā)-去model化開發(fā)

前言

去model化是一種框架設(shè)計上的做法誉碴,其中的model并不是指架構(gòu)中的model層,套用Casa大神博客中的原文就是:

model化就是使用數(shù)據(jù)對象漱挚,去model化就是不使用數(shù)據(jù)對象翔烁。

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


更詳細的理論知識可以看Casa大神的去model化和數(shù)據(jù)對象蹬屹。本文基于Casa大神的實踐基礎(chǔ)使用另外一種去model化的實現(xiàn)方式侣背。

使用背景

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


總結(jié)三個cell都需要的展示數(shù)據(jù)包括:

  • 產(chǎn)品名稱
  • 使用條件
  • 截止日期
  • 背景圖片

此外潮太,優(yōu)惠信息屬于第一個和第二個獨有的。現(xiàn)在這一需求存在的問題主要有這么三點:

三種數(shù)據(jù)對象在服務(wù)器返回的屬性字段中命名差別大
這是大部分的應(yīng)用都存在的一個問題虾攻,但是本文中的數(shù)據(jù)對象有一個顯著的特點是它們對應(yīng)顯示的cell存在很大的相似度铡买,可以被轉(zhuǎn)換成相似的展示數(shù)據(jù)

三種cell可以封裝成一種,卻分別對應(yīng)著不同的數(shù)據(jù)對象
這里涉及cell和數(shù)據(jù)對象的對接問題霎箍,如果cell在以后發(fā)生改變了奇钞,那么原有的數(shù)據(jù)對象是否還能適用

控制器需要在數(shù)據(jù)源方法中調(diào)配不同的cellmodel,耦合過大
這個也是常見的問題之一漂坏,通尘鞍#可以考慮適用工廠模式將調(diào)配的業(yè)務(wù)分離出去,但在本文中采用去model的方式實現(xiàn)

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

實現(xiàn)

我在上一篇文章MVC架構(gòu)雜談中提到過M層的業(yè)務(wù)邏輯放在model中完慧,雖然本文要去model化,但只是去除屬性對象条篷,自身的邏輯處理還保留著骗随。下面是筆者去model化的協(xié)議圖以及協(xié)議聲明屬性:

@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屬性的協(xié)議赴叹,讓這些模型對象在協(xié)議的getter方法中執(zhí)行數(shù)據(jù)->展示這一過程的業(yè)務(wù)邏輯,而model自身只需簡單的持有字典數(shù)據(jù)即可:

字典數(shù)據(jù)->展示數(shù)據(jù)

LXDCouponTicketModel為例指蚜,協(xié)議的實現(xiàn)代碼如下:

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

@end

// m實現(xiàn)
@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;
}

通過讓三個數(shù)據(jù)對象實現(xiàn)這個協(xié)議乞巧,筆者將要展示的數(shù)據(jù)結(jié)果進行統(tǒng)一。在這種情況下摊鸡,封裝成單個的cell也無需關(guān)心model的具體類型是什么绽媒,只需實現(xiàn)針對單元格配置的協(xié)議方法獲取展示的數(shù)據(jù)即可:

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

@end

// m實現(xiàn)
#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

三個問題前兩個已經(jīng)解決了:通過協(xié)議統(tǒng)一數(shù)據(jù)對象的展示效果,這時候并不需要model保存多個屬性對象免猾,只需要在適當?shù)臅r候直接從字典中獲取數(shù)據(jù)并執(zhí)行數(shù)據(jù)可視化這一邏輯即可是辕。cell也不會受限于傳入的參數(shù)類型,只需要簡單的調(diào)用協(xié)議方法獲取需要的數(shù)據(jù)即可猎提。那么最后一個控制器的協(xié)調(diào)問題就變得簡單了:

// m實現(xiàn)
@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

cellmodel共同通過協(xié)議的方式實現(xiàn)交流的時候获三,控制器存儲的數(shù)據(jù)源也就可以不關(guān)心這些對象的具體類型了。通過泛型聲明多個數(shù)據(jù)源,控制器此時的職責僅僅是根據(jù)狀態(tài)機的改變決定使用哪個數(shù)據(jù)源來展示而已疙教。當然棺聊,雖然筆者統(tǒng)一了這三個數(shù)據(jù)源的類型,但是歸根到底總要根據(jù)服務(wù)器返回的json創(chuàng)建不同的數(shù)據(jù)對象存放到這些數(shù)據(jù)源中贞谓。如果把這個業(yè)務(wù)放在控制器中原本就達不到松耦合的作用限佩,因此引入一個中間人Helper來完成這個業(yè)務(wù):

// h文件
@interface LXDTicketDataHelper: NSObject

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

@end

// m實現(xiàn)
#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太多時裸弦,使用繼承關(guān)系的工廠會更好
        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實現(xiàn)
#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化之后整個項目的業(yè)務(wù)流程大致可以用下圖表示:


這種方式最大的好處在于控制器和視圖不再依賴于model的具體類型祟同,這樣在服務(wù)器返回的json中修改了模型對象字段的時候,修改ModelProtocol的對應(yīng)實現(xiàn)即可理疙。甚至在以后的版本再添加現(xiàn)金券各種其他票券的時候耐亏,只需要在Helper這一環(huán)節(jié)添加相應(yīng)的工廠即可完成改動

尾言

去model化是一種有效快捷的松耦合方式,但絕不是萬能藥沪斟。在本文的demo中不難看到筆者使用這一方式最大的原因在于多個cell之間有太多的共性而model的屬性字段全不相同广辰。另一方面在這種設(shè)計中Helper可能會因為模型對象的增加變得臃腫,需要謹慎使用主之。
一個好的項目框架總是隨著需求改變在不斷的調(diào)整的择吊,沒有絕對最佳的設(shè)計方案。但是嘗試使用不同的思路去搭建項目可以提升我們的認知槽奕,培養(yǎng)對于開發(fā)框架設(shè)計的認識几睛。

關(guān)注我的文集iOS開發(fā)來獲取筆者文章動態(tài)(轉(zhuǎn)載請注明本文地址及作者)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市粤攒,隨后出現(xiàn)的幾起案子所森,更是在濱河造成了極大的恐慌,老刑警劉巖夯接,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件焕济,死亡現(xiàn)場離奇詭異,居然都是意外死亡盔几,警方通過查閱死者的電腦和手機晴弃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來逊拍,“玉大人上鞠,你說我怎么就攤上這事⌒旧ィ” “怎么了芍阎?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長缨恒。 經(jīng)常有香客問我谴咸,道長度硝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任寿冕,我火速辦了婚禮蕊程,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘驼唱。我一直安慰自己藻茂,他們只是感情好,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布玫恳。 她就那樣靜靜地躺著辨赐,像睡著了一般。 火紅的嫁衣襯著肌膚如雪京办。 梳的紋絲不亂的頭發(fā)上掀序,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機與錄音惭婿,去河邊找鬼不恭。 笑死,一個胖子當著我的面吹牛财饥,可吹牛的內(nèi)容都是我干的换吧。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼钥星,長吁一口氣:“原來是場噩夢啊……” “哼沾瓦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起谦炒,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤贯莺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后宁改,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體缕探,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年透且,在試婚紗的時候發(fā)現(xiàn)自己被綠了撕蔼。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片豁鲤。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡秽誊,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出琳骡,到底是詐尸還是另有隱情锅论,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布楣号,位于F島的核電站最易,受9級特大地震影響怒坯,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜藻懒,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一剔猿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嬉荆,春花似錦归敬、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至限番,卻和暖如春舱污,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背弥虐。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工扩灯, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人霜瘪。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓驴剔,卻偏偏與公主長得像,于是被迫代替她去往敵國和親粥庄。 傳聞我的和親對象是個殘疾皇子丧失,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容