再谈 MVC、MVP 与 MVVM

本贴最后更新于 2109 天前,其中的信息可能已经事过境迁

最近在主导公司的 Android 客户端架构演进工作,我们再次来认识下 MVC、MVP 与 MVVM。

MVC 模式

概述

MVC 基于 Model-View-Controller,即模型-视图-控制器的架构模式进行设计。

  • 模型对象:存储着优雅的数据和业务逻辑,模型类通常用来映射与应用相关的一些事物,如用户、商店里的商品、服务器上的图片或者一段电视节目。模型对象不关心用户界面,它存在的唯一目的就是存储和管理应用数据。应用的全部模型对象组成了模型层。
  • 视图对象:知道如何在屏幕上绘制自己以及如何响应用户的输入,如用户的触摸等。一个简单的经验法则是,凡是能够在屏幕上看见的对象,就是视图对象。 Android 默认自带了很多可配置的视图类。当然,也可以定制开发自己的视图类。应用的全部视图对象组成了视图层。
  • 控制对象:含有应用的逻辑单元,是视图与模型对象的联系纽带。控制对象响应视图对象触发的各类事件,此外还管理着模型对象与视图间的数据流动。 在 Android 的世界里,控制器通常是 Activity、Fragment 或 Service 的一个子类。

上面这段话摘自《Android 编程权威指南》,反复细读,会发现对于模型对象、视图对象、控制对象各自的职责,描述特别的精准。为什么说精准呢,比如:对于视图对象的描述是 凡是能够在屏幕上看见的对象,就是视图对象,我阅读后的理解是视图对象就对应着控件,也就是视图层是由各种控件组成,这不就是 xml 文件嘛,但是又不能说 xml 文件就是视图层,这一点我们在后面会进行分析。

开始学习编程时,我当时对 Model、View、Controller 的理解是,Model 类似于 Java Bean,存储数据时作为实体使用;Controller 为控制器,负责业务逻辑的处理。在去年去杭州参加一次面试时,在面到 MVC 相关问题时,该公司 CTO 也对我进行了一些指点,观点和上面书中的内容基本一致。当时没有深入探究,今天再拿出来细细研读,结合代码,发现自己原来的理解一直都是错误的。

关键的一个错误点就是在于:

  • Model 并不是一个简单的 Java Bean,而应该还负责业务逻辑处理、数据存储读取等
  • Controller 并不是负责业务逻辑,而应该是负责应用的分发逻辑,是 View 和 Model 之间的桥梁,负责比如 View 上用户的一个相关操作应该交给哪个 Model 去处理,一个 Model 处理完数据后该如何展现在 View 等

关于 MVC 各个模块职责问题也可以结合知乎上张恂老师的回答来学习,很不错的解答。

关于 MVC 还有一篇阮一峰老师的文章:MVC,MVP 和 MVVM 的图示,该篇文章和该文章的评论都很不错。前端 MVC 和后端 MVC 的差别是什么呢,也许读了该篇文章和评论后会有更多值得思考的。

工作流程

MVC 模式的工作流程可以归纳为:

  • 视图对象触发各类事件,交由控制对象进行处理
  • 控制对象选取合适的模型对象处理事件
  • 模型对象处理完事件后,再由控制对象通知视图对象进行更新

demo

附上一个使用 MVC 模式的 demo:

Model 层如下:

public class WeatherModelImpl implements WeatherModel{
    @Override
    public void getWeather(String cityId, final OnWeatherListener listener) {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url("http://www.weather.com.cn/data/sk/" + cityId)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                listener.onError(e.toString());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Gson gson = new Gson();
                Weather weather = gson.fromJson(response.body().string(), Weather.class);
                listener.onSuccess(weather);
            }
        });
    }
}

View 层如下:

Controller 层如下:

public class WeatherActivity extends AppCompatActivity {

    @BindView(R.id.et_city_id)
    EditText mEtCityId;
    @BindView(R.id.tv_weather_description)
    TextView mTvWeatherDescription;
    @BindView(R.id.tv_weather_temperature)
    TextView mTvWeatherTemperature;

