标签: MVC,MVP,MVVM

MVC,MVP 和 MVVM 的图示

复杂的软件必须有清晰合理的架构,否则无法开发和维护。

MVC(Model-View-Controller)是*常见的软件架构之一,业界有着广泛应用。它本身很容易理解,但是要讲清楚,它与衍生的 MVP 和 MVVM 架构的区别就不容易了。

读了《Scaling Isomorphic Javascript Code》后,突然意识到,它们的区别非常简单。我用几段话,就可以说清。

%title插图%num

一、MVC

MVC模式的意思是,软件可以分成三个部分。

%title插图%num

  • 视图(View):用户界面。
  • 控制器(Controller):业务逻辑
  • 模型(Model):数据保存

各部分之间的通信方式如下。

%title插图%num

  1. View 传送指令到 Controller
  2. Controller 完成业务逻辑后,要求 Model 改变状态
  3. Model 将新的数据发送到 View,用户得到反馈

所有通信都是单向的。

二、互动模式

接受用户指令时,MVC 可以分成两种方式。一种是通过 View 接受指令,传递给 Controller。

%title插图%num

另一种是直接通过controller接受指令。

%title插图%num

三、实例:Backbone

实际项目往往采用更灵活的方式,以 Backbone.js 为例。

%title插图%num

1. 用户可以向 View 发送指令(DOM 事件),再由 View 直接要求 Model 改变状态。

2. 用户也可以直接向 Controller 发送指令(改变 URL 触发 hashChange 事件),再由 Controller 发送给 View。

3. Controller 非常薄,只起到路由的作用,而 View 非常厚,业务逻辑都部署在 View。所以,Backbone 索性取消了 Controller,只保留一个 Router(路由器) 。

四、MVP

MVP 模式将 Controller 改名为 Presenter,同时改变了通信方向。

%title插图%num

1. 各部分之间的通信,都是双向的。

2. View 与 Model 不发生联系,都通过 Presenter 传递。

