iOS控制器瘦身-面向超类编程

作者: 小怡情ifelse | 来源:发表于2016-11-12 11:54 被阅读1025次

    今天写这篇文章的目的,是提供一种思路,来帮助大家解决控制器非常臃肿的问题,对控制器瘦身。

    滴滴 老司机要开车了

    如果手边有项目,不妨打开工程看一下你的控制器代码有多少行,是不是非常多?再看一下tableView的代理方法cellForRow和heightForRow的代码是不是也是非常多?里面夹杂着switch和大量if esle的判断逻辑的代码。后期维护看着这些if else是不是特别烦躁?特别是自己在维护前人写的代码,并且还没有注释 一团糟,是不是有更想骂人的冲动?别怕,这里给您提供一种解决思路,让你的tableView代理方法再也没有这种让人头疼的if else判断逻辑,让你的控制器代码量大大减少,并且后期维护成本也大大的减少。

    在说具体解决思路前,先给大家简单复习一下MVC和MVVM,因为今天的主题也是和MVVM有关系。MVC模式大家都很熟悉了,就是Model,View,Controller三层,Model负责数据层,Controller负责业务逻辑层,View负责界面显示层,Model和View通过Controller来实现桥接交互,程序的扩展性很好,好处多多。但是呢,MVC也有它自身的缺陷,那就是控制器太臃肿,如果你想在控制器中定位某一个点是比较麻烦的事。

    那么为什么控制器如此庞大,就是因为tableView的代理方法里的cell 判断逻辑全在控制器,以及网络请求也在控制器发起,另外还有一些其他的业务逻辑。有没有什么更高的模式呢?MVVM模式就是MVC模式的升级版。
    MVVM中Model依然负责数据层,Controller单单负责View的展示和更新,其他业务逻辑不管。View依然负责界面显示。那么ViewController之前负责的业务逻辑现在谁来负责呢?我们再新建一个ViewModel层,处在ViewController层和Model层之间,专门负责业务逻辑,以及网络请求等任务。ViewController从ViewModel中获取数据然后显示在View上,它并不和Model层直接打交道,和Model层直接打交道的是ViewModel层 。
    下面附上一张经典gif图片,帮助大家理解两者之间的关系。 MVC与MVVM的关系 好了,前面的都是回顾一些相关知识,为理解接下来的内容做基础,如果想要深入了解MVC,MVVM可以网上找下,这类文章很多。
    大家可能疑惑到底什么是面向超类编程,其实就是围绕继承这个特性,子类cell继承父类cell,面向父类这个对象来编程,最终对控制器的tableView进行瘦身,也不止是对tableView优化,配合MVVM新建ViewModel可以抽离很大一部分控制器的代码。现在还不清楚没关系,下面会有很详细的描述让你明白。_ 我下面先把大家常用的控制器tableView代理方法的写法,给黏贴出来,然后再用新的面向超类的写法给黏贴出来,大家就可以明显体会到使用面向超类写法的好处了。
    老方式大众写法.png
    面向超类编程写法.png 看到这里,可能会有人吐槽了,新写法就比老式写法少了十几行嘛?其实不是这样的,首先这个demo我只写了三个cell作为例子,真实的项目极少一个控制器只有三个cell吧?控制器的cell越多,好处越明显, 因为在后期不管添加多少cell,控制器tableView的代理方法中的代码几乎都不会增加,相当于构建了一个模版,只需要在新添加cell的内部配置即可。其次,真实的项目也不可能业务逻辑这么简单吧?肯定在if else中嵌套了很多其他的逻辑代码,致使tableView看起来很臃肿。

    面向超类编程的好处:
    1.控制器瘦身。[控制器内部代码量大幅度减少,逻辑更加清晰]
    2.后期维护成本大大降低。[后期如果想添加或者删除cell,只需要新建或者删除一个子类cell,在viewModel中添加或删除一个identifier即可,控制器几乎不用加任何代码]
    面向超类的坏处:
    1.新建更多的cell文件和一个viewModel文件,包大小会响应增加。

    下面就具体讲解面向超类编程瘦身大概要做什么:

    一,新建一个继承自UITableViewCell的父类cell

    #import <UIKit/UIKit.h>
    #import "ResponseNewProgrammeData.h"
    #import "NewProgrammeCellHeightProtocol.h"
    
    //子类需要有回调事件的代理
    @protocol NewProgrammeTableViewCellProtocol <NSObject>
    - (void)cell1DidSelectedRightButton;
    - (void)cell2DidSelectedRightButton;
    - (void)cell3DidSelectedRightButton;
    @end
    
    @interface NewProgrammeBaseCell : UITableViewCell <NewProgrammeCellHeightProtocol>
    @property (nonatomic,   weak) id<NewProgrammeTableViewCellProtocol> delegate;
    @property (nonatomic, strong) ResponseNewProgrammeData * responseNewProgrammeData;
    @end
    

    首先,要包含控制器的数据源,因为子类cell的UI等操作全靠这个父类的数据源。
    其次,要实现NewProgrammeCellHeightProtocol协议,作为计算高度用,具体用法在第二点讲解。
    最后,如果子类cell有点击事件需要回调操作的,可再写一个协议NewProgrammeTableViewCellProtocol作为属性持有,在控制器中将delegate指向控制器作为回调使用。

    二,新建一个NewProgrammeCellHeightProtocol

    #import <Foundation/Foundation.h>
    
    //针对cell的高度写的协议
    @protocol NewProgrammeCellHeightProtocol <NSObject>
    @optional
    + (BOOL)isStaticCell;
    + (float)cellHeight;
    @end
    
    • (BOOL)isStaticCell方法是在子类中使用的,如果当前cell是高度固定的静态cell,就在返回YES,并且在cellHeight方法中返回固定高度。否则返回NO即可,也不需要写+ (float)cellHeight方法。这两个方法会在控制器的heightForRow方法中使用,计算当前cell高度。

    **三,新建控制器所需要的所有cell,且继承自刚才的父类cell **

    #import "NewProgrammeCell1.h"
    
    @interface NewProgrammeCell1 ()
    @property (nonatomic, weak) IBOutlet UILabel *lblName;
    @end
    
    @implementation NewProgrammeCell1
    @synthesize responseNewProgrammeData = _responseNewProgrammeData;
    
    - (void)setResponseNewProgrammeData:(ResponseNewProgrammeData *)responseNewProgrammeData
    {
        _responseNewProgrammeData = responseNewProgrammeData;
        self.lblName.text = _responseNewProgrammeData.string1;
    }
    + (BOOL)isStaticCell
    {
        return YES;
    }
    + (float)cellHeight
    {
        return 44;
    }
    - (IBAction)didPressedPush:(id)sender {
        if (self.delegate && [self.delegate respondsToSelector:@selector(cell1DidSelectedRightButton)]) {
             [self.delegate cell1DidSelectedRightButton];
        }
    }
    @end
    

    这个cell的高度是固定静态的,所以isStaticCell方法返回YES,cellHeight返回高度。
    这个cell中可以拿到控制器数据源,根据自己的需要去获取数据。
    这个cell的didPressedPush方法是模拟需要点击事件的,回调给控制器。
    其他cell的配置都大致是这样的。

    四,新建viewModel文件

    #import "NewProgrammeViewModel.h"
    
    static NSString * const NewProgrammeCell1Identifier = @"NewProgrammeCell1";
    static NSString * const NewProgrammeCell2Identifier = @"NewProgrammeCell2";
    static NSString * const NewProgrammeCell3Identifier = @"NewProgrammeCell3";
    
    @implementation NewProgrammeViewModel
    
    - (NSArray *)getIdentifierList
    {
        return  @[NewProgrammeCell1Identifier,
                  NewProgrammeCell2Identifier,
                  NewProgrammeCell3Identifier];
    }
    - (void)requestData
    {
        self.responseNewProgrammeData = [[ResponseNewProgrammeData alloc] init];
    }
    
    @end
    

    viewModel负责配置控制器所需要注册的cell以及真正要显示的cell,getIdentifierList返回需要注册的所有cell。因为某些页面的cell不是固定显示的,可能根据数据源动态的来配置。同时,viewModel也负责网络请求数据解析等其他业务逻辑代码。这里的viewModel相当于MVVM模式中的胖Model,不仅处理网络请求,还处理页面UI的配置等其他业务逻辑,这样就不会使控制器那么臃肿。

    五,在控制器中做相应代码配置

    - (void)configTableViewCell
    {
        for (NSString * identifer in [self.viewModel getIdentifierList]) {
            [self.tableView registerNib:[UINib nibWithNibName:identifer bundle:nil]  forCellReuseIdentifier:identifer];
        }
    }
    
    #pragma mark - UITableViewDelegate
    
    - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
    {
        return self.viewModel.getIdentifierList.count;
    }
    
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        NSString * cellIdentifier = [self.viewModel.getIdentifierList objectAtIndex:indexPath.row];
        NewProgrammeBaseCell * cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
        cell.delegate = self;
        cell.responseNewProgrammeData = self.viewModel.responseNewProgrammeData;
        return cell;
    }
    
    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        NSString * cellIdentifier  = [self.viewModel.getIdentifierList objectAtIndex:indexPath.row];
        Class<NewProgrammeCellHeightProtocol> cellClass = NSClassFromString(cellIdentifier);
        CGFloat height = 0;
        if ([cellClass isStaticCell]) {
            height  = [cellClass cellHeight];
            return height;
        } else {
            NewProgrammeBaseCell * cell = (NewProgrammeBaseCell*)[self tableView:tableView cellForRowAtIndexPath:indexPath];
            height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingExpandedSize].height;
            return height;
        }
    }
    

    首先,要实例化viewModel对象,在configTableViewCell方法中获取需要注册的所有cell,遍历注册。
    其次,在cellForRow代理方法中,从viewModel对象中获取所有注册的cell的identifier,然后从tableView中获取赋值给父类cell。再针对父类cell做一些赋值操作,也就是分别调用了子类cell,充分利用多态的特性。
    最后,在heightForRow代理方法中,仍然是根据viewModel对象获取所有注册的cell的identifier,再根据identifier反射成对象子类cell的类对象,接着调用cell中的协议方法,来计算高度。
    到这里,面向超类编程瘦身基本思路都说完了,这里有完整的demo点击下载源码,如果喜欢动下小手给个star,谢谢啦😍~自己可以在项目中尝试下,可能在实际应用中会有其他没有想到的问题,比如说,当前控制器的cell最多显示3个,但是在某种情况下是显示2个,有1个不需要显示。那么我们在viewModel中应该怎么配置getIdentifierList数据源呢?我们可以这样做,获取最新的数组。不同的项目业务逻辑不一样,写法也会有差别,具体问题具体分析。

    - (NSArray *)getNewIdentifierList
    {
        NSMutableArray * newList = [[self getIdentifierList] mutableCopy];
        if (**判断条件**) {
            [newList removeObject:NewProgrammeCell2Identifier];
        }
        return [newList copy];
    }
    

    希望本文的瘦身思路可以帮助您,如果此文哪里有纰漏,或者您有什么更好的建议,欢迎提出来,大家一块探讨。iOS开发技术交流qq群: 529560119,提供各种最新权威学习书籍及开发视频

    相关文章

      网友评论

      • R酱哈:我总感觉用RAC会好一些! 个人感觉MVVM也并不是传说中的那么神奇,看起来控制器是简洁了,但实际上并没有解决这个问题,反而复杂化了,维护困难,MVC恰恰相反,简洁明了,方便维护,控制器之所以肿瘤是因为网络层和view层没有控制器好,网络层用类似于MVVM的模式,view进行多拆分就好了!
        R酱哈:@厦大 看了很多关于MVVM的文章 MVVM有他的优势 但劣势也很明确!
        小怡情ifelse:@R酱哈 嗯RAC可以简化很多东西很方便,MVVM维护困难?还是姿势不对,用的合适会比MVC好用的
      • f2c9512de6d1::+1:
        小怡情ifelse:@木偶木 快写文章给你点赞:smile:
      • 欧阳大哥2013:楼主的本质理解是有错误的。MVC里面的M是指业务模型,V是视图,C是M和V的粘合剂也就是控制器。C是不会负责实现业务逻辑的。
        小怡情ifelse:@郭炜1204 竟然有二十年架构师 这是什么级别:scream: 是啊 控制器代码量逻辑少一点,后期维护起来比较方便
        郭炜12045:@厦大 两种方式而已,胖model和瘦model的区别。但是MVC存在的问题还是楼主说的这样,要么控制器臃肿,要么model很胖。楼主的思路很不错,我们公司二十年架构师写的框架最基本的就是这个基于这个思路,所有业务逻辑,可通用的,全部放在基类处理,再加上一个自动绑定,再复杂的一个controller都从来不超过300行代码。其实这种瘦身还是看对数据的理解程度了,哈哈
        小怡情ifelse:@欧阳大哥2013 说的对,理想中的MVC是这样的,但是现实中 控制器反而处理了很多逻辑,所以导致控制器很臃肿。难道我哪里理解错了么?:fearful:
      • 46fdc45388ac:感觉这个采取的就是设计模式里面的state模式的思想,把if-else的分支看成是一个对象的不同状态,所以实现时可以去除if-else,让对象根据实际的状态来实现。
        46fdc45388ac:@厦大 我重构时也用过这种做法,自己的感觉是这种方法的适应性非常有限。因为很少有if-else的结构体大到没法接受程度的情况。而对于if-else结构体巨大但是可以接受的情况,因为用这种子类封装本身代价也不菲,而且这种方式虽然瘦身了但是也把逻辑分离了。因为瘦身本身就是为了让结构清晰便于阅读和理解,而逻辑如果严重分离的话就和初衷违背了
        46fdc45388ac:@厦大 :smile:,恩,本质上类似,就是把if-else的分支抽离成子类的方式,通过把代码封装到子类里面来给主类瘦身。
        小怡情ifelse:@十顿十 恶补了下状态模式state:relieved:有相似之处,不过用多态性描述可能更好一点:smiley:
      • xieminting:当超类臃肿的时候,你是不是打算面向nsobject编程啊?
        小怡情ifelse:@xieminting 不要把自己的想法强加给别人,谢谢。超类中实现子类的点击事件协议,这个缺失有待改进,毕竟都是子类在使用,父类其实没必要关注这些。我也在思考有没有更好的办法,把这些点击事件协议方法拆分到各个子类中,如果你有更好的办法 欢迎分享。

      本文标题:iOS控制器瘦身-面向超类编程

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