Loading...
墨滴

牛角尖

2021/03/22  阅读:56  主题:默认主题

Android 老生常谈之MVC与MVP

写在前面

说起 MVC,大家都不陌生,算起来,自1978年被第一次提出,MVC 已经走过了40多个年头。在这40多年中,MVC 演化出了各种各样的分支,或者说变形—— MVP、MVVM 自然隶属其中。

这篇文章中,就介绍下 MVC 的几个比较有代表性的版本演进,及 MVP 的一些变形。

MVC 结构的出现及演进

1、MV-Editor 与 MVC 的出现

MVC 第一次由 Trygve Reenskaug 提出时,是以 “Model-View-Editor” 的形式出现的,几个月后才被正式命名为 “Model-View-Controller” 。

image.png

在这个版本中,如上图 1 所示,Editor 是被作为一个特殊的 Controller,其作用在于,在需要修改 View 时,要从 View 中获取一个特殊的 Controller 的临时对象,即 Editor,要通过 Editor 来完成对 Model 和 View 的操作。

2、典型的 MVC 结构(Model 1)

后来,MVC 又出现了许多的分支和演进,不过万变不离其宗,这一阶段的 MVC 结构大体上还是如下图 2 所示,被称为 “典型的 MVC 结构”。(为了与后面的 Model 2 相区分,我们姑且称这个阶段的 Model 为 Model 1。)

image.png

从上图大家不难发现,MVC 间两两互相耦合,尤其是 Model 与 View 间的耦合,会显著区别于后面出现的 Model 2 。

这种结构的 MVC 出现的比较早,大多数 Android 开发者从最起初接触 MVC 就是 Model 2 结构的了。Model 2 先按下不表,后面再说。

在这种典型的 MVC 结构中,View 中用户事件发生后,会通过 Controller 通知 Model 进行数据存储、网络请求等;而 Model 中数据等的变化,则是通过 View 在 Model 中注册观察者,来保持 View 中 UI 的展示与 Model 中数据的同步。

需要了解的是,那时候是没有像现在的 Android 、iOS、WP、塞班OS 这样的强大的 GUI 框架的,甚至连 Web 程序还没有出现。那时候的主体是桌面应用程序。

下面我们以 Java 桌面程序的代码为例,帮助理解一下这种典型的 MVC 结构(代码来源于《Android 源码设计模式解析与实战》)。

程序清单 1-1     MVC 下读取 / 清除图片的 Model

import java.awt.Image;
import java.net.URL;

import javax.imageio.ImageIO;

public class Model{
    private String imgUrl = "http://pic33.nipic.com/20131007/13639685_123501617185_2.jpg";
    // 展示的图像
    private Image mImage;
    // Model 状态改变监听器
    private OnStateChangeListener mListener;

    public interface OnStateChangeListener{
        void OnStateChanged(Image image);
    }