    private WeatherModel mWeatherModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_weather);
        ButterKnife.bind(this);

        mWeatherModel = new WeatherModelImpl();
    }

    @OnClick(R.id.tv_get_weather)
    public void onViewClicked() {
        mWeatherModel.getWeather(mEtCityId.getText().toString().trim(), new OnWeatherListener() {
            @Override
            public void onSuccess(Weather weather) {
                mTvWeatherDescription.setText(weather.getDescription());
                mTvWeatherTemperature.setText(weather.getTemperature());
            }

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

demo 分析

在上面 demo 中,WeatherModelImpl 为 Model 对象,xml 布局为 View 对象,WeatherActivity 为 Controller 对象。

我们为获取天气信息这个动作(请求网络数据),定义了一个接口 WeatherModel,该接口的实现类中进行网络请求、回调 UI 更新操作,WeatherModel 就是前面所述的 Model 对象;WeatherActivity 作为 Controller 对象,仅仅负责在合适的时机调度相应的 Model 对象执行操作,在 demo 中就是在用户点击获取天气按钮时,调用 WeatherModel 对象的方法去请求数据,并在数据获取回调成功后,通知 View 层进行 UI 更新。

再回头来回顾下我们刚开始学习 Android 时的,那种把代码一股脑写到 Activity 中的写法:

  • 视图对象对应着 xml 布局文件,然而布局文件能做的事情特别少,很多视图绘制更新、事件处理实际是在 Activity 中完成的,所以 Activity 承担了部分 View 层的工作
  • 业务逻辑也大部分写在了 Activity 中,所以 Activity 还承担了大部分 Model 的工作
  • View 和 Model 的交互也是在 Activity 中,所以 Activity 还承担了 Controller 的全部工作

由此看来,这就是项目中 Activity 动辄上千行的原因,代码极度臃肿,在后期维护中,View 的变更、业务逻辑的变更、控制逻辑的变更,都需要直接对 Activity 进行修改,简直就是一场噩梦。MVC 分层后,业务逻辑的修改我们只需要修改对应的 Model 层即可。

MVC 与三层架构

关于 MVC 与三层架构的区别及联系,我感觉自己现在依然无法很明白。此处引用一下网上的一种说法:

  • 三层架构分层:表示层、业务层、持久层。表示层负责接收用户请求、转发请求、显示数据等;业务层负责组织业务逻辑;持久层负责持久化业务对象
  • 表示层最常用的架构模式就是 MVC

个人理解:

  • 由于现在 Android 客户端更多的数据是直接从服务端获取并展示的,所以三层架构在 Android 客户端的应用并不是很多
  • 当然,三层架构也是可以应用于 Android 客户端的
  • 一个疑问:MVC 中 Model 层的数据持久化和三层架构中的持久层有什么区别与联系呢

MVP 模式

概述

经过了 MVC 的分层后,代码清晰了许多,但是还有问题:Model 层确实从 Activity 中分离出来了,而 View 层的部分代码还在 Activity 中,也就是说现在 Activity 承担着 View 层的部分功能和 Controller 的全部功能。下面该是 MVP 登场的时候了~

我们让 Activity 来专一负责 View 层的功能(毕竟 xml 文件没法增加额外的功能,那就交给老大哥 Activity 来做就好了)。原来 Activity 负责的 Controller 层全部功能,分离出来命名为 Presenter 层。Model 层不变。这样 MVP 的层次划分就出来了,来看个 demo:

demo

首先来看起到纽带作用的 Presenter:

public class WeatherPresenter implements WeatherContract.Presenter {
    private WeatherContract.View mWeatherView;
    private WeatherModel mWeatherModel;

    public WeatherPresenter(WeatherContract.View weatherView) {
        mWeatherView = weatherView;
        mWeatherModel = new WeatherModel();
    }

    @Override
    public void getWeather(String cityId) {
        mWeatherModel.getWeather(cityId, new OnWeatherListener() {
            @Override
            public void onSuccess(Weather weather) {
                mWeatherView.onUpdateWeatherSuccess(weather);
            }

            @Override
            public void onError(String msg) {
                mWeatherView.onUpdateWeatherFail(msg);
            }
        });
    }
}

Model 层代码如下:

public class WeatherModel implements WeatherContract.Model {

    @Override
    public void getWeather(String cityId, final OnWeatherListener listener) {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url("http://www.weather.com.cn/data/sk/" + cityId)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                listener.onError(e.toString());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Gson gson = new Gson();
                Weather weather = gson.fromJson(response.body().string(), Weather.class);
                listener.onSuccess(weather);
            }
        });
    }
}

View 层代码如下:

