# iOS开发之去model化开发

语言：zh-CN
原文：[iOS开发之去model化开发](<https://allluckly.cn/%E6%8A%95%E7%A8%BF/tougao56>)
中文：[中文 HTML](<https://allluckly.cn/%E6%8A%95%E7%A8%BF/tougao56>)
English: [English HTML](<https://allluckly.cn/en/journal/2016-07-28-decoupling-ios-views-from-concrete-model-types/>)
发布于: 2016-07-28T00:00:00+08:00
内容更新于: 2016-07-28T00:00:00+08:00
署名：Sindri

面对字段不同但展示相近的多种票券数据，使用字典承载数据并通过协议暴露统一展示属性，让 Cell 与控制器减少对具体模型类型的依赖，讨论数据转换职责的放置。

原文署名Sindri，Bison 编辑收录。

[原文所列来源](<http://www.jianshu.com/p/2b2290fa0beb>)

**阅读前** 这是 2015–2017 年间的技术记录，本次整理未重新运行验证示例。涉及的 API、依赖和工具版本请结合发表日期阅读。

`去model化`是一种框架设计上的做法，其中的`model`并不是指架构中的`model`层，套用Casa大神博客中的原文就是：

> model化就是使用数据对象，去model化就是不使用数据对象。

编辑：[Bison](<https://allluckly.cn/>)  
 来源：[Sindri的小巢](<http://www.jianshu.com/p/2b2290fa0beb>)

常见的`去model化`做法是使用字典保存数据信息，然后提供一个`reformer`负责将这些字典数据转换成`View`层可展示的信息，其流程图如下：

![1](<https://allluckly.cn/images/blog/tuogao/tougao56/1.jpg>)

更详细的理论知识可以看Casa大神的[去model化和数据对象](<http://casatwy.com/OOP_nomodel.html>)。本文基于Casa大神的实践基础使用另外一种`去model化`的实现方式。

## 使用背景

在很早之前就看过大神的文章，不过一直没有去尝试这种做法。在笔者最近跳入新坑之后，总算是有了这么一次机会。需求是存在着三个非常相似的`cell`，但分别对应着不同的数据`model`：

![1](<https://allluckly.cn/images/blog/tuogao/tougao56/2.jpg>)

总结三个`cell`都需要的展示数据包括：

- 产品名称
- 使用条件
- 截止日期
- 背景图片

此外，`优惠信息`属于第一个和第二个独有的。现在这一需求存在的问题主要有这么三点：

> **三种数据对象在服务器返回的属性字段中命名差别大** *这是大部分的应用都存在的一个问题，但是本文中的数据对象有一个显著的特点是它们对应显示的`cell`存在很大的相似度，可以被转换成相似的展示数据*

> **三种`cell`可以封装成一种，却分别对应着不同的数据对象** *这里涉及`cell`和数据对象的对接问题，如果`cell`在以后发生改变了，那么原有的数据对象是否还能适用*

> **控制器需要在数据源方法中调配不同的`cell`和`model`，耦合过大** *这个也是常见的问题之一，通常可以考虑适用工厂模式将调配的业务分离出去，但在本文中采用`去model`的方式实现*

这些问题都有可能导致项目后期维护的过程中变得难以修改，小小的需求改动都会导致代码的大改。笔者的解决方式是制定`cell`和`model`之间对应的两个协议，从而控制器无需理会两者的具体类型。

## 实现

我在上一篇文章[MVC架构杂谈](<http://www.jianshu.com/p/4847c9a1e19b>)中提到过`M`层的业务逻辑放在`model`中，虽然本文要`去model化`，但只是去除属性对象，自身的逻辑处理还保留着。下面是笔者`去model化`的协议图以及协议声明属性： ![1](<https://allluckly.cn/images/blog/tuogao/tougao56/3.jpg>)

```
@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`自身只需简单的持有字典数据即可：

![1](<https://allluckly.cn/images/blog/tuogao/tougao56/4.jpg>)

以`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化`之后整个项目的业务流程大致可以用下图表示：

![1](<https://allluckly.cn/images/blog/tuogao/tougao56/5.jpg>)

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

## 尾言

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

---

好文推荐：[分分钟解决iOS开发中App启动动画广告的功能](<https://allluckly.cn/lblaunchimagead/LBLaunchImageAd>)

