iOS开发-去model化开发

前言

去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

cellmodel共同通过协议的方式实现交流的时候,控制器存储的数据源也就可以不关心这些对象的具体类型了。通过泛型声明多个数据源,控制器此时的职责仅仅是根据状态机的改变决定使用哪个数据源来展示而已。当然,虽然笔者统一了这三个数据源的类型,但是归根到底总要根据服务器返回的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化之后整个项目的业务流程大致可以用下图表示:


这种方式最大的好处在于控制器和视图不再依赖于model的具体类型,这样在服务器返回的json中修改了模型对象字段的时候,修改ModelProtocol的对应实现即可。甚至在以后的版本再添加现金券各种其他票券的时候,只需要在Helper这一环节添加相应的工厂即可完成改动

尾言

去model化是一种有效快捷的松耦合方式,但绝不是万能药。在本文的demo中不难看到笔者使用这一方式最大的原因在于多个cell之间有太多的共性而model的属性字段全不相同。另一方面在这种设计中Helper可能会因为模型对象的增加变得臃肿,需要谨慎使用。
一个好的项目框架总是随着需求改变在不断的调整的,没有绝对最佳的设计方案。但是尝试使用不同的思路去搭建项目可以提升我们的认知,培养对于开发框架设计的认识。

关注我的文集iOS开发来获取笔者文章动态(转载请注明本文地址及作者)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容