MVC,MVP,MVVM分析

186 阅读11分钟
  • MVC = Model-View-Controller
  • MVP = Model-View-Presenter
  • MVVM = Model-View-ViewModel

这三个架构模式,都分别有三个不同的部件,都有相同的 Model 层和 View 层。Model 为模型层,主要管理业务模型的数据和行为;View 为展示层,其职责就是负责用户界面。三个架构模式目的都是为了解耦 Model 和 View,主要不同点就在于三者实现解耦的方案不同。Controller、Presenter、ViewModel,分别对应着三种不同的解耦方案,三种与 M 和 V 的连接方式。

MVC

下面,我们先从 MVC 开始,正确理解了 MVC,另外两个就非常容易理解了。

在 MVC 中,对应用程序划分出了三种角色:Model、View、Controller。三者有各自的具体用途和职责,并通过彼此的相互通信实现程序功能。对 MVC 了解不深的人其实存在疑惑,比如,Model 是否等于实体类?业务逻辑是归于 Controller 还是 Model?Model 与 View 具体是如何通信的?这些问题你会在下面的内容找到答案。

MVC 三件套中,最难理解的就是 Model,所以我们先来剖析它。前面我们说过,Model 层主要管理业务模型的数据和行为,它既保存程序的数据,也定义了处理该数据的逻辑,所以 Model = 数据 + 业务逻辑。因此,处理业务逻辑属于 Model 的职责,而非 Controller。从数据的维度来说,可以细分为数据的定义、数据的存储、数据的获取。数据的定义其实就是定义数据结构,一般用实体类来定义,以方便在不同角色间传递数据。数据的存储和获取则可能有几种途径:数据库、网络或缓存等。因此,在实际应用中,一个 Model 并不只是简单的一个对象,而是一个更广泛的层级。很多时候,会将 Model 层再进行分解,比如应用于客户端程序时,可以将 Model 层再细分为业务逻辑层、网络层、存储层等,而实体类其实只是贯穿其中的一种数据结构而已。不过,狭义上,当我们说一个 Model 对象的时候,主要是对外部组件而言的,更多是指这个 Model 对外所提供的数据,并不关心数据从何而来。这可能就是让很多人将 Model 误认为就是实体类的原因。

View 是 MVC 里最好理解的,它会接收用户的交互请求并展示数据信息给用户。一个 View 展示的数据可能只是一个 Model 对象的部分数据,也可能是一个 Model 对象的全部数据,甚至可能是多个 Model 对象数据的组合。在 MVC 里,View 被设计为可嵌套的,使用了组合(Composite)模式来实现。比如,列表视图(ListView)或表格视图(TableView)由每个 Item 组成,每个 Item 又可以由图片、文本、按钮等组成。View 是倾向于可复用的,因此,在实际应用中,倾向于将 View 开发成相对通用的组件。

Controller 层主要担任 Model 与 View 之间的桥梁,用于控制程序的流程。Controller 负责确保 View 可以访问到需要显示的 Model 对象数据,并充当 View 了解 Model 更改的渠道。View 接收到用户的交互请求之后,会将请求转发给 Controller,Controller 解析用户的请求之后,就会交给对应的 Model 去处理,而Model对数据进行处理后通过Controller交给view进行界面的更新。因此,理论上,Controller只是担当View和Model一个沟通桥梁而已,应该是很轻的。

MVC 通信机制

image.png

View 和 Model 并不直接通信,而统一通过 Controller 实现数据的传递。Model 将结果告知 Controller,Controller 再去更新 View。

据我所知,苹果提出了这种变种,在苹果之前,有没有其他人提出该变种,我不得而知。另外,很多 Web 框架的设计也是基于这种变种模式的 MVC 的设计思想,比如 SpringMVC 框架,当然,实际的实现比这个复杂得多,但主要设计思路还是 MVC。

这个MVC 通信机制,很多人会将其认为另外一个经典架构模式「三层架构」,即他们认为 MVC 就是三层架构。其实,两者是不同的。三层架构分别为:表现层、业务逻辑层、数据访问层。虽然和 MVC 的通信方式很相似,但划分的各层的职责是不同的,最重要的是,两者的使用范围不同。三层架构是从整个应用程序架构的角度来划分的三层,而 MVC 只是表现层里再进行功能划分的设计方案,因此,要说两者之间有什么关联,那也是 MVC 属于三层架构里的一个子集。

实际应用中,Android 中的 Activity 充当了Controller角色,但很多情况是同时担任 Controller 和部分 View 的角色。另外,在 App 应用里,Controller 从 Model 请求数据时,通常会比较耗时,所以 Model 会异步通知 Controller。

MVC总结