3. View 非常薄,不部署任何业务逻辑,称为”被动视图”(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。

五、MVVM

MVVM 模式将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

%title插图%num

唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。Angular 和 Ember 都采用这种模式。

android中MVC,MVP和MVVM三种模式详解析

我们都知道,Android本身就采用了MVC模式,model层数据源层我们就不说了,至于view层即通过xml来体现,而 controller层的角色一般是由activity来担当的。虽然我们项目用到了MVP模式,但是现在人们并没有总结出一种规范,所以MVP模式的写法并不统一,而至于MVVM模式看网上的呼声似乎也是赞同和拍砖的参半,所以对于这几种模式我也不发表意见了,适合自己的才是*好的。下面是我看到的关于这几种模式的几篇文章,整合了一下分享给大家。

相信大家对MVC,MVP和MVVM都不陌生,作为三个*耳熟能详的Android框架,它们的应用可以是非常广泛的,但是对于一些新手来说,可能对于区分它们三个都有困难,更别说在实际的项目中应用了,有些时候想用MVP的,代码写着写着就变成了MVC,久而久之就对它们三个的选择产生了恐惧感,如果你也是这样的人群,那么这篇文章可能会对你有很大的帮助,希望大家看完都会有收获吧!

文章重点:

(1)了解并区分MVC,MVP,MVVM。

(2)知道这三种模式在Android中如何使用。

(3)走出data binding的误区。

(4)了解MVP+data binding的开发模式。

本篇文章的demo我将会上传到我的github上。

水之积也不厚,则其负大舟也无力

正如庄子在逍遥游中说的,如果水不够深,那就没有能够担负大船的力量 。所以在真正开始涉及具体的代码之前,我们要先对MVC,MVP和MVVM做一个初步的了解。如果各位同学对此已经有所了解了,可以选择性跳过这一节。

MVC

MVC,Model View Controller,是软件架构中*常见的一种框架,简单来说就是通过controller的控制去操作model层的数据,并且返回给view层展示,具体见下图

%title插图%num

当用户出发事件的时候,view层会发送指令到controller层,接着controller去通知model层更新数据,model层更新完数据以后直接显示在view层上,这就是MVC的工作原理。

那具体到Android上是怎么样一个情况呢?

大家都知道一个Android工程有什么对吧,有Java的class文件,有res文件夹,里面是各种资源,还有类似manifest文件等等。对于原生的Android项目来说,layout.xml里面的xml文件就对应于MVC的view层,里面都是一些view的布局代码,而各种java bean,还有一些类似repository类就对应于model层,至于controller层嘛,当然就是各种activity咯。大家可以试着套用我上面说的MVC的工作原理是理解。比如你的界面有一个按钮,按下这个按钮去网络上下载一个文件,这个按钮是view层的,是使用xml来写的,而那些和网络连接相关的代码写在其他类里,比如你可以写一个专门的networkHelper类,这个就是model层,那怎么连接这两层呢?是通过button.setOnClickListener()这个函数,这个函数就写在了activity中,对应于controller层。是不是很清晰。

大家想过这样会有什么问题吗?显然是有的,不然为什么会有MVP和MVVM的诞生呢,是吧。问题就在于xml作为view层,控制能力实在太弱了,你想去动态的改变一个页面的背景,或者动态的隐藏/显示一个按钮,这些都没办法在xml中做,只能把代码写在activity中,造成了activity既是controller层,又是view层的这样一个窘境。大家回想一下自己写的代码,如果是一个逻辑很复杂的页面,activity或者fragment是不是动辄上千行呢?这样不仅写起来麻烦,维护起来更是噩梦。(当然看过Android源码的同学其实会发现上千行的代码不算啥,一个RecyclerView.class的代码都快上万行了呢。。)

MVC还有一个重要的缺陷,大家看上面那幅图,view层和model层是相互可知的,这意味着两层之间存在耦合,耦合对于一个大型程序来说是非常致命的,因为这表示开发,测试,维护都需要花大量的精力。

正因为MVC有这样那样的缺点,所以才演化出了MVP和MVVM这两种框架。

MVP

MVP作为MVC的演化,解决了MVC不少的缺点,对于Android来说,MVP的model层相对于MVC是一样的,而activity和fragment不再是controller层,而是纯粹的view层,所有关于用户事件的转发全部交由presenter层处理。下面还是让我们看图

%title插图%num

从图中就可以看出,*明显的差别就是view层和model层不再相互可知,完全的解耦,取而代之的presenter层充当了桥梁的作用,用于操作view层发出的事件传递到presenter层中,presenter层去操作model层,并且将数据返回给view层,整个过程中view层和model层完全没有联系。看到这里大家可能会问,虽然view层和model层解耦了,但是view层和presenter层不是耦合在一起了吗?其实不是的,对于view层和presenter层的通信,我们是可以通过接口实现的,具体的意思就是说我们的activity,fragment可以去实现实现定义好的接口,而在对应的presenter中通过接口调用方法。不仅如此,我们还可以编写测试用的View,模拟用户的各种操作,从而实现对Presenter的测试。这就解决了MVC模式中测试,维护难的问题。

当然,其实*好的方式是使用fragment作为view层,而activity则是用于创建view层(fragment)和presenter层(presenter)的一个控制器。

MVVM

MVVM*早是由微软提出的

%title插图%num

这里要感谢泡在网上的日子,因为前面看到的三张图我都是从它的博客中摘取的,如果有人知道不允许这样做的话请告诉我,我会从我的博客中删除的,谢谢。

从图中看出,它和MVP的区别貌似不大,只不过是presenter层换成了viewmodel层,还有一点就是view层和viewmodel层是相互绑定的关系,这意味着当你更新viewmodel层的数据的时候,view层会相应的变动ui。

我们很难去说MVP和MVVM这两个MVC的变种孰优孰劣,还是要具体情况具体分析。

纸上得来终觉浅,*知此事要躬行

对于程序员来说,空谈是*没效率的一种方式,相信大家看了我上面对于三种模式的分析,或多或少都会有点云里雾里,下面让我们结合代码来看看。

让我们试想一下下面这个情景,用户点击一个按钮A,获取github上对应公司对应仓库中贡献排行*的任的名字,然后我们还会有一个按钮B,用户点击按钮B,界面上排行*的那个人的名字就会换成自己的。

MVC

MVC实现是*简单的。

首先看对应view层的xml文件

[html] view plain copy 在CODE上查看代码片派生到我的代码片
  1. <?xmlversionxmlversion=”1.0″encoding=”utf-8″?>  
  2. <LinearLayoutxmlns:androidLinearLayoutxmlns:android=“http://schemas.android.com/apk/res/android”  
  3.     xmlns:tools=“http://schemas.android.com/tools”  
  4.     android:layout_width=“match_parent”  
  5.     android:layout_height=“match_parent”  
  6.     android:id=“@+id/container”  
  7.     android:orientation=“vertical”  
  8.     tools:context=“.ui.view.MainActivity”  
  9.     android:fitsSystemWindows=“true”>  
  10.     <Button  
  11.         android:text=“get”  
  12.         android:layout_width=“match_parent”  
  13.         android:layout_height=“wrap_content”  
  14.         android:onClick=“get”/>  
  15.     <Button  
  16.         android:text=“change”  
  17.         android:layout_width=“match_parent”  
  18.         android:layout_height=“wrap_content”  
  19.         android:onClick=“change”/>  
  20.     <TextView  
  21.         android:id=“@+id/top_contributor”  
  22.         android:layout_width=“match_parent”  
  23.         android:layout_height=“match_parent”  
  24.         android:gravity=“center”  
  25.         android:textSize=“30sp”/>  
  26. </LinearLayout>  

很简单,两个Button一个TextView

接着看对应controller层的activity

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public class MainActivity extends AppCompatActivity {  
  2.     private ProcessDialog dialog;  
  3.     private Contributor contributor = new Contributor();  
  4.     private TextView topContributor;  
  5.     private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {  
  6.         @Override  
  7.         public void onStart() {  
  8.             showProgress();
  9.         }
  10.         @Override  
  11.         public void onCompleted() {  
  12.         }
  13.         @Override  
  14.         public void onError(Throwable e) {  
  15.         }
  16.         @Override  
  17.         public void onNext(Contributor contributor) {  
  18.             MainActivity.this.contributor = contributor;  
  19.             topContributor.setText(contributor.login);
  20.             dismissProgress();
  21.         }
  22.     };
  23.     @Override  
  24.     protected void onCreate(Bundle savedInstanceState) {  
  25.         super.onCreate(savedInstanceState);  
  26.         setContentView(R.layout.activity_main);
  27.         topContributor = (TextView)findViewById(R.id.top_contributor);
  28.     }
  29.     public void get(View view){  
  30.         getTopContributor(“square”, “retrofit”);  
  31.     }
  32.     public void change(View view){  
  33.         contributor.login = “zjutkz”;  
  34.         topContributor.setText(contributor.login);
  35.     }
  36.     public void getTopContributor(String owner,String repo){  
  37.         GitHubApi.getContributors(owner, repo)
  38.                 .take(1)  
  39.                 .observeOn(AndroidSchedulers.mainThread())
  40.                 .subscribeOn(Schedulers.newThread())
  41.                 .map(new Func1<List<Contributor>, Contributor>() {  
  42.                     @Override  
  43.                     public Contributor call(List<Contributor> contributors) {  
  44.                         return contributors.get(0);  
  45.                     }
  46.                 })
  47.                 .subscribe(contributorSub);
  48.     }
  49.     public void showProgress(){  
  50.         if(dialog == null){  
  51.             dialog = new ProcessDialog(this);  
  52.         }
  53.         dialog.showMessage(“正在加载…”);  
  54.     }
  55.    public void dismissProgress(){  
  56.         if(dialog == null){  
  57.             dialog = new ProcessDialog(this);  
  58.         }
  59.         dialog.dismiss();
  60.     }
  61. }

我们看一下get()方法中调用的getTopContributor方法

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public void getTopContributor(String owner,String repo){  
  2.     GitHubApi.getContributors(owner, repo)
  3.             .take(1)  
  4.             .observeOn(AndroidSchedulers.mainThread())
  5.             .subscribeOn(Schedulers.newThread())
  6.             .map(new Func1<List<Contributor>, Contributor>() {  
  7.                 @Override  
  8.                 public Contributor call(List<Contributor> contributors) {  
  9.                     return contributors.get(0);  
  10.                 }
  11.             })
  12.             .subscribe(contributorSub);
  13. }

 

熟悉rxjava和retrofit的同学应该都明白这是啥意思,如果对这两个开源库不熟悉也没事,可以参考给 Android 开发者的 RxJava 详解和用 Retrofit 2 简化 HTTP 请求这两篇文章。

对于这里大家只要知道这段代码的意思就是去获取github上owner公司中的repo仓库里贡献排名*的那个人。贡献者是通过Contributor这个java bean存储的。

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public class Contributor {  
  2.     public String login;  
  3.     public int contributions;  
  4.   @Override  
  5.     public String toString() {  
  6.         return login + “, ” + contributions;  
  7.     }
  8. }

 

很简单,login表示贡献者的名字,contributor表示贡献的次数。

然后通过rxjava的subscriber中的onNext()函数得到这个数据。

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {  
  2.     @Override  
  3.     public void onStart() {  
  4.         showProgress();
  5.     }
  6.     @Override  
  7.     public void onCompleted() {  
  8.     }
  9.     @Override  
  10.     public void onError(Throwable e) {  
  11.     }
  12.     @Override  
  13.     public void onNext(Contributor contributor) {  
  14.         MainActivity.this.contributor = contributor;  
  15.         topContributor.setText(contributor.login);
  16.         dismissProgress();
  17.     }
  18. };

至于另外那个change按钮的工作大家应该都看得懂,这里不重复了。

好了,我们来回顾一遍整个流程。

首先在xml中写好布局代码。

其次,activity作为一个controller,里面的逻辑是监听用户点击按钮并作出相应的操作。比如针对get按钮,做的工作就是调用GithubApi的方法去获取数据。

GithubApi,Contributor等类则表示MVC中的model层,里面是数据和一些具体的逻辑操作。

说完了流程再来看看问题,还记得我们前面说的吗,MVC在Android上的应用,一个具体的问题就是activity的责任过重,既是controller又是view。这里是怎么体现的呢?看了代码大家发现其中有一个progressDialog,在加载数据的时候显示,加载完了以后取消,逻辑其实是view层的逻辑,但是这个我们没办法写到xml里面啊,包括TextView.setTextView(),这个也一样。我们只能把这些逻辑写到activity中,这就造成了activity的臃肿,这个例子可能还好,如果是一个复杂的页面呢?大家自己想象一下。

MVP

通过具体的代码大家知道了MVC在Android上是如何工作的,也知道了它的缺点,那MVP是如何修正的呢?

这里先向大家推荐github上的一个第三方库,通过这个库大家可以很轻松的实现MVP。好了,还是看代码吧。

首先还是xml

[html] view plain copy 在CODE上查看代码片派生到我的代码片
  1. <?xml version=“1.0” encoding=”utf-8″?>  
  2. <LinearLayout xmlns:android=“http://schemas.android.com/apk/res/android”  
  3.     xmlns:tools=“http://schemas.android.com/tools”  
  4.     android:layout_width=“match_parent”  
  5.     android:layout_height=“match_parent”  
  6.     android:id=“@+id/container”  
  7.     android:orientation=“vertical”  
  8.     tools:context=“.ui.view.MainActivity”  
  9.     android:fitsSystemWindows=“true”>  
  10.     <Button  
  11.         android:text=“get”  
  12.         android:layout_width=“match_parent”  
  13.         android:layout_height=“wrap_content”  
  14.         android:onClick=“get”/>  
  15.     <Button  
  16.         android:text=“change”  
  17.         android:layout_width=“match_parent”  
  18.         android:layout_height=“wrap_content”  
  19.         android:onClick=“change”/>  
  20.     <TextView  
  21.         android:id=“@+id/top_contributor”  
  22.         android:layout_width=“match_parent”  
  23.         android:layout_height=“match_parent”  
  24.         android:gravity=“center”  
  25.         android:textSize=“30sp”/>  
  26. </LinearLayout>  

 

这个和MVC是一样的,毕竟界面的形式是一样的嘛。

接下去,我们看一个接口。

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public interface ContributorView extends MvpView {  
  2.     void onLoadContributorStart();  
  3.     void onLoadContributorComplete(Contributor topContributor);  
  4.     void onChangeContributorName(String name);  
  5. }

这个接口起什么作用呢?还记得我之前说的吗?MVP模式中,view层和presenter层靠的就是接口进行连接,而具体的就是上面的这个了,里面定义的三个方法,*个是开始获取数据,第二个是获取数据成功,第三个是改名。我们的view层(activity)只要实现这个接口就可以了。

下面看activity的代码

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public class MainActivity extends MvpActivity<ContributorView,ContributorPresenter> implements ContributorView {  
  2.     private ProcessDialog dialog;  
  3.     private TextView topContributor;  
  4.     @Override  
  5.     protected void onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         setContentView(R.layout.activity_main);
  8.         topContributor = (TextView)findViewById(R.id.top_contributor);
  9.     }
  10.     @NonNull  
  11.     @Override  
  12.     public ContributorPresenter createPresenter() {  
  13.         return new ContributorPresenter();  
  14.     }
  15.     public void get(View view){  
  16.         getPresenter().get(“square”, “retrofit”);  
  17.     }
  18.     public void change(View view){  
  19.         getPresenter().change();
  20.     }
  21.     @Override  
  22.     public void onLoadContributorStart() {  
  23.         showProgress();
  24.     }
  25.     @Override  
  26.     public void onLoadContributorComplete(Contributor contributor) {  
  27.         topContributor.setText(contributor.toString());
  28.         dismissProgress();
  29.     }
  30.     @Override  
  31.     public void onChangeContributorName(String name) {  
  32.         topContributor.setText(name);
  33.     }
  34.     public void showProgress(){  
  35.         if(dialog == null){  
  36.             dialog = new ProcessDialog(this);  
  37.         }
  38.         dialog.showMessage(“正在加载…”);  
  39.     }
  40.     public void dismissProgress(){  
  41.         if(dialog == null){  
  42.             dialog = new ProcessDialog(this);  
  43.         }
  44.         dialog.dismiss();
  45.     }
  46. }

 

