iOS用被误解的MVC重构代码

LinTrott 7年前
   <h2>前言</h2>    <p>这段时间在重构代码,看了几种模式,最后选择使用被误解的MVC来重构。</p>    <p>下面分别简要介绍MVVM(RAC)、MVP、MVC模式,同时分享一下在重构代码过程中的一些想法。</p>    <h2>MVVM</h2>    <ol>     <li>优点:</li>    </ol>    <ul>     <li>双向绑定(data-binding):View的变动,自动反映在ViewModel,反之亦然。使用过Angular 和 Ember 的朋友应该对这点很熟悉。</li>     <li>使得 Model 层和 View 层解耦</li>     <li>结合RAC使用变得神乎其技。特别是面对 <strong> <em>View与View之间变化关系紧密</em> </strong> 时RAC能处理得很elegant。</li>     <li>解决了 <strong> <em>状态量</em> </strong> 的问题(即无状态)</li>    </ul>    <p style="text-align:center"><img src="https://simg.open-open.com/show/e83618e8ab549d3cc3761a3340a3aca9.png"></p>    <p style="text-align:center">MVVM</p>    <p>2.缺点:</p>    <ul>     <li>ViewModel承担了大部分MVC中C的事务。【本质上没有解决MVC的 <strong> <em>massive viewcontroller</em> </strong> 问题】</li>     <li>数据绑定使得 Bug 调试变难。【由于双向绑定使得 <strong> <em>View和Model的bug</em> </strong> 较难定位】</li>     <li>数据绑定需要花费更多的内存。【这是个缺点,但项目实践中我没怎么发觉到】</li>     <li>RAC学习成本较高。</li>    </ul>    <p>3.总结:</p>    <p>MVVM是我最先考虑的模式,原因是被RAC吸引了。</p>    <p>MVVM不失为一个良好的模式,但其 <strong> <em>缺点由其优点而来</em> </strong> ,使用过程中较难避免。</p>    <p>关于项目是否使用MVVM,我的观点是:</p>    <ul>     <li>如果团队人员都能较好领会函数响应式编程思想、bug定位能力较强的话,可以使用。</li>     <li> <p>如果项目的逻辑较为复杂导致状态量较多时可以考虑使用。</p> <p>我在业余作品中还是喜欢使用RAC的,在工作上没有使用RAC原因是没有很好的队友,为了项目的可维护性而放弃了RAC。</p> </li>    </ul>    <h2>MVP</h2>    <p>MVP 是从MVC演变而来,它们有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。MVP与MVC有一个重大的区别:在MVP中View并不直接使用Model,它们之间的通信是通过Presenter (MVC中的Controller)来进行的,所有的交互都发生在Presenter内部,而在 <strong> <em>MVC中View会直接从Model中读取数据</em> </strong> 而不是通过 Controller。</p>    <p>看到最后一句的时候相信大家都会有疑问,也许会指着下面这张斯坦福教授的图说MVC的View和Model是没有直接通讯的。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/d10d5ff4b969ca871402f0ee8c77ad17.jpg"></p>    <p style="text-align:center">斯坦福MVC</p>    <p>但传统的MVC并不是这样的,百科 <a href="/misc/goto?guid=4959742137393590177" rel="nofollow,noindex">MVC框架</a></p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/12f48ff3baea54fbbf1c284259fccddd.jpg"></p>    <p style="text-align:center">MVC图1</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/9fdd187111e2965d6e4c95e4dc97ac4c.jpg"></p>    <p style="text-align:center">MVC图2</p>    <p>那么哪个才是真正的MVC?这也是今天主要想跟大家交流的,为了继续这个话题我们先进一步了解MVP模式。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/b5f962bb0fa35430f89bb8793e1ba265.png"></p>    <p style="text-align:center">MVP</p>    <p>在MVP中View持有一个Presenter对象,View将界面的响应处理移交给Presenter,而Presenter调用Model进行处理,最后Presenter将Model处理完毕的数据通过Interface的形式递交给View做相应的改变。</p>    <p>MV(X)本是同根生,自然有一些相同点。MVC在每一个平台上都有自己的特点,自然也会稍许不同。所以,你也许会感觉 <strong>MVP才跟斯坦福教授讲的MVC比较像</strong> !</p>    <h2>重构</h2>    <p>在重构前先看几个问题:</p>    <ol>     <li>iOS中的ViewController到底是MVC中的View还是Controller?还是有独到的看法?<br> 我在圈子里面做了一个访谈。总数53人,有21人答案是View,30人答案是Controller,2人有独到的看法。当时我很惊讶!尽然对ViewController有这么多不同的看法。在此分享对此的一些看法,如有疏漏,望大家指正。<br> 做过Android的朋友会发现ViewController与Android的Activity及其相似。我认为ViewController总体上属于MVC中的View层,但与传统的View不一样的是ViewController附带了一些Controller的逻辑,但该逻辑 <strong>仅为"视图逻辑"</strong> (相对于"业务逻辑"而言)。我想这也是apple管它叫"视图控制器"的原因。需要明白的一点是,apple造了一个ViewController,但它和MVC模式都没有限制我们只能把它当做Controller,完全可以自定义一个Controller。</li>     <li><strong>是什么导致了massive viewcontroller?</strong><br> 我的理解是因为没有将MVC的各层职能分清,而把视图、业务逻辑都往ViewController上堆,自然就成了massive viewcontroller。</li>     <li>如果使用MVVM,那么tableview的datasource&delegate应该放在哪里比较合适?如何解决这个问题?<br> 我没有答案,因为觉得放在MVVM中的哪一层都觉得不合适。望大神告知!</li>    </ol>    <p>为了解决开发中的问题,我对MVC各层重新做了职能分配。</p>    <p style="text-align:center"><img src="https://simg.open-open.com/show/33a7ff0ccc855e46a9a700638455f8f8.png"></p>    <p style="text-align:center">MVC</p>    <p>注:单独箭头表示直接引用,箭头带圆圈表示以接口引用。</p>    <p>重构后的分层模式与职能分配:</p>    <ul>     <li> <p>View层:由View与ViewController组成。View为单独的视图,ViewController负责多个视图的管理、tableview的datasource & delegate等视图逻辑(这也就解决了问题3)。ViewController会持有一个Controller来传递视图需要响应的业务逻辑。</p> </li>     <li> <p>Controller层:负责业务逻辑的处理。Controller持有View和Service的接口引用(Service可根据项目特点选择直接/接口引用)。Controller通过调用Service来处理View层传递下来的业务,并用接口引用递交结果给View层做相应的改变。</p> </li>     <li> <p>Model层:由Service与Entity组成。Service为Controller层提供网络与本地数据服务,即Service处理网络请求、数据库、文件等操作。Entity为实体类,负责定义数据的模型。</p> </li>    </ul>    <h2>Show me the code</h2>    <p>先说明一下code的场景:</p>    <p>code为一个登录模块,账号类型分老师和学生,并且老师和学生的登录界面不同,但接口调用一致。</p>    <p>Model层代码</p>    <p>Entity</p>    <pre>  <code class="language-javascript">@interface CATUserEntity : NSObject    @property (nonatomic,copy) NSString* username;  @property (nonatomic,copy) NSString* gender;  @property (nonatomic)  NSInteger age;    @end</code></pre>    <p>Service</p>    <pre>  <code class="language-javascript">@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</code></pre>    <p>Controller层代码(由于项目特点,这里的Model没有以接口形式引用)</p>    <pre>  <code class="language-javascript">@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</code></pre>    <p>View层代码</p>    <p>老师登录界面</p>    <pre>  <code class="language-javascript">@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</code></pre>    <p>学生登录界面</p>    <pre>  <code class="language-javascript">@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</code></pre>    <h2>小结</h2>    <p>重构后的优点:</p>    <ol>     <li>各层职能变得更加清晰。</li>     <li>View与Controller彻底解耦。(LoginController以接口形式调用视图层,界面更改对其不产生影响,自身的修改也对视图层不产生影响。)</li>     <li>代码复用度高。(LoginController可复用于老师和学生的账号登录)</li>     <li>测试方便。(若要测试登录接口是否可行,可直接实例化 LoginService调用登录接口进行测试)</li>     <li>把视图逻辑交于ViewController,业务逻辑交于Controller,解决了massive viewcontroller和视图的datasource、delegate代码放置位置等问题。</li>     <li>任务分配方便。(接口约定完毕后视图层、控制层、模型层可以单独由不同人完成)</li>    </ol>    <p>缺点:</p>    <ol>     <li>多了一些胶水代码。</li>     <li>需要多定义视图、模型的接口(CATLoginControllerDelegate)</li>     <li>...</li>    </ol>    <h2>最后</h2>    <p>本文的分层方式并不一定适合每个工程,大家可以根据自己工程的情况自行调整。简友【我在睡觉被占用】说得好,其实不用太拘泥与什么模式,去扣定义。只要遵循尽量解耦,关系逻辑清晰的原则就行了。在此表示感谢!</p>    <p>然而,可能只有我误解了MVC。</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/02d0d12a1fa9</p>    <p> </p>