public class WeatherActivity extends AppCompatActivity implements WeatherContract.View {
    @BindView(R.id.et_city_id)
    EditText mEtCityId;
    @BindView(R.id.tv_weather_description)
    TextView mTvWeatherDescription;
    @BindView(R.id.tv_weather_temperature)
    TextView mTvWeatherTemperature;
    private WeatherContract.Presenter mPresenter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_weather);
        ButterKnife.bind(this);
        mPresenter = new WeatherPresenter(this);
    }

    @OnClick(R.id.tv_get_weather)
    public void onViewClicked() {
        mPresenter.getWeather(mEtCityId.getText().toString().trim());
    }

    @Override
    public void onUpdateWeatherSuccess(Weather weather) {
        mTvWeatherDescription.setText(weather.getDescription());
        mTvWeatherTemperature.setText(weather.getTemperature());
    }

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

其中接口定义代码如下:

public interface WeatherContract {
    interface Model {
        void getWeather(String cityId, OnWeatherListener listener);
    }

    interface View {
        void onUpdateWeatherSuccess(Weather weather);

        void onUpdateWeatherFail(String msg);
    }

    interface Presenter {
        void getWeather(String cityId);
    }
}

demo 分析

通过 demo 可以看到,Presenter 作为 Model 和 View 之间的纽带,会同时持有 Model 层对象和 View 层对象,这样做的好处是,在需要的时候可以调用合适的 Model 对象去处理请求或数据,以及在数据处理完毕后调用 View 的方法去更新视图。

Activity 作为 Android 四大组件之一,为应用的主要入口。在 MVP 模式中,Activity 作为 View 层对象,用户操作 View 对象触发的各类事件都交由 Presenter 层对象进行处理。所以让 Activity 对象持有 Presenter 对象,可以在事件触发时调用 Presenter 的相应方法来处理事件。同时在 Presenter 的构造器中将 Activity 自身传递过去,使得 Presenter 持有 Activity 的引用。

MVP 与 MVC 对比

通过上面的流程图可以看到,由 MVC 到 MVP 最根本的变化是:MVC 中 Activity 所负责的控制器功能,单独抽离出来并改名为 Presenter,Activity 专心负责视图层。

事件流动方向不变。

MVVM 模式

概述

在介绍 MVVM 之前,我们先来回顾下 MVP 模式中的交互流程:

  • 用户触发 View 层事件时,View 对象会调用 Presenter 对象的方法把事件传递给 Presenter 进行处理,Presenter 会选择合适的 Model 来处理事件
  • 当 Model 处理完业务逻辑需要更新 UI 时,Model 会去通知 Presenter,Presenter 去获取到 View 层的控件对象,然后调用控件的相应方法进行更新 UI。

MVVM 模式在 MVP 模式的基础上进行了再次升级,把 View 与 Presenter 之间的交互做成全自动化了,不需要我们程序猿再去手动调用接口方法了,并且把 Presenter 再次改名为 ViewModel,先来看流程图:

View 和 ViewModel 之间的自动化交互在 Android 平台上有一个官方支持库 DataBinding 来帮我们实现,我们只需要手动将 ViewModel 和 View 绑定到一起即可,用户触发 View 层事件会自动调用 ViewModel 层的方法,Model 处理完业务逻辑通知 ViewModel 后,ViewModel 对自身的状态进行变更,此变更会自动把 View 层的 UI 进行更新。

demo

View 层代码:

public class WeatherActivity extends AppCompatActivity {
    private ActivityWeather2Binding mBinding;
    private WeatherViewModel mWeatherViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_weather2);
        mWeatherViewModel = new WeatherViewModel(new WeatherModel());
        mBinding.setWeatherViewModel(mWeatherViewModel);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <variable
            name="weatherViewModel"
            type="com.xuchongyang.architecturedemo.mvvm.WeatherViewModel"/>
    </data>

    <android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="16dp"
        tools:context=".mvc.WeatherActivity">

        <EditText
            android:id="@+id/et_city_id"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toStartOf="@id/tv_get_weather"/>

        <TextView
            android:id="@+id/tv_get_weather"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="获取天气"
            android:onClick="@{() -> weatherViewModel.getWeather()}"
            app:layout_constraintStart_toEndOf="@id/et_city_id"
            app:layout_constraintTop_toTopOf="@id/et_city_id"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="@id/et_city_id"/>

        <TextView
            android:id="@+id/tv_weather_description"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            tools:text="天气:"
            android:text="@{weatherViewModel.mDescription}"
            app:layout_constraintTop_toBottomOf="@id/et_city_id"/>

        <TextView
            android:id="@+id/tv_weather_temperature"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            tools:text="气温:"
            android:text="@{weatherViewModel.mTemperature}"
            app:layout_constraintTop_toBottomOf="@id/tv_weather_description"/>

    </android.support.constraint.ConstraintLayout>
</layout>

ViewModel 层代码:

public class WeatherViewModel {
    public final ObservableField<String> mDescription = new ObservableField<>();
    public final ObservableField<String> mTemperature = new ObservableField<>();

    private WeatherModel mWeatherModel;

    public WeatherViewModel(WeatherModel weatherModel) {
        mWeatherModel = weatherModel;
    }

    public void getWeather() {
        mWeatherModel.getWeather("", new OnWeatherListener() {
            @Override
            public void onSuccess(Weather weather) {
                mDescription.set(weather.getDescription());
                mTemperature.set(weather.getTemperature());
            }

            @Override
            public void onError(String msg) {

            }
        });
    }
}

Model 层代码:

public class WeatherModel implements IWeatherModel {
    @Override
    public void getWeather(String cityId, final OnWeatherListener listener) {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url("http://www.weather.com.cn/data/sk/" + cityId)
                .build();
        client.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                listener.onError(e.toString());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                Gson gson = new Gson();
                Weather weather = gson.fromJson(response.body().string(), Weather.class);
                listener.onSuccess(weather);
            }
        });
    }
}

demo 分析

mBinding = DataBindingUtil.setContentView(this, R.layout.activity_weather2);
mWeatherViewModel = new WeatherViewModel(new WeatherModel());
mBinding.setWeatherViewModel(mWeatherViewModel);

从上面代码中可以看到 View 和 ViewModel 的绑定过程,首先获取到 DataBinding 帮我们生成的 View 对象对应的 DataBinding 对象,然后为其设置 ViewModel,这样就将二者绑定到一起了。

<TextView
    android:id="@+id/tv_get_weather"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="获取天气"
    android:onClick="@{() -> weatherViewModel.getWeather()}"
    app:layout_constraintStart_toEndOf="@id/et_city_id"
    app:layout_constraintTop_toTopOf="@id/et_city_id"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintBottom_toBottomOf="@id/et_city_id"/>

上面代码可以看到,用户触发的 View 层事件,可以直接在 View 层写代码,如上面示例所示,用户点击时,DataBinding 库会自动调用 ViewModel 的 getWeather 方法。

mWeatherModel.getWeather("", new OnWeatherListener() {
    @Override
    public void onSuccess(Weather weather) {
        mDescription.set(weather.getDescription());
        mTemperature.set(weather.getTemperature());
    }

    @Override
    public void onError(String msg) {

    }
});

上面代码可以看到,当 Model 处理完业务逻辑,需要更新 UI 时,直接更改 ViewModel 层的属性状态即可,View 层 UI 会自动进行更新。

MVVM 与 MVP 对比

综上可以看到,MVVM 相比于 MVP 升级的地方在于:View 调用 Presenter 接口的操作和 Presenter 获取 View 控件并更新 UI 的操作,全部自动化了,变为双向动态绑定。Presenter 和 Model 的交互过程不变。

总结

至此,我们已经分析完了 Android 开发中三种常见的架构模式,最普通的编码方法将视图、业务逻辑、应用逻辑全部放在 Activity 中,造成 Activity 异常臃肿,维护困难;那我们就把 Activity 中业务逻辑部分的代码抽取出来,作为 Model 层,使业务和视图进行分离,代码耦合性得到有效降低,这就是 MVC 模式;然而虽然业务逻辑代码被抽出来了,Activity 依然负责着视图层代码和应用控制逻辑代码,解耦依旧不彻底,那我们就把应用控制逻辑代码也抽出来,让 Activity 专心负责视图代码,这就是 MVP 模式;由于视图代码和控制逻辑代码分离了,在更新视图时就要考虑到 Activity 的声明周期问题,并且视图和 Presenter 间的接口粒度也不好控制,那我们就把视图和 Presenter 之间的交互做成全自动的吧,我们就不需要手动调用接口了,这就是 MVVM 模式。

可以看到,每一种模式都是在前一种模式的基础上进行一定的优化,解决了一个个痛点。但任何事物都是两面性的,升级到最后的 MVVM 模式后,并不是就完美了,由于代码的封装,代码的可读性变差,并且造成在调试时就比较困难。当然,模式是死的,项目和程序员是活的,我们可以选择适合自己适合项目的模式去编码,保持高内聚低耦合,遵从良好的编码规范。

参考资料

如何理解 MVC 中的 Model?

三层构架和 MVC 不同吗?

MVVM 在 Android 上的正确使用方式

  • 架构

    我们平时所说的“架构”主要是指软件架构,这是有关软件整体结构与组件的抽象描述,用于指导软件系统各个方面的设计。另外还有“业务架构”、“网络架构”、“硬件架构”等细分领域。

    142 引用 • 442 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...