先对 MVC 做一个小总结。MVC 为业务和视图的实现分离提供了开创性的设计思路,让负责业务逻辑的 Model 与负责展示的 View 实现了解耦,从而 Model 的复用性高,多个 View 就可以共享一个 Model,以及,在不修改 Model 的情况下就可以替换 View 的表现形式。

不过,在 App 的实际应用中,又是另一种交互结构。出现了一个 ViewController 角色,如Android中的Activity,不止承担 Controller 的职责,也承接了部分 View 的职责,主要包括对 View 的生命周期管理和数据填充等,而原本的 View 角色就只剩下展示的功能。这种方式的主要优点就是 View 变轻了,可复用性更高了;但缺点也很明显,原本的 Controller 是很轻的,但现在的 ViewController 则是很重的,承担了太多职责。

另外,在很多实际项目中,ViewController 这种模式其实还产生了副作用,当开发人员对 MVC 的理解不深的时候,就会错以为 MVC 的 Controller 就是这样子的,就会错将一些属于 View 和 Model 的代码也移到了 ViewController,导致已经很重的 ViewController 变得更臃肿了。因此,MVC 变成了 Massive View Controller,从而偏离了 MVC 原本的初衷。

为了从根本上解决这个问题,因此,很多 App 项目都改用 MVP 或 MVVM。接下来,我们再来看看 MVP 和 MVVM。

MVP

MVP 结构图

image.png

看到这关系图,你还会发现,这和前篇文章说的变种 MVC 不是一模一样吗?没错,关系图的确是一样的,但背后的实现和角色划分却不太一样,我们后面就讲。至于,这种模式的 MVP 是如何演化而来的,我也不得而知,只知道这已经成为了当代 MVP 的标准结构。

在 MVP 里,三件套各自的职责和依赖关系和变种 MVC 里的职责和依赖关系其实是一样的,但不同的是,MVP 之间的交互主要是通过接口实现的,Model、View、Presenter 都有各自的接口,定义各自的行为方法。针对接口编程,自然就能减低耦合,提高可复用性,以及容易进行单元测试。

之前我们说过,实际应用中的 MVC,UIViewController(ios中) 和 Activity 其实是同时担任了 Controller 和部分 View 的角色的,职责划分不明确,才导致 UIViewController 和 Activity 的代码混乱不堪,越来越臃肿。而采用 MVP 模式,UIViewController 和 Activity 就明确地划分为 View 角色了,原有的 Controller 角色的职责则交由 Presenter 负责,职责清晰了,代码自然也容易清晰了。

MVP 的简单使用

我们就以一个简单的登录案例来说明如何使用 MVP,下图是该案例的类图:

image.png

定义了 4 个接口和 3 个实现类,其中,LoginActivity 是 Android 的 Activity 类,在 iOS 中,则可定义为 LoginViewController。LoginActivity 实现了 LoginView 接口,同时还会持有一个 LoginPresenter 对象。LoginView 和 LoginActivity 都明确划分到 View 层,LoginView 定义了登录流程中涉及到的几个UI层的接口方法,包括显示和隐藏加载框,以及登录失败时的错误信息展示,和登录成功后的处理,这些方法都会在 LoginPresenterImpl 的方法中被调用。Presenter 层定义了两个接口,LoginPresenter 只有一个登录的接口方法,OnLoginFinishedListener 则定义了登录结果的回调方法,两个接口统一由 LoginPresenterImpl 来实现即可。另外,从图中也可看到,LoginPresenterImpl 既持有一个 LoginView 对象,也持有一个 LoginModel 对象,LoginPresenterImpl 其实就是 LoginView 和 LoginModel 之间交互的桥梁。而 Model 层的 LoginModel 则不直接持有 LoginPresenter 对象,只在登录接口中加了一个回调对象的参数。

LoginActivity 下面,我们来简单看看一些关键代码的实现。先来看看 LoginActivity 的关键代码:

public class LoginActivity extends AppCompatActivity implements LoginView {
   private LoginPresenter loginPresenter;
   private ProgressBar progressBar;
   private LoginButton loginBtn;
   private EditText userNameEdt;
   private EditText passwordEdt;

   @Override
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_login);
      progressBar = findViewById(R.id.loading);
      loginBtn = findViewById(R.id.login);
      userNameEdt = findViewById(R.id.username);
      passwordEdt = findViewById(R.id.password);

      loginPresenter = new LoginPresenterImpl();
      loginPresenter.attachView(this);

      loginBtn.setOnClickListener(new View.OnClickListener() {
         @Override
         public void onClick(View v) {
            loginPresenter.login(userNameEdt.getText().toString(),
                    passwordEdt.getText().toString());
         }
      });
   }

   @Override
   public void showLoading() {
      if (!progressBar.isShowing()) {
         progressBar.show();
      }
   }

   @Override
   public void hideLoading() {
      if (progressBar.isShowing()) {
         progressBar.dismiss();
      }
   }

   @Override
   public void onLoginError(String msg) {
      Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
   }

   @Override
   public void onLoginSuccess() {
      Toast.makeText(this, "Success", Toast.LENGTH_SHORT).show();
      startActivity(new Intent(this, MainActivity.class));
   }

   @Override
   protected void onDestroy() {
      super.onDestroy();
      loginPresenter.detachView();
   }
}