它继承自MvpActivity,实现了刚才的ContributorView接口。继承的那个MvpActivity大家这里不用太关心主要是做了一些初始化和生命周期的封装。我们只要关心这个activity作为view层,到底是怎么工作的。

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public void get(View view){  
  2.     getPresenter().get(“square”, “retrofit”);  
  3. }
  4. public void change(View view){  
  5.     getPresenter().change();
  6. }

get()和change()这两个方法是我们点击按钮以后执行的,可以看到,里面完完全全没有任何和model层逻辑相关的东西,只是简单的委托给了presenter,那我们再看看presenter层做了什么

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public class ContributorPresenter extends MvpBasePresenter<ContributorView> {  
  2.     private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {  
  3.         @Override  
  4.         public void onStart() {  
  5.             ContributorView view = getView();
  6.             if(view != null){  
  7.                 view.onLoadContributorStart();
  8.             }
  9.         }
  10.         @Override  
  11.         public void onCompleted() {  
  12.         }
  13.         @Override  
  14.         public void onError(Throwable e) {  
  15.         }
  16.         @Override  
  17.         public void onNext(Contributor topContributor) {  
  18.             ContributorView view = getView();
  19.             if(view != null){  
  20.                 view.onLoadContributorComplete(topContributor);
  21.             }
  22.         }
  23.     };
  24.     public void get(String owner,String repo){  
  25.         GitHubApi.getContributors(owner, repo)
  26.                 .take(1)  
  27.                 .observeOn(AndroidSchedulers.mainThread())
  28.                 .subscribeOn(Schedulers.newThread())
  29.                 .map(new Func1<List<Contributor>, Contributor>() {  
  30.                     @Override  
  31.                     public Contributor call(List<Contributor> contributors) {  
  32.                         return contributors.get(0);  
  33.                     }
  34.                 })
  35.                 .subscribe(contributorSub);
  36.     }
  37.     public void change(){  
  38.         ContributorView view = getView();
  39.         if(view != null){  
  40.             view.onChangeContributorName(“zjutkz”);  
  41.         }
  42.     }
  43. }

其实就是把刚才MVC中activity的那部分和model层相关的逻辑抽取了出来,并且在相应的时机调用ContributorView接口对应的方法,而我们的activity是实现了这个接口的,自然会走到对应的方法中。好了,我们来捋一捋。

首先,和MVC*大的不同,MVP把activity作为了view层,通过代码也可以看到,整个activity没有任何和model层相关的逻辑代码,取而代之的是把代码放到了presenter层中,presenter获取了model层的数据之后,通过接口的形式将view层需要的数据返回给它就OK了。

这样的好处是什么呢?首先,activity的代码逻辑减少了,其次,view层和model层完全解耦,具体来说,如果你需要测试一个http请求是否顺利,你不需要写一个activity,只需要写一个java类,实现对应的接口,presenter获取了数据自然会调用相应的方法,相应的,你也可以自己在presenter中mock数据,分发给view层,用来测试布局是否正确。