    public Model(){
        try {
            // 初始化时预加载一个图片作为默认图
            mImage = ImageIO.read(new URL(imgUrl));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void setOnStateChangeListener(OnStateChangeListener listener){
        mListener = listener;
    }

    public void loadImage(){
        new Thread(() -> {
            try {
                // 模拟耗时操作
                Thread.sleep(3000);
                // 获取图像
                mImage = ImageIO.read(new URL(imgUrl));
                // 回传给 View
                if(null != mListener){
                    mListener.OnStateChanged(mImage);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();;
    }

    public void clearImage(){
        // 清除图像
        mImage = null;
        // 回传给 View
        if(null != mListener){
            mListener.OnStateChanged(mImage);
        }
    }

    public Image getImage(){
        return mImage;
    }
}

可以看到,代码中的 Model 类实现了两个与数据存储、网络请求相关的方法:loadImage() 和 clearImage() ,同时又提供了一个监听器 OnStateChangeListener(即上文中所说的观察者),用于当 Image 的状态发生变化时,通知监听器的注入方,注入方是通过 setOnStateChangeListener() 方法进行注入的。而监听器的注入方,就是 View 了。

程序清单 1-2     MVC 下读取 / 清除图片的 View

import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.Image;
import java.awt.event.ActionListener;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class View extends JFrame implements Model.OnStateChangeListener{
    private final JButton mButtonLoad = new JButton("Load");
    private final JButton mButtonClear = new JButton("Clear");

    private final JLabel mLabelImage = new JLabel();

    public View(Model model){
     // 构造 View 时加载默认图
        mLabelImage.setIcon(new ImageIcon(model.getImage()));

        JPanel rootPanel = new JPanel();
        rootPanel.setLayout(new BorderLayout());
        rootPanel.add(mLabelImage, BorderLayout.CENTER);

        JPanel btnPanel = new JPanel();
        btnPanel.setLayout(new FlowLayout());
        btnPanel.add(mButtonLoad);
        btnPanel.add(mButtonClear);

        rootPanel.add(btnPanel, BorderLayout.SOUTH);

        setContentPane(rootPanel);
        pack();
        setTitle("MVC");
        setVisible(true);
    }

    @Override
    public void OnStateChanged(Image image) {
        if(null == image){
            mLabelImage.setIcon(new ImageIcon());
        }else{
            mLabelImage.setIcon(new ImageIcon(image));
        }
    }

    public void addLoadListener(ActionListener listener){
        mButtonLoad.addActionListener(listener);
    }

    public void addClearListener(ActionListener listener){
        mButtonClear.addActionListener(listener);
    }

}

上述代码中大家看着面生的类基本都来源于 Java Swing,用于构建 Java 桌面版程序,现在用的很少了,大家不了解的也不需要做太多关注了。

我们重点看 View 实现了 Model 中的监听接口 OnStateChangeListener,这意味着当调用 Model 的 setOnStateChangeListener(OnStateChangeListener listener) 方法时,是可以将 View 作为参数注入到 Model 中的,也就实现了 Model 中图片状态发生变化时可以通知到 View。

至于方法 addLoadListener() 和 addClearListener() 的作用,我们来看下 Controller 吧。

程序清单 1-3     MVC 下读取 / 清除图片的 Controller

public class Controller{
    private Model mModel;
    private View mView;

    public Controller(Model model, View view){
        mModel = model;
        mView = view;

        mView.addLoadListener(e -> model.loadImage());
        mView.addClearListener(e -> model.clearImage());
    }
}

可以看到,Controller 管理了 Model 和 View ,同时调用了 View 中的方法 addLoadListener() 和 addClearListener() 为 Button 设置了监听,用于用户点击时调用 Model 加载或者清除图片。

示例如下图 。 image.png 通过上述代码示例,相信大家已经看到典型的 MVC 结构与大家所熟知的 MVC 结构(Model 2)的不同了,典型的 MVC 结构中 Model 与 View 是强耦合的,甚至是互相注入的。

后来,随着 Web 程序的流行,典型的 MVC 结构迅速演进为了 Model 2 的结构。

需要提醒大家注意的是,有些书,以及大多数的关于 MVC 和 MVP 比较的技术博客,对 MVC 的讨论都止步于此。这是不大合适的,因为后续的 MVC 的演进才是最终演进出 MVP 的关键。

3、我们熟知的 MVC 结构(Model 2)

Model 2 结构的 MVC 如下图所示。

image.png

这个结构,我们可以从 JavaWeb 类型的程序来理解。

在 JavaWeb 中,JavaBean、JSP、Servlet 分别对应于 M、V、C 。

1、当用户在 Browser 中的 JSP 页面上有交互请求时,这些交互请求会被 Browser 交给 Controller,由 Controller 根据实际情况对 Model 进行操作;

2、当 Model 操作有结果反馈时会反馈给 Controller ;

3、再由 Controller 交给 View ;

4、最后由 View 在 Browser 上进行 UI 更新反馈给用户。

在这种结构中,Controller 完全演变成了 Model 和 View 的中间人角色,Model 和 View 是完全解耦的,这样的处理方式,使之能够适应于 Web 程序复杂又多变的逻辑情况,这是这种结构的优点。

然而这种结构也是有缺点的。

大家可以看到,用户界面中事件的捕获触发是 Controller 完成的,而 UI 的更新渲染是 View 实现的,但 Controller 和 View 却是分离的,这在 Android 、iOS 、WP 、塞班 这种有着强大又完善的 GUI 框架的程序中简直是无法想象的。因为在这种 GUI 框架中,View 与 View中的事件的关联性实在是太强了,几乎是无法分割的。

我们以 Android 为例,xml 文件相当于 View,Activity 相当于 Controller,Bean、DB、Network 这些相当于 Model 。大家很清楚,UI 控件的事件处理和 UI 界面的渲染更新,是几乎无法拆分的。这也就导致,这一阶段的 MVC 结构在 Android 这样的 GUI 框架中的应用相当于:

xml + Activity == View + Controller Model == Model

很多说自己在 Andorid APP 的开发中用了 MVC 结构的小伙伴,基本上都是这样应用的。

这当然无可诟病。技术的选型,没有最优,只有最合适。

4、进一步演进的 MVC 结构(Model 2、View 2、Controller 2)

需要大家关注的是,上述的 Model 2 结构的 MVC,当页面的 UI 和逻辑越来越复杂,Activity 这种相当于 View + Controller 的结构的代码量会越来越大,类越来越臃肿,维护的难度也会越来越大。

也就是说,Model 2 结构的 MVC 无法适应于中大型项目。于是,MVC 又有了新的演进。

Controller 被进行拆分,将其中负责用户交互的部分与 View 合并,组成了新的 View(姑且称之为 View 2),将 Controller 剩余的与逻辑相关的部分独立为新的 Controller(姑且称之为 Controller 2),Model 2 保持不变。

这种结构——如下图所示——与我们下文要关注的 MVP 结构已经十分相像了。

image.png

MVP 结构的出现及变形

实际上 MVC 结构的演进聊到 Model 2、View 2、Controller 2 的阶段,MVP 的出现已经没有必要解释了,因为那就是简易版的 MVP 结构:

Model 2     ==     M
View 2      ==      V
Controller 2  ==  P

简单说下定义:

M 和 V 就还是 Model 和 View,P 对应于英文单词 Presenter,翻译过来是 “主持人” 的意思,作为 Model 和 View 沟通的桥梁。

另外,还有一个伴随着 MVP 出现的 Contract ,翻译过来是 “协议、合同”的意思,其通常作为接口出现,负责对 View 接口 和 Presenter 接口,甚至对 Model 接口的统一管理。

在聊 MVP 之前,还有两点需要啰嗦一下的: 1、MVP 结构只是 MVC 结构的一个分支或者说演进,并且就目前来看,MVP 并没有标准化的模式(当然 MVC 也不见得有),大家在自己的开发中要选用什么样的模式,也许会随着自己对 MVP 的应用发生变化。还是那句话,没有最优,只有最合适。 2、MVP 结构的设计,与面向对象的设计原则关系密切,诸如单一职责原则、开闭原则、依赖倒置原则等,如果有不熟悉这几个原则的小伙伴,还是建议大家先去补一下知识。

好了,进入主题,介绍几种 MVP 的变形,这也是我在 MVP 应用的过程中,自己所做的几次调整。

1、典型的 MVP 结构

大家在大多数讲解 MVP 结构的文章中,看到的就是这种典型的 MVP 结构,我手写了个小demo,从网络查询电脑配置的示例,帮助我们理解。

程序清单 2-1     MVP 下查询电脑配置的 Contract

import java.util.List;

public interface Contract{

    public interface IPresenter{
        void getComputerComponents();
    }

    public interface IModel{
        void getComputerComponents(INetworkCallback<List<String>> networkCallback);
    }

    public interface IView{
        void onSuccessGetComputerComponents(List<String> componentsList);

        void onFailGetComputerComponents(String errorMsg);
    }

}

上文我们有说,Contract 是作为协议存在的,负责对 View、Model、Presenter 的统一管理,可清晰获知相关的功能方法。

程序清单 2-2     MVP 下网络请求的结果回调 INetworkCallback

/**
 * 网络请求回调接口
 */
public interface INetworkCallback<T>{
    void onSuccess(T result);

    void onFail(String errorMsg);
}

通过泛型的方式,当 Model 中的网路请求成功时,将请求结果返回给 Presenter

程序清单 2-3     MVP 下查询电脑配置的 Model

import java.util.ArrayList;
import java.util.List;

public class Model implements Contract.IModel{

    @Override
    public void getComputerComponents(INetworkCallback<List<String>> networkCallback) {
        
        new Thread(() -> {
            try {
                // 模拟耗时操作
                Thread.sleep(3000);
                // 查询成功,返回电脑配件集合
                List<String> componentsList = new ArrayList<>();
                componentsList.add("Wired keyborder");
                componentsList.add("Wired mouse");
                // 将结果回传给 Presenter
                if(null != networkCallback){
                    networkCallback.onSuccess(componentsList);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }).start();;

    }
}



程序清单 2-4     MVP 下查询电脑配置的 Presenter

import java.util.List;

import Contract.IView;

public class Presenter implements Contract.IPresenter{

    private Contract.IModel mModel;
    private Contract.IView mView;

    public Presenter(Contract.IView view){
        mModel = new Model();
        mView = view;
    }

    @Override
    public void getComputerComponents() {
        mModel.getComputerComponents(new INetworkCallback<List<String>>(){

            @Override
            public void onSuccess(List<String> componentsList) {
                mView.onSuccessGetComputerComponents(componentsList);
            }

            @Override
            public void onFail(String errorMsg) {
                mView.onFailGetComputerComponents(errorMsg);
            }

        });
    }

}

可以看到,Presenter 拥有 View 接口和 Model 接口的实例,作为桥梁负责 View 与 Model 间的逻辑连接。

程序清单 2-5     MVP 下查询电脑配置的 View

import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.util.List;

import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.JTextField;

public class View extends JFrame implements Contract.IView{
    
    private JButton mButton = new JButton("Get Computer Components");

    private Contract.IPresenter mPresenter;
    private JDialog mDialog;
    private JTextArea mTextArea;

    public View(){
        mPresenter = new Presenter(View.this);

        // 构建界面UI
        JPanel rootPanel = new JPanel();
        rootPanel.setLayout(new BorderLayout());

        JPanel contentPanel = new JPanel();
        contentPanel.setLayout(new FlowLayout());
        contentPanel.add(mButton);
        mButton.addActionListener(e -> {
            mPresenter.getComputerComponents();
        });

        rootPanel.add(contentPanel, BorderLayout.SOUTH);

        mDialog = new JDialog();
        mDialog.setTitle("Computer Components List");
        mDialog.setSize(400, 300);
        mDialog.setLocation(100, 100);

        setContentPane(rootPanel);
        pack();
        setTitle("MVP");
        setVisible(true);
    }

    @Override
    public void onSuccessGetComputerComponents(List<String> componentsList) {
        StringBuilder stringBuilder = new StringBuilder();
        for(int index=0; index<componentsList.size(); index++){
            stringBuilder.append(componentsList.get(index)+"\r\n");
        }
        mTextArea = new JTextArea();
        mTextArea.setText(stringBuilder.toString());
        mDialog.add(mTextArea);
        mDialog.setVisible(true);;
    }

    @Override
    public void onFailGetComputerComponents(String errorMsg) {
        
    }

}

View 中还是用的 Java Swing ,在构造函数中构建了 Java 桌面版程序,具体的 Swing 控件大家不需要关心太多。需要注意的,View 负责按钮控件的事件处理,之后调用 Presenter 处理相应的业务逻辑。

程序清单 2-6     MVP 下查询电脑配置的 Main

public class Main{
    public static void main(String[] args) {
        Contract.IView mView = new View();
    }
}

MVP 的代码示例到此为止,比较简单,逻辑还算比较清晰。

那现在请大家回想一下,上文所述 MVC 的演进,从第 3 步 Model 2 到 第 4 步 Model 2、View 2、Controller 2 的演进中 Controller 的拆分,View 2 负责处理 UI 界面及用户交互,Model 2 负责数据存取、网络请求这样 Data 操作,Controller 2 专心负责其他的业务逻辑 。

这个分工,就是典型的 MVP 的结构的思想,UI 界面和用户交互全部交给 View ,业务逻辑全部交给 Presenter 处理。

2、MVP 结构的几种变形

下面列几条 MVP 的变形。实际上也称不上变形,可能说 MVP 代码结构上的小调整、小技巧会更合适一些。

(1)页面间互相独立的 MVP 结构,就是说,每个有数据存取、网络操作的 Activity、Fragment 等,都拥有与其几乎一 一对应 MVP 接口及实现类。

比如说:消息列表页面 MessageListActivity 和 消息详情页 MessageDetailActivity 分别拥有其独有的 MVPC。

(2)业务上的模块使用统一的 Contract 和 Model,其中每个有数据存取、网络操作的 Activity、Fragment 等拥有其独立的 View 和 Presenter 。以消息 Msg 模块的代码做个示例:

程序清单 3-1     消息模块的 MVP 代码示例

/**
 * 消息
 */
public interface MsgContract {
    interface Model extends IBaseModel {

        /**
         * 查询消息列表
         * @param networkCallback
         */
        void getMsgList(INetworkCallback<MsgListBean> networkCallback);

        /**
         * 查询单条消息详情
         * @param msgId
         * @param networkCallback
         */
        void getMsgDetails(String msgId, INetworkCallback<MsgDetailsBean> networkCallback);
    }

    interface MsgListView extends IBaseView {
        void onSuccessGetMsgList(MsgListBean msgListBean);
    }

    interface MsgListPresenter {
        void getMsgList(MsgReqParamsBean msgReqParamsBean);
    }

    interface MsgDetailsView extends IBaseView {
        void onSuccessGetMsgDetails(MsgDetailsBean msgDetailsBean);
    }

    interface MsgDetailsPresenter {
        void getMsgDetails(String msgId);
    }
}

写在最后

MVC 和 MVP 这样的架构模式的演进,是几十年来许多软件工程师前辈编码经验和软件设计思想的演进与结晶,只要理清了 MVC 的演进路线,MVP 的理解与应用就会变得水到渠成且十分简单。

另外,无论是 MVC、MVP 这样的架构模式,还是面向对象的设计思想,或是 23 种设计模式,一定要多实践、多思考,才不会出现 “好像会用,却又说不清道不明” 的情况。

感谢

  • 《Android源码设计模式解析与实战》 何红辉 关爱民等
  • 《Android进阶之光》 刘望舒

本文首发于 公众号:牛角尖尖上起舞,ID:niujiaojianhi,欢迎关注。

牛角尖

2021/03/22  阅读:56  主题:默认主题

作者介绍

牛角尖