说说view与model解耦

作者: 熊妹妹 | 来源:发表于2016-10-28 15:43 被阅读813次

    在开发应用时,一般我们都是一个view对应一个model作为它的数据源,model的实例变量值通过一定的对应关系显示到view上,这就造成了view依赖model。一个view的存在必须依赖指定类型的model。

    比如有个view

    @interface CCView : UIView
    
    @property (nonatomic,strong) UILabel *titleLabel;
    @property (nonatomic,strong) UILabel *subtitleLabel;
    
    @property (nonatomic,strong) CCAModel *aModel;
    
    @end
    

    它的数据源model

    @interface CCAModel : NSObject
    
    @property (nonatomic,strong) NSString *name;
    @property (nonatomic,strong) NSString *age;
    
    @end
    

    我们一定会有如下代码,view内容的显示逻辑

    self.titleLabel.text = aModel.name;
    self.subtitleLabel.text = aModel.age;
    

    而有的时候两种类型model都显示成同一种类型的view,现在又多了一个数据源类型。

    @interface CCBModel : NSObject
    
    @property (nonatomic,strong) NSString *story;
    @property (nonatomic,strong) NSString *description;
    
    @end
    

    我猜你马上会想到两种非常简单的方法解决。一种是让view去兼容这两个model,在view显示的时候去辨别当前数据源是哪个类型的model我要显示它的哪个实例变量,也就是要给view添加多个类型model的实例变量;一种重新生成一个新的model来作兼容,把其他类型的model都转为同一种类型。那么问题来了,如果使用第一种方法我一个view会有多个类型model做数据源时代码会写的非常复杂,每次新增都会引起很多代码变动,代码会变得非常庞大并包含很多if else。如果使用第二种方法那每新增一个model类型都要做一次转化。下面来说说最近用到的两个方法。

    类簇形式

    类簇,在iOS中大量的被使用,它将一些私有的、具体的子类组合在一个公共的、抽象的超类下面,由超类做一个所有对外接口的承载。NSString,NSArray,NSDictionary,NSNumber都是类簇的实现。在背后它隐藏了很多实现类却只暴露最简单的接口。有时你会发现,变量声明的NSString类型,打印出来发现是__NSCFString类型,说明NSString类型的对象实际是用__NSCFString类型实例化的。

    接着分析上面的问题,当一个类型的view有多个类型model作为数据源时,这些model具有相似的行为就是给view上显示的元素提供对应的数据,仅仅他们的实现形式可能会有不同,我们就可以抽象出超类。view并不关心是哪个类型model对它提供数据,只关心要显示的内容,model的差异都在工厂内处理,view需要的数据用协议进行声明。

    所以在view增加对数据源获取的协议

    @protocol CCViewModelProtocol <NSObject>
    
    - (NSString *)title;
    - (NSString *)subtitle;
    
    @end
    
    @interface CCView : UIView
    
    @property (nonatomic,strong) UILabel *titleLabel;
    @property (nonatomic,strong) UILabel *subtitleLabel;
    
    @property (nonatomic,strong) id<CCViewModelProtocol> model;
    
    @end
    

    model就是view指定的数据源。然后我们创建一个承载对接view接口的类。这个类可以不作为超类,因为在实际开发当中model的来源可能很多。并不一定都能继承子同一个类,所以只要它满足协议就可以了,这样更为灵活。

    @interface CCModel : NSObject <CCViewModelProtocol>
    
    - (id)modelWithType:(NSInteger)type;
    
    @end
    
    @implementation CCModel
    
    - (id)modelWithType:(NSInteger)type {
        switch (type) {
            case 0:
                return [CCAModel new];
                break;
            case 1:
                return [CCBModel new];
                break;
            default:
                return [super init];
                break;
        }
    }
    
    - (NSString *)title {
        return @"";
    }
    
    - (NSString *)subtitle {
        return @"";
    }
    
    @end
    

    之后在修改各个model增加CCViewModelProtocol协议即可。必要的时候可以新建一个子类继承原有的数据类型,再添加协议。总之这里的核心就是定义了一个view需要数据源接口的协议,然后通过一个抽象的超类包装所有具体实现的那些model数据类型。

    代理形式

    NSProxy用来实现消息转发,给了我们一个思路。view对显示数据的获取,从一个代理获取。代理里面做的事情,就是把消息转发给对应的model数据类型。

    我们同样需要一个协议,这个协议定义view所需要的数据接口。

    @protocol CCViewModelProtocol <NSObject>
    
    - (NSString *)title;
    - (NSString *)subtitle;
    
    @end
    
    @interface CCView : UIView
    
    @property (nonatomic,strong) UILabel *titleLabel;
    @property (nonatomic,strong) UILabel *subtitleLabel;
    
    @property (nonatomic,strong) id<CCViewModelProtocol> proxy;
    
    @end
    

    差别只是在于中间接口的承载着,用代理来实现消息的转发。把具体实现接口的实例注入到代理中,然后由代理进行消息转发。

    @interface CCProxy : NSProxy
    
    - (void)injectObject:(id)object;
    
    @end
    
    @implementation CCProxy {
        id _object;
    }
    
    - (void)injectObject:(id)object{
        _object = object;
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
    {
        if ([_object respondsToSelector:sel]) {
            return [_object methodSignatureForSelector:sel];
        }
        
        return [super methodSignatureForSelector:sel];
    }
    
    - (void)forwardInvocation:(NSInvocation *)invocation
    {
        if ([_object respondsToSelector:invocation.selector]) {
            [invocation invokeWithTarget:_object];
            return;
        }
        [super forwardInvocation:invocation];
    }
    
    @end
    

    调用时的代码如下

    CCAModel *aModel = [[CCAModel alloc] init];
    CCProxy *proxy = [CCProxy alloc];
    [proxy injectObject:aModel];
    
    view.proxy = (id)proxy;
    view.titleLabel.text = [view.proxy title];
    view.subtitleLabel.text = [view.proxy subtitle];
    

    这只是一个简单代理的使用方式,同时我们可以用它支持更多协议。只要在注入对象时,同时注入对应的协议。用一个字典来保存协议和对象的对应关系,消息转发时再用respondsToSelector来判断,这样可以代理就会变得更加灵活。

    总结一下,view与model解耦的核心在于view获取对应数据内容时抽象出的协议接口。这些接口的实现可以通过一个类簇(其实就是一个抽象工厂)或代理来实现。类簇的方式返回真正执行接口的对象,代理的方式把消息转发给真正实现接口的对象。通过中间这一层将view于model分离做到解耦。

    相关文章

      网友评论

        本文标题:说说view与model解耦

        本文链接:https://www.haomeiwen.com/subject/wnkeuttx.html