%title插图%num

它竟然说data binding的viewmodel层是binding类,其实不止是这篇文章,其他有一些开发者写的关于data binding的文章里都犯了一样的错误。大家如果也有这样的概念,请务必纠正过来!!说完了错误的概念,那data binding中真正的viewmodel是什么呢?我们还是以之前MVC,MVP的那个例子做引导。首先是view层,这没啥好说的,和MVP一样,只不过多了数据绑定。view层就是xml和activity。

[html] view plain copy 在CODE上查看代码片派生到我的代码片
  1. <layout xmlns:android=“http://schemas.android.com/apk/res/android”>  
  2.     <data>  
  3.         <variable name=“contributor” type=”zjutkz.com.mvvm.viewmodel.Contributor”/>  
  4.     </data>  
  5.     <LinearLayout  
  6.         android:layout_width=“match_parent”  
  7.         android:layout_height=“match_parent”  
  8.         android:id=“@+id/container”  
  9.         android:orientation=“vertical”  
  10.         android:fitsSystemWindows=“true”>  
  11.         <Button  
  12.             android:id=“@+id/get”  
  13.             android:text=“get”  
  14.             android:layout_width=“match_parent”  
  15.             android:layout_height=“wrap_content”  
  16.             android:onClick=“get”/>  
  17.         <Button  
  18.             android:id=“@+id/change”  
  19.             android:text=“change”  
  20.             android:layout_width=“match_parent”  
  21.             android:layout_height=“wrap_content”  
  22.             android:onClick=“change”/>  
  23.         <TextView  
  24.             android:id=“@+id/top_contributor”  
  25.             android:layout_width=“match_parent”  
  26.             android:layout_height=“match_parent”  
  27.             android:gravity=“center”  
  28.             android:textSize=“30sp”  
  29.             android:text=“@{contributor.login}”/>  
  30.     </LinearLayout>  
  31. </layout>  

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public class MainActivity extends AppCompatActivity {  
  2.     private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {  
  3.         @Override  
  4.         public void onStart() {  
  5.             showProgress();
  6.         }
  7.         @Override  
  8.         public void onCompleted() {  
  9.         }
  10.         @Override  
  11.         public void onError(Throwable e) {  
  12.         }
  13.         @Override  
  14.         public void onNext(Contributor contributor) {  
  15.             binding.setContributor(contributor);
  16.             dismissProgress();
  17.         }
  18.     };
  19.     private ProcessDialog dialog;  
  20.     private MvvmActivityMainBinding binding;  
  21.     @Override  
  22.     protected void onCreate(Bundle savedInstanceState) {  
  23.         super.onCreate(savedInstanceState);  
  24.         binding = DataBindingUtil.setContentView(this, R.layout.mvvm_activity_main);  
  25.     }
  26.     public void get(View view){  
  27.         getContributors(“square”, “retrofit”);  
  28.     }
  29.     public void change(View view){  
  30.         if(binding.getContributor() != null){  
  31.             binding.getContributor().setLogin(“zjutkz”);  
  32.         }
  33.     }
  34.     public void showProgress(){  
  35.         if(dialog == null){  
  36.             dialog = new ProcessDialog(this);  
  37.         }
  38.         dialog.showMessage(“正在加载…”);  
  39.     }
  40.     public void dismissProgress(){  
  41.         if(dialog == null){  
  42.             dialog = new ProcessDialog(this);  
  43.         }
  44.         dialog.dismiss();
  45.     }
  46.     public void getContributors(String owner,String repo){  
  47.         GitHubApi.getContributors(owner, repo)
  48.                 .take(1)  
  49.                 .observeOn(AndroidSchedulers.mainThread())
  50.                 .subscribeOn(Schedulers.newThread())
  51.                 .map(new Func1<List<Contributor>, Contributor>() {  
  52.                     @Override  
  53.                     public Contributor call(List<Contributor> contributors) {  
  54.                         return contributors.get(0);  
  55.                     }
  56.                 })
  57.                 .subscribe(contributorSub);
  58.     }
  59. }

如果你对data binding框架是有了解的,上面的代码你能轻松的看懂。

那model层又是什么呢?当然就是那些和数据相关的类,GithubApi等等。

重点来了,viewmodel层呢?好吧,viewmodel层就是是Contributor类!大家不要惊讶,我慢慢的来说。

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public class Contributor extends BaseObservable{  
  2.     private String login;  
  3.     private int contributions;  
  4.     @Bindable  
  5.     public String getLogin(){  
  6.         return login;  
  7.     }
  8.     @Bindable  
  9.     public int getContributions(){  
  10.         return contributions;  
  11.     }
  12.     public void setLogin(String login){  
  13.         this.login = login;  
  14.         notifyPropertyChanged(BR.login);
  15.     }
  16.     public void setContributions(int contributions){  
  17.         this.contributions = contributions;  
  18.         notifyPropertyChanged(BR.contributions);
  19.     }
  20.     @Override  
  21.     public String toString() {  
  22.         return login + “, ” + contributions;  
  23.     }
  24. }

我们可以看到,Contributor和MVP相比,继承自了BaseObservable,有基础的同学都知道这是为了当Contributor内部的variable改变的时候ui可以同步的作出响应。

我为什么说Contributor是一个viewmodel呢。大家还记得viewmodel的概念吗?view和viewmodel相互绑定在一起,viewmodel的改变会同步到view层,从而view层作出响应。这不就是Contributor和xml中那些组件元素的关系吗?所以,大家不要被binding类迷惑了,data binding框架中的viewmodel是自己定义的那些看似是model类的东西!比如这里的Contributor!

话说到这里,那binding类又是什么呢?其实具体对应到之前MVVM的那张图就很好理解了,我们想一下,binding类的工作是什么?

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. binding = DataBindingUtil.setContentView(this,R.layout.mvvm_activity_main);  
  2. binding.setContributor(contributor);

首先,binding要通过DataBindingUtil.setContentView()方法将xml,也就是view层设定。

接着,通过setXXX()方法将viewmodel层注入进去。

由于这两个工作,view层(xml的各个组件)和viewmodel层(contributor)绑定在了一起。

好了,大家知道了吗,binding类,其实就是上图中view和viewmodel中间的那根线啊!!

 

MVVM

