iOS用被误解的MVC重构代码

前言

这段时间在重构代码,看了几种模式,最后选择使用被误解的MVC来重构。
下面分别简要介绍MVVM(RAC)、MVP、MVC模式,同时分享一下在重构代码过程中的一些想法。


MVVM

  1. 优点:
  • 双向绑定(data-binding):View的变动,自动反映在ViewModel,反之亦然。使用过Angular 和 Ember 的朋友应该对这点很熟悉。
  • 使得 Model 层和 View 层解耦
  • 结合RAC使用变得神乎其技。特别是面对View与View之间变化关系紧密时RAC能处理得很elegant。
  • 解决了状态量的问题(即无状态)
MVVM

2.缺点:

  • ViewModel承担了大部分MVC中C的事务。【本质上没有解决MVC的massive viewcontroller问题】
  • 数据绑定使得 Bug 调试变难。【由于双向绑定使得View和Model的bug较难定位】
  • 数据绑定需要花费更多的内存。【这是个缺点,但项目实践中我没怎么发觉到】
  • RAC学习成本较高。

3.总结:
MVVM是我最先考虑的模式,原因是被RAC吸引了。
MVVM不失为一个良好的模式,但其缺点由其优点而来,使用过程中较难避免。
关于项目是否使用MVVM,我的观点是:

  • 如果团队人员都能较好领会函数响应式编程思想、bug定位能力较强的话,可以使用。

  • 如果项目的逻辑较为复杂导致状态量较多时可以考虑使用。

    我在业余作品中还是喜欢使用RAC的,在工作上没有使用RAC原因是没有很好的队友,为了项目的可维护性而放弃了RAC。


MVP

MVP 是从MVC演变而来,它们有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。MVP与MVC有一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在MVC中View会直接从Model中读取数据而不是通过 Controller。
看到最后一句的时候相信大家都会有疑问,也许会指着下面这张斯坦福教授的图说MVC的View和Model是没有直接通讯的。

斯坦福MVC

但传统的MVC并不是这样的,百科MVC框架

MVC图1

MVC图2

那么哪个才是真正的MVC?这也是今天主要想跟大家交流的,为了继续这个话题我们先进一步了解MVP模式。

MVP

在MVP中View持有一个Presenter对象,View将界面的响应处理移交给Presenter,而Presenter调用Model进行处理,最后Presenter将Model处理完毕的数据通过Interface的形式递交给View做相应的改变。

MV(X)本是同根生,自然有一些相同点。MVC在每一个平台上都有自己的特点,自然也会稍许不同。所以,你也许会感觉MVP才跟斯坦福教授讲的MVC比较像


重构

在重构前先看几个问题:

  1. iOS中的ViewController到底是MVC中的View还是Controller?还是有独到的看法?
    我在圈子里面做了一个访谈。总数53人,有21人答案是View,30人答案是Controller,2人有独到的看法。当时我很惊讶!尽然对ViewController有这么多不同的看法。在此分享对此的一些看法,如有疏漏,望大家指正。
    做过Android的朋友会发现ViewController与Android的Activity及其相似。我认为ViewController总体上属于MVC中的View层,但与传统的View不一样的是ViewController附带了一些Controller的逻辑,但该逻辑仅为"视图逻辑"(相对于"业务逻辑"而言)。我想这也是apple管它叫"视图控制器"的原因。需要明白的一点是,apple造了一个ViewController,但它和MVC模式都没有限制我们只能把它当做Controller,完全可以自定义一个Controller。
  2. 是什么导致了massive viewcontroller?
    我的理解是因为没有将MVC的各层职能分清,而把视图、业务逻辑都往ViewController上堆,自然就成了massive viewcontroller。
  3. 如果使用MVVM,那么tableview的datasource&delegate应该放在哪里比较合适?如何解决这个问题?
    我没有答案,因为觉得放在MVVM中的哪一层都觉得不合适。望大神告知!

为了解决开发中的问题,我对MVC各层重新做了职能分配。

MVC

注:单独箭头表示直接引用,箭头带圆圈表示以接口引用。

重构后的分层模式与职能分配:

  • View层:由View与ViewController组成。View为单独的视图,ViewController负责多个视图的管理、tableview的datasource & delegate等视图逻辑(这也就解决了问题3)。ViewController会持有一个Controller来传递视图需要响应的业务逻辑。

  • Controller层:负责业务逻辑的处理。Controller持有View和Service的接口引用(Service可根据项目特点选择直接/接口引用)。Controller通过调用Service来处理View层传递下来的业务,并用接口引用递交结果给View层做相应的改变。

  • Model层:由Service与Entity组成。Service为Controller层提供网络与本地数据服务,即Service处理网络请求、数据库、文件等操作。Entity为实体类,负责定义数据的模型。


Show me the code

先说明一下code的场景:
code为一个登录模块,账号类型分老师和学生,并且老师和学生的登录界面不同,但接口调用一致。
code地址:https://github.com/CatchZeng/MVCRefactoring

Model层代码
Entity

@interface CATUserEntity : NSObject

@property (nonatomic,copy) NSString* username;
@property (nonatomic,copy) NSString* gender;
@property (nonatomic)  NSInteger age;

@end

Service

@interface CATLoginService : CATBaseService

-(void)loginWithUsername:(NSString *)username password:(NSString *)password type:(NSInteger)type success:(CATSuccessBlock)success failed:(CATFailedBlock)failed;

@end 


@implementation CATLoginService

