去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可能会因为模型对象的增加变得臃肿,需要谨慎使用。 一个好的项目框架总是随着需求改变在不断的调整的,没有绝对最佳的设计方案。但是尝试使用不同的思路去搭建项目可以提升我们的认知,培养对于开发框架设计的认识。

转载请注明本文地址及作者