MVVM的问题呢,其实和MVC有一点像。data binding框架解决了数据绑定的问题,但是view层还是会过重,大家可以看我上面那个MVVM模式下的activity

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public class MainActivity extends AppCompatActivity {  
  2.     private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {  
  3.         @Override  
  4.         public void onStart() {  
  5.             showProgress();
  6.         }
  7.         @Override  
  8.         public void onCompleted() {  
  9.         }
  10.         @Override  
  11.         public void onError(Throwable e) {  
  12.         }
  13.         @Override  
  14.         public void onNext(Contributor contributor) {  
  15.             binding.setContributor(contributor);
  16.             dismissProgress();
  17.         }
  18.     };
  19.     private ProcessDialog dialog;  
  20.     private MvvmActivityMainBinding binding;  
  21.     @Override  
  22.     protected void onCreate(Bundle savedInstanceState) {  
  23.         super.onCreate(savedInstanceState);  
  24.         binding = DataBindingUtil.setContentView(this, R.layout.mvvm_activity_main);  
  25.     }
  26.     public void get(View view){  
  27.         getContributors(“square”, “retrofit”);  
  28.     }
  29.     public void change(View view){  
  30.         if(binding.getContributor() != null){  
  31.             binding.getContributor().setLogin(“zjutkz”);  
  32.         }
  33.     }
  34.     public void showProgress(){  
  35.         if(dialog == null){  
  36.             dialog = new ProcessDialog(this);  
  37.         }
  38.         dialog.showMessage(“正在加载…”);  
  39.     }
  40.     public void dismissProgress(){  
  41.         if(dialog == null){  
  42.             dialog = new ProcessDialog(this);  
  43.         }
  44.         dialog.dismiss();
  45.     }
  46.     public void getContributors(String owner,String repo){  
  47.         GitHubApi.getContributors(owner, repo)
  48.                 .take(1)  
  49.                 .observeOn(AndroidSchedulers.mainThread())
  50.                 .subscribeOn(Schedulers.newThread())
  51.                 .map(new Func1<List<Contributor>, Contributor>() {  
  52.                     @Override  
  53.                     public Contributor call(List<Contributor> contributors) {  
  54.                         return contributors.get(0);  
  55.                     }
  56.                 })
  57.                 .subscribe(contributorSub);
  58.     }
  59. }

大家有没有发现,activity在MVVM中应该是view层的,但是里面却和MVC一样写了对model的处理。有人会说你可以把对model的处理放到viewmodel层中,这样不是更符合MVVM的设计理念吗?这样确实可以,但是progressDialog的show和dismiss呢?你怎么在viewmodel层中控制?这是view层的东西啊,而且在xml中也没有,我相信会有解决的方案,但是我们有没有一种更加便捷的方式呢?

 

首先还是view层。

[html] view plain copy 在CODE上查看代码片派生到我的代码片
  1. <layout xmlns:android=“http://schemas.android.com/apk/res/android”>  
  2.     <data>  
  3.         <variable name=“contributor” type=”zjutkz.com.mvpdatabinding.viewmodel.Contributor”/>  
  4.     </data>  
  5.     <LinearLayout  
  6.         android:layout_width=“match_parent”  
  7.         android:layout_height=“match_parent”  
  8.         android:id=“@+id/container”  
  9.         android:orientation=“vertical”  
  10.         android:fitsSystemWindows=“true”>  
  11.         <Button  
  12.             android:id=“@+id/get”  
  13.             android:text=“get”  
  14.             android:layout_width=“match_parent”  
  15.             android:layout_height=“wrap_content”  
  16.             android:onClick=“get”/>  
  17.         <Button  
  18.             android:id=“@+id/change”  
  19.             android:text=“change”  
  20.             android:layout_width=“match_parent”  
  21.             android:layout_height=“wrap_content”  
  22.             android:onClick=“change”/>  
  23.         <TextView  
  24.             android:id=“@+id/top_contributor”  
  25.             android:layout_width=“match_parent”  
  26.             android:layout_height=“match_parent”  
  27.             android:gravity=“center”  
  28.             android:textSize=“30sp”  
  29.             android:text=“@{contributor.login}”/>  
  30.     </LinearLayout>  
  31. </layout>  
[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public class MainActivity extends MvpActivity<ContributorView,ContributorPresenter> implements ContributorView {  
  2.     private ProcessDialog dialog;  
  3.     private ActivityMainBinding binding;  
  4.     @Override  
  5.     protected void onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         binding = DataBindingUtil.setContentView(this, R.layout.activity_main);  
  8.     }
  9.     @NonNull  
  10.     @Override  
  11.     public ContributorPresenter createPresenter() {  
  12.         return new ContributorPresenter();  
  13.     }
  14.     public void get(View view){  
  15.         getPresenter().get(“square”, “retrofit”);  
  16.     }
  17.     public void change(View view){  
  18.         if(binding.getContributor() != null){  
  19.             binding.getContributor().setLogin(“zjutkz”);  
  20.         }
  21.     }
  22.     @Override  
  23.     public void onLoadContributorStart() {  
  24.         showProgress();
  25.     }
  26.     @Override  
  27.     public void onLoadContributorComplete(Contributor contributor) {  
  28.         binding.setContributor(contributor);
  29.         dismissProgress();
  30.     }
  31.     public void showProgress(){  
  32.         if(dialog == null){  
  33.             dialog = new ProcessDialog(this);  
  34.         }
  35.         dialog.showMessage(“正在加载…”);  
  36.     }
  37.     public void dismissProgress(){  
  38.         if(dialog == null){  
  39.             dialog = new ProcessDialog(this);  
  40.         }
  41.         dialog.dismiss();
  42.     }
  43. }

然后是presenter层

[java] view plain copy 在CODE上查看代码片派生到我的代码片
  1. public class ContributorPresenter extends MvpBasePresenter<ContributorView> {  
  2.     private Subscriber<Contributor> contributorSub = new Subscriber<Contributor>() {  
  3.         @Override  
  4.         public void onStart() {  
  5.             ContributorView view = getView();
  6.             if(view != null){  
  7.                 view.onLoadContributorStart();
  8.             }
  9.         }
  10.         @Override  
  11.         public void onCompleted() {  
  12.         }
  13.         @Override  
  14.         public void onError(Throwable e) {  
  15.         }
  16.         @Override  
  17.         public void onNext(Contributor topContributor) {  
  18.             ContributorView view = getView();
  19.             if(view != null){  
  20.                 view.onLoadContributorComplete(topContributor);
  21.             }
  22.         }
  23.     };
  24.     public void get(String owner,String repo){  
  25.         GitHubApi.getContributors(owner, repo)
  26.                 .take(1)  
  27.                 .observeOn(AndroidSchedulers.mainThread())
  28.                 .subscribeOn(Schedulers.newThread())
  29.                 .map(new Func1<List<Contributor>, Contributor>() {  
  30.                     @Override  
  31.                     public Contributor call(List<Contributor> contributors) {  
  32.                         return contributors.get(0);  
  33.                     }
  34.                 })
  35.                 .subscribe(contributorSub);
  36.     }
  37. }

model层就是GithubApi等等。

我们使用了data binding框架去节省了类似findViewById和数据绑定的时间,又使用了presenter去将业务逻辑和view层分离。

当然这也不是固定的,你大可以在viewmodel中实现相应的接口,presenter层的数据直接发送到viewmodel中,在viewmodel里更新,因为view和viewmodel是绑定的,这样view也会相应的作出反应。

说到这里,我还是想重复刚才的那句话,*佳实践都是人想出来的,用这些框架根本的原因也是为了尽量低的耦合性和尽量高的可复用性。

iOS 架构模式–解密 MVC,MVP,MVVM以及VIPER架构

在 iOS 中使用 MVC 架构感觉很奇怪? 迁移到MVVM架构又怀有疑虑?听说过 VIPER 又不确定是否真的值得切换?

相信你会找到以上问题的答案,如果没找到请在评论中指出。