代码逻辑比较简单,从代码中可知,在 onCreate() 方法里初始化了 loginPresenter 对象,并绑定了自身作为 LoginView 的引用,并在登录按钮的点击事件中调用了 loginPresenter.login() 方法,从而将登录事件传递给了 loginPresenter。

LoginPresenterImpl

Presenter 就是 View 和 Model 之间的桥梁,我们来看看 LoginPresenterImpl 的关键代码:

public class LoginPresenterImpl implements LoginPresenter, OnLoginFinishedListener {
   private LoginView loginView;
   private LoginModel loginModel;

   public LoginPresenterImpl() {
      this.loginModel = new LoginModelImpl();
   }

   @Override
   public void attachView(LoginView loginView) {
      this.loginView = loginView;
   }

   @Override
   public void detachView() {
      this.loginView = null;
   }

   @Override
   public boolean isViewAttached(){
      return loginView != null;
   }

   @Override
   public void login(String username, String password) {
      loginView.showLoading();
      loginModel.login(username, password, this);
   }

   @Override
   public void onLoginError(String msg) {
      if (isViewAttached()) {
         loginView.hideLoading();
         loginView.onLoginError(msg);
      }
   }

   @Override
   public void onLoginSuccess() {
      if (isViewAttached()) {
         loginView.hideLoading();
         loginView.onLoginSuccess();
      }
   }

LoginModelImpl

最后,LoginModelImpl 就是对登录的业务逻辑处理了,一般是通过网络请求实现登录逻辑,我们来看看伪代码:

public class LoginModelImpl {
   @Override
   public void login(String username, String password, OnLoginFinishedListener listener) {
      apiService.login(username, password)
              .subscribeOn(Schedulers.newThread())
              .observeOn(AndroidSchedulers.mainThread())
              .subscribe(new Subscriber<Response>() {
                 @Override
                 public void onCompleted() {
                 }

                 @Override
                 public void onError(Throwable e) {
                    listener.onLoginError("Server Error")
                 }

                 @Override
                 public void onNext(Response res) {
                    if (res.isSuccess()) {
                       listener.onLoginSuccess();
                    } else {
                       listener.onLoginError(res.getMsg());
                    }
                 }
              });
   }

该示例代码用到了 Retrofit + RxJava,最核心的代码就在 onNext() 方法里,这是请求响应返回后被调用的方法。

至此,MVP 最简单的使用案例就讲解完了。另外,由于每一个业务功能基本都有对应的一套 MVP 的接口,因此,很多时候会将这一套接口封装在一起,组成一套契约,如下所示:

public interface LoginContract {
    public interface LoginView {
       ...
    }

    public interface LoginPresenter {
       ...
    }

    public interface OnLoginFinishedListener {
       ...
    }

    public interface LoginModel {
       ...
    }
}

至此,对 MVP 最简单的用法就演示到这里结束。在实际应用中,应该做一些调整,比如抽离出一个 Base 层,将一些冗余的重复代码封装到对应的 Base 接口或类中,比如抽离出 BaseView、BaseActivity、BasePresenter、BaseListener 等。以及,设计上每一套 MVP 尽量设计成实现单一功能,以便能被复用,从而无需每一个页面都编写单独一套 MVP,尽量使用组合或继承的方式复用 MVP。

MVP 的优缺点

接着,我们来总结下 MVP 有哪些优缺点。先来看看有哪些优点呢:

  1. 我们知道,MVC 模式在 App 实际应用中,Activity 和 UIViewController 既同时担任 Controller 又担任部分 View 的职责,职责不清,导致 Activity 和 UIViewController 容易变得越来越臃肿。而应用 MVP 模式,直接将 Activity 和 UIViewController 划分到 View 层了,职责明确了,自然也避免了 Activity 和 UIViewController 臃肿的问题。
  2. MVP 之间的交互通过接口来进行的,那就便于进行单元测试了,维护性和扩展性也提高了。
  3. M 和 V 之间彻底分离了,降低了耦合性,修改 V 层也不会影响 M 层。

不过,相应地,相比 MVC 也引入了一些弊端:

  1. 由于增加了很多接口的定义,需要编写的代码量暴增,增加了项目的复杂度。
  2. 需要对很多业务模块之间的交互抽象成接口定义,对开发人员的设计能力要求更高了。