-(void)loginWithUsername:(NSString *)username password:(NSString *)password type:(NSInteger)type success:(CATSuccessBlock)success failed:(CATFailedBlock)failed{
    //在这里调用网络、操作数据库等
    //返回数据并解析成相应的数据,这里模拟返回一个User的实体。
    //网络层这里推荐 巧哥使用命令模式封装的YTKNetworking!!! 
    CATUserEntity* user = [[CATUserEntity alloc]init];
    user.gender = @"男";
    user.age = 20;
    if (type == 1) {
        user.username = @"老师";
    }else{
        user.username = @"学生";
    }
    success(@"登录成功!",user);
}

@end

Controller层代码(由于项目特点,这里的Model没有以接口形式引用)

@protocol CATLoginControllerDelegate <NSObject>

-(void)loginSuccessWithData:(id)data;
-(void)loginFailedWithMsg:(NSString *)msg;

@end

@interface CATLoginController : NSObject

-(id)initWith:(id<CATLoginControllerDelegate>)delegate;

-(void)loginWithUsername:(NSString *)username password:(NSString *)password type:(NSInteger)type;

@end



@interface CATLoginController()

@property (nonatomic,weak) id<CATLoginControllerDelegate> delegate;
@property (nonatomic,strong) CATLoginService* service;

@end

@implementation CATLoginController

-(id)initWith:(id<CATLoginControllerDelegate>)delegate{
    self = [super init];
    if (self) {
        _delegate = delegate;
    }
    return self;
}

-(void)loginWithUsername:(NSString *)username password:(NSString *)passwor type:(NSInteger)type{
    WEAKSELF
    [self.service loginWithUsername:username password:passwor type:type success:^(NSString *msg, id data) {
        STRONGSELF
        if (data && strongSelf.delegate && [strongSelf.delegate respondsToSelector:@selector(loginSuccessWithData:)]){//登录成功 && delegate实现了相应的方法
            [strongSelf.delegate  loginSuccessWithData:data];
        }else if(strongSelf.delegate && [strongSelf.delegate respondsToSelector:@selector(loginFailedWithMsg:)]){//登录失败 && delegate实现了相应的方法
            [strongSelf.delegate loginFailedWithMsg:msg];
        }else{
            //handle...
        }
    } failed:^(NSString *msg) {
        //handle error
    }];
}

- (CATLoginService *) service {
    if(!_service) {
        _service = [[CATLoginService alloc] init];
    }
    return _service;
}

@end

View层代码

老师登录界面

@interface CATTeacherLoginViewController ()<CATLoginControllerDelegate>

@property (nonatomic,strong) CATLoginController* controller;
@property (weak, nonatomic) IBOutlet UILabel *labMsg;

@end

@implementation CATTeacherLoginViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"老师登录界面";
}

- (IBAction)loginButtonClicked:(id)sender {
    [self.controller loginWithUsername:@"111" password:@"111" type:1];
}

- (CATLoginController *) controller {
    if(_controller == nil) {
        _controller = [[CATLoginController alloc] initWith:self];
    }
    return _controller;
}

-(void)loginSuccessWithData:(id)data{
    //处理登录成功后的界面呈现
    if (data && [data isKindOfClass:[CATUserEntity class]]) {
        CATUserEntity* user = (CATUserEntity *)data;
        _labMsg.text = [NSString stringWithFormat:@"登录成功!你好:%@",user.username];
    }
}

-(void)loginFailedWithMsg:(NSString *)msg{
    //处理登录失败后的界面呈现
    NSLog(@"登录失败:%@",msg);
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

@end

学生登录界面

@interface CATStudentLoginViewController ()<CATLoginControllerDelegate>

@property (nonatomic,strong) CATLoginController* controller;
@property (weak, nonatomic) IBOutlet UILabel *labMsg;

@end

@implementation CATStudentLoginViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"学生登录界面";
}

- (IBAction)loginButtonClicked:(id)sender {
    [self.controller loginWithUsername:@"111" password:@"111" type:2];
}

- (CATLoginController *) controller {
    if(_controller == nil) {
        _controller = [[CATLoginController alloc] initWith:self];
    }
    return _controller;
}

-(void)loginSuccessWithData:(id)data{
    //处理登录成功后的界面呈现
    if (data && [data isKindOfClass:[CATUserEntity class]]) {
        CATUserEntity* user = (CATUserEntity *)data;
        _labMsg.text = [NSString stringWithFormat:@"登录成功!你好:%@",user.username];
    }
}

-(void)loginFailedWithMsg:(NSString *)msg{
    //处理登录失败后的界面呈现
    NSLog(@"登录失败:%@",msg);
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

@end

小结

重构后的优点:

  1. 各层职能变得更加清晰。
  2. View与Controller彻底解耦。(LoginController以接口形式调用视图层,界面更改对其不产生影响,自身的修改也对视图层不产生影响。)
  3. 代码复用度高。(LoginController可复用于老师和学生的账号登录)
  4. 测试方便。(若要测试登录接口是否可行,可直接实例化 LoginService调用登录接口进行测试)
  5. 把视图逻辑交于ViewController,业务逻辑交于Controller,解决了massive viewcontroller和视图的datasource、delegate代码放置位置等问题。
  6. 任务分配方便。(接口约定完毕后视图层、控制层、模型层可以单独由不同人完成)

缺点:

  1. 多了一些胶水代码。
  2. 需要多定义视图、模型的接口(CATLoginControllerDelegate)
  3. ...

最后

本文的分层方式并不一定适合每个工程,大家可以根据自己工程的情况自行调整。简友【我在睡觉被占用】说得好,其实不用太拘泥与什么模式,去扣定义。只要遵循尽量解耦,关系逻辑清晰的原则就行了。在此表示感谢!

然而,可能只有我误解了MVC。

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

推荐阅读更多精彩内容