你将要整理出你在 iOS 环境下所有关于架构模式的知识。我们将带领大家简要的回顾一些流行的架构,并且在理论和实践上对它们进行比较,通过一些小的例子深化你的认知。如果对文中提到的一些关键词有兴趣,可以点击连接去查看更详细的内容。

掌控设计模式可能会使人上瘾,所以要当心,你可能会对一些问题清晰明了,不再像阅读之前那样迷惑,比如下面这些问题:

谁应该来负责网络请求?Model 还是 Controller ?

应该怎样向一个新的页面的 ViewModel 传入一个 Model ?

谁来创建一个 VIPER 模块,是 Router 还是 Presenter ?

10.png

为什么要关注架构设计?

因为假如你不关心架构,那么总有一天,需要在同一个庞大的类中调试若干复杂的事情,你会发现在这样的条件下,根本不可能在这个类中快速的找到以及有效的修改任何bug.当然,把这样的一个类想象为一个整体是困难的,因此,有可能一些重要的细节总会在这个过程中会被忽略。如果现在的你正是处于这样一个开发环境中,很有可能具体的情况就像下面这样:

  • 这个类是一个UIViewController的子类
  • 数据直接在UIViewController中存储
  • UIView类几乎不做任何事情
  • Model 仅仅是一个数据结构
  • 单元测试覆盖不了任何用例

以上这些情况仍旧会出现,即使是你遵循了Apple的指导原则并且实现了其 MVC(模式,所以,大可不必惊慌。Apple所提出的 MVC 模式存在一些问题,我们之后会详述。

在此,我们可以定义一个好的架构应该具备的特点:

  1. 任务均衡分摊给具有清晰角色的实体
  2. 可测试性通常都来自与上一条(对于一个合适的架构是非常容易)
  3. 易用性和低成本维护

为什么采用分布式?

采用分布式可以在我们要弄清楚一些事情的原理时保持一个均衡的负载。如果你认为你的开发工作越多,你的大脑越能习惯复杂的思维,其实这是对的。但是,不能忽略的一个事实是,这种思维能力并不是线性增长的,而且也并不能很快的到达峰值。所以,能够战胜这种复杂性的*简单的方法就是在遵循 单一功能原则 的前提下,将功能划分给不同的实体。

为什么需要易测性?

其实这条要求对于哪些习惯了单元测试的人并不是一个问题,因为在添加了新的特性或者要增加一些类的复杂性之后通常会失效。这就意味着,测试可以避免开发者在运行时才发现问题—-当应用到达用户的设备,每一次维护都需要浪费长达至少[一周](http://appreviewtimes.com)的时间才能再次分发给用户。

为什么需要易用性?

这个问题没有固定的答案,但值得一提的是,*好的代码是那些从未写过的代码。因此,代码写的越少,Bug就越少。这意味着希望写更少的代码不应该被单纯的解释为开发者的懒惰,而且也不应该因为偏爱更聪明的解决方案而忽视了它的维护开销。

MV(X)系列概要

当今我们已经有很架构设计模式方面的选择:

前三种设计模式都把一个应用中的实体分为以下三类:

  • Models–负责主要的数据或者操作数据的数据访问层,可以想象 Perspn 和 PersonDataProvider 类。
  • Views–负责展示层(GUI),对于iOS环境可以联想一下以 UI 开头的所有类。
  • Controller/Presenter/ViewModel–负责协调 Model 和 View,通常根据用户在View上的动作在Model上作出对应的更改,同时将更改的信息返回到View上。

将实体进行划分给我们带来了以下好处:

  • 更好的理解它们之间的关系
  • 复用(尤其是对于View和Model)
  • 独立的测试

让我们开始了解MV(X)系列,之后再返回到VIPER模式。

MVC的过去

在我们探讨Apple的MVC模式之前,我们来看下传统的MVC模式

1-E9A5fOrSr0yVmc7Kly5C6A.png

传统的MVC

在这里,View并没有任何界限,仅仅是简单的在Controller中呈现出Model的变化。想象一下,就像网页一样,在点击了跳转到某个其他页面的连接之后就会完全的重新加载页面。尽管在iOS平台上实现这这种MVC模式是没有任何难度的,但是它并不会为我们解决架构问题带来任何裨益。因为它本身也是,三个实体间相互都有通信,而且是紧密耦合的。这很显然会大大降低了三者的复用性,而这正是我们不愿意看到的。鉴于此我们不再给出例子。

“传统的MVC架构不适用于当下的iOS开发”

苹果推荐的MVC–愿景

02.png

Cocoa MVC

由于Controller是一个介于View 和 Model之间的协调器,所以View和Model之间没有任何直接的联系。Controller是一个*小可重用单元,这对我们来说是一个好消息,因为我们总要找一个地方来写逻辑复杂度较高的代码,而这些代码又不适合放在Model中。

理论上来讲,这种模式看起来非常直观,但你有没有感到哪里有一丝诡异?你甚至听说过,有人将MVC的缩写展开成(Massive View Controller),更有甚者,为View controller减负也成为iOS开发者面临的一个重要话题。如果苹果继承并且对MVC模式有一些进展,所有这些为什么还会发生?

苹果推荐的MVC–事实

1-PkWjDU0jqGJOB972cMsrnA.png

Realistic Cocoa MVC

Cocoa的MVC模式驱使人们写出臃肿的视图控制器,因为它们经常被混杂到View的生命周期中,因此很难说View和ViewController是分离的。尽管仍可以将业务逻辑和数据转换到Model,但是大多数情况下当需要为View减负的时候我们却无能为力了,View的*大的任务就是向Controller传递用户动作事件。ViewController*终会承担一切代理和数据源的职责,还负责一些分发和取消网络请求以及一些其他的任务,因此它的名字的由来…你懂的。

你可能会看见过很多次这样的代码:

var userCell = tableView.dequeueReusableCellWithIdentifier("identifier") as UserCell
userCell.configureWithUser(user)

这个cell,正是由View直接来调用Model,所以事实上MVC的原则已经违背了,但是这种情况是一直发生的甚至于人们不觉得这里有哪些不对。如果严格遵守MVC的话,你会把对cell的设置放在 Controller 中,不向View传递一个Model对象,这样就会大大增加Controller的体积。

“Cocoa 的MVC被写成Massive View Controller 是不无道理的。”

直到进行单元测试的时候才会发现问题越来越明显。因为你的ViewController和View是紧密耦合的,对它们进行测试就显得很艰难–你得有足够的创造性来模拟View和它们的生命周期,在以这样的方式来写View Controller的同时,业务逻辑的代码也逐渐被分散到View的布局代码中去。

我们看下一些简单的例子:

import UIKit

struct Person { // Model
    let firstName: String
    let lastName: String
}

class GreetingViewController : UIViewController { // View + Controller
    var person: Person!
    let showGreetingButton = UIButton()
    let greetingLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
    }
    
    func didTapButton(button: UIButton) {
        let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
        self.greetingLabel.text = greeting
        
    }
    // layout code goes here
}
// Assembling of MVC
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
view.person = model;

“MVC可以在一个正在显示的ViewController中实现”

这段代码看起来可测试性并不强,我们可以把和greeting相关的都放到GreetingModel中然后分开测试,但是这样我们就无法通过直接调用在GreetingViewController中的UIView的方法(viewDidLoad和didTapButton方法)来测试页面的展示逻辑了,因为一旦调用则会使整个页面都变化,这对单元测试来讲并不是什么好消息。

事实上,在单独一个模拟器中(比如iPhone 4S)加载并测试UIView并不能保证在其他设备中也能正常工作,因此我建议在单元测试的Target的设置下移除”Host Application”项,并且不要在模拟器中测试你的应用。

“View和Controller的接口并不适合单元测试。”

以上所述,似乎Cocoa MVC 看起来是一个相当差的架构方案。我们来重新评估一下文章开头我们提出的MVC一系列的特征:

  • 任务均摊–View和Model确实是分开的,但是View和Controller却是紧密耦合的
  • 可测试性–由于糟糕的分散性,只能对Model进行测试
  • 易用性–与其他几种模式相比*小的代码量。熟悉的人很多,因而即使对于经验不那么丰富的开发者来讲维护起来也较为容易。

如果你不想在架构选择上投入更多精力,那么Cocoa MVC无疑是*好的方案,而且你会发现一些其他维护成本较高的模式对于你所开发的小的应用是一个致命的打击。

“就开发速度而言,Cocoa MVC是*好的架构选择方案。”

MVP 实现了Cocoa的MVC的愿景

021.png

Passive View variant of MVP

这看起来不正是苹果所提出的MVC方案吗?确实是的,这种模式的名字叫做MVC,但是,这就是说苹果的MVC实际上就是MVP了?不,并不是这样的。如果你仔细回忆一下,View是和Controller紧密耦合的,但是MVP的协调器Presenter并没有对ViewController的生命周期做任何改变,因此View可以很容易的被模拟出来。在Presenter中根本没有和布局有关的代码,但是它却负责更新View的数据和状态。

QQ截图20160107154558.png

“假如告诉你UIViewController就是View呢?”

就MVP而言,UIViewController的子类实际上就是Views并不是Presenters。这点区别使得这种模式的可测试性得到了*大的提高,付出的代价是开发速度的一些降低,因为必须要做一些手动的数据和事件绑定,从下例中可以看出:

import UIKit

struct Person { // Model
    let firstName: String
    let lastName: String
}

protocol GreetingView: class {
    func setGreeting(greeting: String)
}

protocol GreetingViewPresenter {
    init(view: GreetingView, person: Person)
    func showGreeting()
}

class GreetingPresenter : GreetingViewPresenter {
    unowned let view: GreetingView
    let person: Person
    required init(view: GreetingView, person: Person) {
        self.view = view
        self.person = person
    }
    func showGreeting() {
        let greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
        self.view.setGreeting(greeting)
    }
}

class GreetingViewController : UIViewController, GreetingView {
    var presenter: GreetingViewPresenter!
    let showGreetingButton = UIButton()
    let greetingLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
    }
    
    func didTapButton(button: UIButton) {
        self.presenter.showGreeting()
    }
    
    func setGreeting(greeting: String) {
        self.greetingLabel.text = greeting
    }
    
    // layout code goes here
}
// Assembling of MVP
let model = Person(firstName: "David", lastName: "Blaine")
let view = GreetingViewController()
let presenter = GreetingPresenter(view: view, person: model)
view.presenter = presenter

关于整合问题的重要说明

MVP是*个如何协调整合三个实际上分离的层次的架构模式,既然我们不希望View涉及到Model,那么在显示的View Controller(其实就是View)中处理这种协调的逻辑就是不正确的,因此我们需要在其他地方来做这些事情。例如,我们可以做基于整个App范围内的路由服务,由它来负责执行协调任务,以及View到View的展示。这个出现并且必须处理的问题不仅仅是在MVP模式中,同时也存在于以下集中方案中。

我们来看下MVP模式下的三个特性的分析:

  • 任务均摊–我们将*主要的任务划分到Presenter和Model,而View的功能较少(虽然上述例子中Model的任务也并不多)。
  • 可测试性–非常好,由于一个功能简单的View层,所以测试大多数业务逻辑也变得简单
  • 易用性–在我们上边不切实际的简单的例子中,代码量是MVC模式的2倍,但同时MVP的概念却非常清晰

“iOS 中的MVP意味着可测试性强、代码量大。”

MVP–绑定和信号

还有一些其他形态的MVP–监控控制器的MVP。

这个变体包含了View和Model之间的直接绑定,但是Presenter仍然来管理来自View的动作事件,同时也能胜任对View的更新。

022.png

Supervising Presenter variant of the MVP

但是我们之前就了解到,模糊的职责划分是非常糟糕的,更何况将View和Model紧密的联系起来。这和Cocoa的桌面开发的原理有些相似。

和传统的MVC一样,写这样的例子没有什么价值,故不再给出。

MVVM–*新且是*伟大的MV(X)系列的一员

MVVM架构是MV(X)系列*新的一员,因此让我们希望它已经考虑到MV(X)系列中之前已经出现的问题。

从理论层面来讲MVVM看起来不错,我们已经非常熟悉View和Model,以及Meditor,在MVVM中它是View Model。

023.png

MVVM

它和MVP模式看起来非常像:

  • MVVM将ViewController视作View
  • 在View和Model之间没有紧密的联系

此外,它还有像监管版本的MVP那样的绑定功能,但这个绑定不是在View和Model之间而是在View和ViewModel之间。

那么问题来了,在iOS中ViewModel实际上代表什么?它基本上就是UIKit下的每个控件以及控件的状态。ViewModel调用会改变Model同时会将Model的改变更新到自身并且因为我们绑定了View和ViewModel,*步就是相应的更新状态。

绑定

我在MVP部分已经提到这点了,但是该部分我们仍会继续讨论。

如果我们自己不想自己实现,那么我们有两种选择:

事实上,尤其是*近,你听到MVVM就会想到ReactiveCoca,反之亦然。尽管通过简单的绑定来使用MVVM是可实现的,但是ReactiveCocoa却能更好的发挥MVVM的特点。

但是关于这个框架有一个不得不说的事实:强大的能力来自于巨大的责任。当你开始使用Reactive的时候有很大的可能就会把事情搞砸。换句话来说就是,如果发现了一些错误,调试出这个bug可能会花费大量的时间,看下函数调用栈:

1-WGIs3XQL1MtKiyApr-m9bg.png

Reactive Debugging

在我们简单的例子中,FRF框架和KVO被过渡禁用,取而代之地我们直接去调用showGreeting方法更新ViewModel,以及通过greetingDidChange 回调函数使用属性。

import UIKit

struct Person { // Model
    let firstName: String
    let lastName: String
}

protocol GreetingViewModelProtocol: class {
    var greeting: String? { get }
    var greetingDidChange: ((GreetingViewModelProtocol) -> ())? { get set } // function to call when greeting did change
    init(person: Person)
    func showGreeting()
}

class GreetingViewModel : GreetingViewModelProtocol {
    let person: Person
    var greeting: String? {
        didSet {
            self.greetingDidChange?(self)
        }
    }
    var greetingDidChange: ((GreetingViewModelProtocol) -> ())?
    required init(person: Person) {
        self.person = person
    }
    func showGreeting() {
        self.greeting = "Hello" + " " + self.person.firstName + " " + self.person.lastName
    }
}

class GreetingViewController : UIViewController {
    var viewModel: GreetingViewModelProtocol! {
        didSet {
            self.viewModel.greetingDidChange = { [unowned self] viewModel in
                self.greetingLabel.text = viewModel.greeting
            }
        }
    }
    let showGreetingButton = UIButton()
    let greetingLabel = UILabel()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.showGreetingButton.addTarget(self.viewModel, action: "showGreeting", forControlEvents: .TouchUpInside)
    }
    // layout code goes here
}
// Assembling of MVVM
let model = Person(firstName: "David", lastName: "Blaine")
let viewModel = GreetingViewModel(person: model)
let view = GreetingViewController()
view.viewModel = viewModel

让我们再来看看关于三个特性的评估:

  • 任务均摊 — 在例子中并不是很清晰,但是事实上,MVVM的View要比MVP中的View承担的责任多。因为前者通过ViewModel的设置绑定来更新状态,而后者只监听Presenter的事件但并不会对自己有什么更新。
  • 可测试性 — ViewModel不知道关于View的任何事情,这允许我们可以轻易的测试ViewModel。同时View也可以被测试,但是由于属于UIKit的范畴,对他们的测试通常会被忽略。
  • 易用性 — 在我们例子中的代码量和MVP的差不多,但是在实际开发中,我们必须把View中的事件指向Presenter并且手动的来更新View,如果使用绑定的话,MVVM代码量将会小的多。

“MVVM很诱人,因为它集合了上述方法的优点,并且由于在View层的绑定,它并不需要其他附加的代码来更新View,尽管这样,可测试性依然很强。”

VIPER–把LEGO建筑经验迁移到iOS app的设计

VIPER是我们*后要介绍的,由于不是来自于MV(X)系列,它具备一定的趣味性。

迄今为止,划分责任的粒度是很好的选择。VIPER在责任划分层面进行了迭代,VIPER分为五个层次:

024.png

VIPER

  • 交互器 — 包括关于数据和网络请求的业务逻辑,例如创建一个实体(数据),或者从服务器中获取一些数据。为了实现这些功能,需要使用服务、管理器,但是他们并不被认为是VIPER架构内的模块,而是外部依赖。
  • 展示器 — 包含UI层面的业务逻辑以及在交互器层面的方法调用。
  • 实体 — 普通的数据对象,不属于数据访问层次,因为数据访问属于交互器的职责。
  • 路由器 — 用来连接VIPER的各个模块。

基本上,VIPER模块可以是一个屏幕或者用户使用应用的整个过程–想想认证过程,可以由一屏完成或者需要几步才能完成,你的模块期望是多大的,这取决于你。

当我们把VIPER和MV(X)系列作比较时,我们会在任务均摊性方面发现一些不同:

  • Model 逻辑通过把实体作为*小的数据结构转换到交互器中。
  • Controller/Presenter/ViewModel的UI展示方面的职责移到了Presenter中,但是并没有数据转换相关的操作。
  • VIPER是*个通过路由器实现明确的地址导航模式。

“找到一个适合的方法来实现路由对于iOS应用是一个挑战,MV(X)系列避开了这个问题。”

例子中并不包含路由和模块之间的交互,所以和MV(X)系列部分架构一样不再给出例子。

 

import UIKit

struct Person { // Entity (usually more complex e.g. NSManagedObject)
    let firstName: String
    let lastName: String
}

struct GreetingData { // Transport data structure (not Entity)
    let greeting: String
    let subject: String
}

protocol GreetingProvider {
    func provideGreetingData()
}

protocol GreetingOutput: class {
    func receiveGreetingData(greetingData: GreetingData)
}

class GreetingInteractor : GreetingProvider {
    weak var output: GreetingOutput!
    
    func provideGreetingData() {
        let person = Person(firstName: "David", lastName: "Blaine") // usually comes from data access layer
        let subject = person.firstName + " " + person.lastName
        let greeting = GreetingData(greeting: "Hello", subject: subject)
        self.output.receiveGreetingData(greeting)
    }
}

protocol GreetingViewEventHandler {
    func didTapShowGreetingButton()
}

protocol GreetingView: class {
    func setGreeting(greeting: String)
}

class GreetingPresenter : GreetingOutput, GreetingViewEventHandler {
    weak var view: GreetingView!
    var greetingProvider: GreetingProvider!
    
    func didTapShowGreetingButton() {
        self.greetingProvider.provideGreetingData()
    }
    
    func receiveGreetingData(greetingData: GreetingData) {
        let greeting = greetingData.greeting + " " + greetingData.subject
        self.view.setGreeting(greeting)
    }
}

class GreetingViewController : UIViewController, GreetingView {
    var eventHandler: GreetingViewEventHandler!
    let showGreetingButton = UIButton()
    let greetingLabel = UILabel()
    override func viewDidLoad() {
        super.viewDidLoad()
        self.showGreetingButton.addTarget(self, action: "didTapButton:", forControlEvents: .TouchUpInside)
    }
    
    func didTapButton(button: UIButton) {
        self.eventHandler.didTapShowGreetingButton()
    }
    
    func setGreeting(greeting: String) {
        self.greetingLabel.text = greeting
    }
    
    // layout code goes here
}
// Assembling of VIPER module, without Router
let view = GreetingViewController()
let presenter = GreetingPresenter()
let interactor = GreetingInteractor()
view.eventHandler = presenter
presenter.view = view
presenter.greetingProvider = interactor
interactor.output = presenter

让我们再来评估一下特性:

  • 任务均摊 — 毫无疑问,VIPER是任务划分中的佼佼者。
  • 可测试性 — 不出意外地,更好的分布性就有更好的可测试性。
  • 易用性 — *后你可能已经猜到了维护成本方面的问题。你必须为很小功能的类写出大量的接口。

什么是LEGO

当使用VIPER时,你的感觉就像是用乐高积木来搭建一个城堡,这也是一个表明当前存在一些问题的信号。可能现在就应用VIPER架构还为时过早,考虑一些更为简单的模式可能会更好。一些人会忽略这些问题,大材小用。假定他们笃信VIPER架构会在未来给他们的应用带来一些好处,虽然现在维护起来确实是有些不合理。如果你也持这样的观点,我为你推荐 Generamba 这个用来搭建VIPER架构的工具。虽然我个人感觉,使用起来就像加农炮的自动瞄准系统,而不是简单的像投石器那样的简单的抛掷。

总结

我们了解了集中架构模式,希望你已经找到了到底是什么在困扰你。毫无疑问通过阅读本篇文章,你已经了解到其实并没有完全的银弹。所以选择架构是一个根据实际情况具体分析利弊的过程。

因此,在同一个应用中包含着多种架构。比如,你开始的时候使用MVC,然后突然意识到一个页面在MVC模式下的变得越来越难以维护,然后就切换到MVVM架构,但是仅仅针对这一个页面。并没有必要对哪些MVC模式下运转良好的页面进行重构,因为二者是可以并存的。

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速