不一样的视角带你看 MVC、MVVM、MVP

阅读 1534
收藏 101
2017-02-16
原文链接:github.com

对于绝大多数开发者来说,对于选择程序的架构,并没有太多的考虑。一方面是业务变化太快,怎么快,怎么来;另一方面,程序架构一般由团队的核心开发来选择,其他开发者可能会处于云里雾里的状态。本文会详细说明,并有代码参考,教你彻底认清 MVC、MVVM、MVP 的相关知识,希望对大家有用吧!

阅读之前请谨记

  • 架构来源于业务,并没有好坏之分。好的架构是在业务、成本、时间之间取得一个完美的平衡
  • 希望读者有自己的思考,具有怀疑和批判精神,千万不要相信本文的观点

MVC

MVC 全名是 Model View Controller,顾名思义,Controller 作为控制器,通过用户输入,控制视图的展示,还可以通过 Model 层的数据组件获取数据,进而改变 View 层的展示,其结构图如下

MVC

  • Controller
    • 用户动作映射成模型更新
    • 选择响应的视图
  • View
    • 获取模型数据
    • 模型展示、更新
    • 收集用户行为,发送给控制器
  • Model
    • 封装应用程序状态
    • 响应状态查询
    • 通知视图更新

这里将 Controller、View 放在同一级别,主要是为了说明其调用关系,Controller 对 View 是单向调用,Controller 和 View 对 Model 的调用也是单向的,以实箭头表示。Model 将数据传递给 Controller 或者是 View,传递的方式可以是调用的时候返回,也可以是以回调接口的方式传递,这里用虚箭头表示。

MVC 和 MVVM、MVP 一样,只是一种设计典范,有多种实现形式,例如 ASP.NET MVC 中,控制器(Controller)只是充当了一个 router 的作用,根据用户的请求,返回不同的页面(View),每一个页面调用 db 对象获取数据(Model),展示到页面中。在 JSP 中,控制器是 servlet,视图是 jsp 页面,模型是 bean 对象和 db。

在 Android 中,MVC 又各表示什么呢?Activity 属于控制器,它接收了所有的用户输入请求;layout.xml 等各种界面布局属于视图;各种 bean、repository 等属于模型。不过在 Android 中,也可以把 Activity 也看作视图,它响应用户的输入,从模型层获取数据,进而控制视图的显示与隐藏,主要原因是 xml 没有自处理的能力,只能靠 Activity 来控制,这样就只能把 Activity 和 xml 等都归属于视图,类似在 iOS 中 ViewController 的作用。

在 Android 中使用 MVC 模式,正是因为 Controller 和 View 不清不楚的关系,很容易就写出万能的 Activity 类,业务越复杂,代码量越膨胀,动不动就是上千行。在 iOS 中也一样,iOS 中直接将 View 和 Controller 合成了一个,名字就叫 ViewController。

很多文章会说 MVC 中的 Model 层不能解耦,个人觉得是一种错误的解释。MVC 的出现正是为了将用户输入、视图显示、业务逻辑分离,实现解耦。之所以会给大家留下 Model 层不能和 View、Controller 解耦的现状,其实是因为并没有将 Model 层抽象出来,Model 层属于 Controller、View 的下层,可以以接口的形式来给出,这样接口和实现即可分离,为何不能解耦?

下面将以一个 Android 的代码示例来说明 MVC 的使用。

View

activity_home.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@color/background"
                android:orientation="vertical">

    <RelativeLayout
        android:id="@+id/toolbar"
        android:layout_width="match_parent"
        android:layout_height="@dimen/toolbar_height"
        android:layout_alignParentTop="true"
        android:background="@color/base_color">

        <ImageView
            android:id="@+id/btn_back"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_centerVertical="true"
            android:paddingLeft="16dp"
            android:paddingRight="16dp"
            android:src="@drawable/selector_button_title_back"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerInParent="true"
            android:text="@string/app_name"
            android:textColor="@color/white"
            android:textSize="18sp"/>
    </RelativeLayout>

    <ListView
        android:id="@+id/list"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1"
        android:groupIndicator="@android:color/transparent"/>

</LinearLayout>

这里包含一个标题栏,一个列表。列表的展示如下

activity_home_item.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="72dp"
                android:background="@color/white"
                android:minHeight="72dp"
                android:orientation="vertical">

    <ImageView
        android:id="@+id/iv_icon"
        android:layout_width="20dp"
        android:layout_height="20dp"
        android:layout_centerVertical="true"
        android:layout_marginEnd="12dp"
        android:layout_marginLeft="16dp"
        android:layout_marginRight="12dp"
        android:layout_marginStart="16dp"
        android:background="@mipmap/icon_note"/>

    <TextView
        android:id="@+id/tv_item_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_toEndOf="@+id/iv_icon"
        android:layout_toRightOf="@+id/iv_icon"
        android:textColor="@color/text_grey"
        android:textSize="18sp"/>

</RelativeLayout>

Controller (View)

HomeActivity.java
public class HomeActivity extends AppCompatActivity {

    private ListView mList;
    private HomeListAdapter mListAdapter;

    private TaskRepository mRepository;
    private BaseScheduler mScheduler;
    private BaseThread mMainThread;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        mList = (ListView)findViewById(R.id.list);

        mListAdapter = new HomeListAdapter(this);
        mList.setAdapter(mListAdapter);

        mRepository = TaskRepository.getInstance(
                Injection.provideLocalProxy(),
                Injection.provideRemoteProxy());
        mScheduler = ThreadPoolScheduler.getInstance();
        mMainThread = new HandlerThread();
    }

    @Override
    public void onResume() {
        super.onResume();
        initPage();
    }

    private void initPage() {
        mScheduler.execute(new Runnable() {
            @Override
            public void run() {
                List<Task> tasks = mRepository.getTasks();
                onTasksLoaded(tasks);

                tasks = mRepository.refreshTasks();
                onTasksLoaded(tasks);
            }
        });
    }

    private void onTasksLoaded(final List<Task> tasks) {
        if (tasks != null) {
            mMainThread.post(new Runnable() {
                @Override
                public void run() {
                    mListAdapter.setTasks(tasks);
                }
            });
        }
    }
}

这里直接使用 TaskRepository 获取数据,获取到之后,设置给 list 的 adapter 去展示。先调用 getTasks 获取本地数据,通知界面更新之后,再调用 refreshTasks 获取服务端数据来做刷新。HomeListAdapter 的代码就不展示出来了。

Model

TaskRepository.java
public class TaskRepository implements Repository {

    private static TaskRepository sInstance;

    public static TaskRepository getInstance() {
        if (sInstance == null) {
            synchronized (TaskRepository.class) {
                if (sInstance == null) {
                    sInstance = new TaskRepository();
                }
            }
        }
        return sInstance;
    }

    public static TaskRepository getInstance(LocalProxy localProxy, RemoteProxy remoteProxy) {
        TaskRepository taskRepository = getInstance();
        taskRepository.setLocalProxy(localProxy);
        taskRepository.setRemoteProxy(remoteProxy);
        return taskRepository;
    }

    private LocalProxy mLocalProxy;
    private RemoteProxy mRemoteProxy;

    private TaskRepository() {
    }

    public void setLocalProxy(LocalProxy localProxy) {
        mLocalProxy = localProxy;
    }

    public LocalProxy getLocalProxy() {
        return mLocalProxy;
    }

    public RemoteProxy getRemoteProxy() {
        return mRemoteProxy;
    }

    public void setRemoteProxy(RemoteProxy remoteProxy) {
        mRemoteProxy = remoteProxy;
    }

    @Override
    public List<Task> getTasks() {
        return mLocalProxy.getAll();
    }

    @Override
    public List<Task> refreshTasks() {
        return mRemoteProxy.getAllTask();
    }
}

这里可以看到 TaskRepository 实现了 Repository 接口,定义如下

public interface Repository {

    List<Task> getTasks();

    List<Task> refreshTasks();
}

Model 层可以使用接口来抽象,达到解耦的目的。

小结

  • 对于复杂度不高的业务,使用 MVC,代码会比较少,比较直接,也能快速实现
  • 对于没有太多合作的业务来说,可以使用 MVC,可以由同一人来实现 View 和 Controller 部分。

MVVM

MVVM 全名是 Model View ViewModel,其本质是在 View 和 Model 之间加入了一层中间层,将 Model 表示为一个可展示的对象,其结构图如下 MVVM

  • View
    • 用户动作映射成模型更新
    • 选择响应的视图
    • 获取模型数据
    • 模型展示、更新
  • Model
    • 封装应用程序状态
    • 响应状态查询
    • 通知视图更新
  • ViewModel
    • 将 Model 层数据适配为 View 层所需要的数据

相对于 MVC 模式,View 层包含了 MVC 中 View、Controller 的职责,Model 的职责病没有发生变化,ViewModel 可以看做是一个适配器,将 Model 层的数据适配成 View 需要展示的数据。

MVVM 是一种设计范例,它也有多种实现方式,业内最多的实现方式是以 data binding 的形式去实现,而且在该设计范例中,更强调数据绑定的作用。例如在 ASP.NET 中最早就可以将一个数据源绑定到一个控件上。

MVVM 在 Android 中是如何实现的呢?谷歌官方实现了 data binding 的框架,下面我们来看看是如何实现的。

build.gradle

android {

    dataBinding {
        enabled = true
    }
}

在 gradle 的默认设置中加入 data binding 的支持。

View

activity_home_item.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="task" type="com.android.mvp.data.bean.Task" />
    </data>

         ...

        <TextView
            android:id="@+id/tv_item_title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_centerVertical="true"
            android:layout_toEndOf="@+id/iv_icon"
            android:layout_toRightOf="@+id/iv_icon"
            android:textColor="@color/text_grey"
            android:textSize="18sp"
            android:text="@{task.title}"/>

    </RelativeLayout>
</layout>

相对于 MVC 中的 activity_home_item.xml,这里增加了一个 data 节点,用来声明 Task 对象,将 TextView 的显示字段直接绑定到 Task 对象的 title 字段上。这里省略了一些与上文中重复的内容,下同。我们来看一下具体在 HomeListAdapter 中是如何完成数据绑定的。

HomeListAdapter.java
@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ActivityHomeItemBinding binding;
    Task task = mTasks.get(position);
    if (convertView == null) {
        binding = ActivityHomeItemBinding.inflate(mLayoutInflater, parent, false);
    } else {
        binding = DataBindingUtil.getBinding(convertView);
    }
    binding.setTask(task);
    binding.executePendingBindings();

    return binding.getRoot();
}

这里使用了框架生成的 ActivityHomeItemBinding 对象,更新的时候直接使用 setTask 即可将数据绑定到视图上。这里的实现与 MVC 不同的地方在于不需要再手动将 Task 的 title 字段设置到 ImageView 控件上,框架会自动完成。在 MVC 中我们是这么写的

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    final ViewHolder viewHolder;
    Task task;
    task = mTasks.get(position);
    if (convertView == null) {
        convertView = mLayoutInflater.inflate(R.layout.activity_home_item, null);
        viewHolder = new ViewHolder();

        viewHolder.tvItemTitle = (TextView)convertView.findViewById(R.id.tv_item_title);
        convertView.setTag(viewHolder);
    } else {
        viewHolder = (ViewHolder) convertView.getTag();
    }

    bindData(viewHolder, task);
    return convertView;
}

private void bindData(ViewHolder viewHolder, Task task) {
    viewHolder.tvItemTitle.setText(task.getTitle());
}

我们写了一个 bindData 的方法,手动去更新数据。其实这里可以看出来,使用了 data binding 框架,可以减少我们手工去绑定数据的过程,这里只是绑定一个控件,如果在大量控件的情况下,是能提高开发效率的。data binding 框架需要 ViewModel 的支持,下面来看看 ViewModel 是如何实现的。

ViewModel

Task.java
public final class Task extends BaseObservable {

    private long mId;
    private String mTitle;

    public Task() {
        this.mId = 0L;
        this.mTitle = "";
    }

    public long getId() {
        return mId;
    }

    public void setId(long id) {
        mId = id;
    }

    @Bindable
    public String getTitle() {
        return mTitle;
    }

    public void setTitle(String title) {
        mTitle = title;
        notifyPropertyChanged(BR.title);
    }
}

ViewModel 需要继承 BaseObservable,被绑定的字段会提供一个定义了 @Bindable 注解的 getter 方法。很多文章将 ActivityHomeItemBinding 描述为 ViewModel 对象,是一种错误的说法,ActivityHomeItemBinding 只是一个自动生成的辅助工具类,数据要更新到视图上。xml 文件中的 task.title 在显示时会调用 Task.getTitle 方法。

Model

Model 层相对于 MVC 来说是一样的,并没有什么变化。

小结

  • MVVM 着重强调数据绑定,使用数据绑定工具,可以提高开发效率
  • 抛开 MVVM 这种模式,其数据绑定用在其他模式上也一样实用。个人觉得,可以不把 MVVM 当作一种模式,它只是使用工具代替了人工绑定数据而已

MVP

在介绍 MVP 之前,我先说明一下代码的层次化、模块化的概念。其实层次化的结构是平时生活中经常都可以看到的,例如一个产业的上下游。每一个层次中有可以分为多个模块。为何要分层、分模块?因为社会「分工」精细化,一个庞大的工程一个组织完不成,必须拆解,一步一步细化。细化之后如何拼接在一起呢?彼此之间需要约定,我们也可以称之为「协议」,将协议标准化,即可构成一个「生态」。协议在 IT 行业无处不在,例如 x86 指令集属于硬件协议,HTTP 属于软件协议。分层与分模块的概念在 IT 行业更是比比皆是,例如网络的七层模型,下层为上层提供服务,在传输层又可以分为 TCP 与 UDP 两种不同的传输方式,我们可以认为它是这一层的两个模块。

再这个基础之上再来看代码的层次化、模块化可能就更加明朗了。大多数情况下,一个软件不是一个人可以完成的,所以需要进行「分工」,分工方式可以按照层次化的方式来分,例如做 UI 的人专门写 UI,做框架的人专门写框架,做数据更新与存储的人专门做数据;也可以按照业务来分,做某一块业务的人从数据获取到 UI 展示一条龙全写了。

哪种方式更好?答案是按层次来划分会更好。每个层次很多代码可以做到可「复用」,例如数据层的人可以写一个统一的数据存储框架,处理所有业务模块的数据,带来的问题就是产生了上层与下层的协作与沟通成本。按照业务来划分的话,两个人同时写到从网络获取数据的时候,是各自用一个框架呢,还是统一用一个呢?同样会产生协作与沟通成本。但是按照层次来划分,上下层的可以通过「接口」来约束行为,达到解耦的目的,而按照业务来划分就做不到了。

一般来说,对于有图形界面的客户端软件来说,我们可以简单地分为三层

layer

我们所用的图形界面,看到的是什么?本质上来说就是数据的可视化呈现。这里就正式引入 MVP 概念了,它是 Model View Presenter 的简称,Model 提供数据,View 负责展示,Presenter 负责处理逻辑,它的结构图如下

layer

和上面的分层一摸一样!在 MVP 里,Presenter 完全把 Model 和 View 进行了分离,主要的程序逻辑在 Presenter 里实现。而且,Presenter 与具体的 View 是没有直接关联的,而是通过定义好的接口进行交互,从而使得在变更 View 时候可以保持 Presenter 的不变,即重用!

从上到下是直接调用,用实箭头表示。从下到上是回调,用虚箭头表示。依赖关系是上层对下层是直接依赖,下层不能依赖上层。那从上层调用下层是不是必须定义接口?业内有不少声音呼吁,不要再给 Presenter 加接口了,还给出了诸多理由,我简单列举几条

  • 加了接口 IPresenter,实现类写成 IPresenterImpl 名字不好看
  • 加了接口让方法数翻倍
  • 通过接口不好定位具体的实现,程序的走向很难把控
  • 接口并没有提高项目的可测试性

个人觉得,需要看情况而定,一般来说下层不定义接口,上层直接依赖下层的实现,并没有什么问题。但是在并行开发中,我想在开发过程中使用下层逻辑怎么办呢,下层并没有实现完成啊?这个时候就需要下层定义好接口,彼此之间通过「接口」来约束行为。这里我说到过很多次「接口」,相信大家也知道接口的重要性,因为分工的关系,很多时候我们需要「面向接口编程」,而不是「面向实现编程」。上面的诸多理由都是可以一一被驳回的

  • 可以通过内部类的方式来规范命名
  • 定义良好的接口一般数量会比较少,相比整个项目的方法数来说简直是九牛一毛
  • 多态的特性都不想要了么,是不是凡是用到多态的地方都可以用这个理由去反驳
  • 接口并没有提高项目的可测试性,这一条简直是大错特错,有了接口,我们就可以写 mock 数据,写 mock 实现,上层的测试完全不需要再依赖下层

我个人比较主张每一层都以接口去定义,这样有利于每一层的独立测试,上层可以写一个 mock 实现,只要按照接口约定的逻辑返回即可,这也是 clean 架构的思想。下层回调上层必然是以回调接口的形式去完成,这是毋庸置疑的。

MVP 很好地将 View 与 Model 做了分离,同时 Presenter 也是可以复用的,假设有有两个页面,一个显示列表大纲,一个显示列表详情,如果操作大致一样,那可以复用同一个 Presenter。将 Presenter 的功能做一个最小集的拆分,有利于 Presenter 的复用,同一个视图里面可以同时存在多个 Presenter,每个 Presenter 实现不同的功能,更新不同的区域。总之,在 MVP 架构中,每一层均可以拆分成独立的可复用的组件,因为彼此都可以只是接口依赖。

下面给出一个 MVP 的代码实现的例子。

View

activity_home.xml,activity_home_item.xml

与 MVC一致,不再重复。

HomeActivity.java
public class HomeActivity extends AppCompatActivity implements HomeContract.View {

    private ListView mList;

    private HomeContract.Presenter mPresenter;

    private HomeListAdapter mListAdapter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        mList = (ListView)findViewById(R.id.list);

        Context context = getApplicationContext();
        BaseScheduler scheduler = ThreadPoolScheduler.getInstance();
        BaseThread thread = new HandlerThread();

        TaskRepository taskRepository = TaskRepository.getInstance(
                Injection.provideLocalProxy(),
                Injection.provideRemoteProxy());

        mPresenter = new HomePresenter(this, scheduler, thread, taskRepository);
        mPresenter.start();

        mListAdapter = new HomeListAdapter(context, mPresenter);
        mList.setAdapter(mListAdapter);
    }

    @Override
    public void onResume() {
        super.onResume();
        mPresenter.loadTasks(true);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        mPresenter.stop();
    }

    @Override
    public void onTasksLoaded(List<Task> tasks) {
        mListAdapter.setTasks(tasks);
    }

    @Override
    public void onTasksNotAvailable() {

    }

    @Override
    public void onError(int code, String message) {
        Toast.makeText(this, "error", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void setPresenter(HomeContract.Presenter presenter) {

    }
}

这里创建了一个 HomePresenter 的实例,当然这里也可以使用工厂模式去解耦,类似

public class Injection {
    public static RemoteProxy provideRemoteProxy() {
        return new TaskServerApi();
    }

    public static LocalProxy provideLocalProxy() {
        return new TaskDbApi();
    }
}

这样的做法。这样可以在 mock 的环境下,写一份 mock 实现去测试上层的逻辑。

public class Injection {
    public static RemoteProxy provideRemoteProxy() {
        return new RemoteProxy() {

            private List<Task> mTasks = makeFakeTasks();
            @Override
            public boolean addTask(Task task) {
                mTasks.add(task);
                return true;
            }

            @Override
            public boolean deleteTask(long taskId) {
                for (Task task : mTasks) {
                    if (task.getId() == taskId) {
                        mTasks.remove(task);
                        return true;
                    }
                }
                return false;
            }

            @Override
            public Task getTask(long taskId) {
                for (Task task : mTasks) {
                    if (task.getId() == taskId) {
                        return task;
                    }
                }
                return null;
            }

            @Override
            public boolean updateTask(Task task) {
                for (int i = 0; i < mTasks.size(); ++i) {
                    Task t = mTasks.get(i);
                    if (t.getId() == task.getId()) {
                        mTasks.set(i, task);
                        return true;
                    }
                }
                return false;
            }

            @Override
            public List<Task> getAllTask() {
                return mTasks;
            }

            private List<Task> makeFakeTasks() {
                List<Task> tasks = new LinkedList<>();
                Date date = new Date();
                long time = date.getTime();
                for (int i = 0; i < 10; ++i) {
                    Task task = new Task();
                    task.setId(time + i);
                    task.setTitle("this is a test title " + String.valueOf(i));
                    tasks.add(task);
                }

                return tasks;
            }
        };
    }

    public static LocalProxy provideLocalProxy() {
        return new TaskDbApi();
    }
}

HomeActivity 里面只是调用了 HomePresenter 的 loadTasks 方法,以及 start,stop 方法。HomeListAdapter 的实现与 MVC 中一样,这里就不展示了。可以看出,这里 View 只是对 Presenter 有依赖,对 Model 层是没有依赖的。

Presenter

BasePresenter.java
public interface BasePresenter {

    void start();

    void stop();

}
BaseView.java
public interface BaseView<T extends BasePresenter> {

    void setPresenter(T presenter);

}

上面两个类只是为了说明:一个 Presenter 属于一个 View。

HomeContract.java
public class HomeContract {

    public interface View extends BaseView<Presenter> {

        void onTasksLoaded(List<Task> tasks);

        void onTasksNotAvailable();

        void onError(int code, String message);

    }

    public interface Presenter extends BasePresenter {

        void loadTasks(boolean refresh);

    }
}

该类定义了一个具体的业务场景的 View 接口与 Presenter 接口,这里通过内部类的形式去实现,是为了强调 View 与 Presenter 的一一对应关系,并且很好地解决了上面说的命名的问题。

HomePresenter.java
public class HomePresenter implements HomeContract.Presenter {

    private HomeContract.View mView;
    private Repository mRepository;
    private BaseScheduler mScheduler;
    private BaseThread mMainThread;
    private LoadTask mLoadTask;

    public HomePresenter(HomeContract.View view, BaseScheduler scheduler, BaseThread thread, Repository repository) {
        this.mView = view;
        this.mScheduler = scheduler;
        this.mMainThread = thread;
        this.mRepository = repository;

        mLoadTask = new LoadTask(mScheduler);
    }

    @Override
    public void loadTasks(boolean refresh) {
        LoadTask.RequestValues requestValues = new LoadTask.RequestValues();
        requestValues.setRefresh(refresh);
        requestValues.setRepository(mRepository);
        mLoadTask.setRequestValues(requestValues);

        mLoadTask.execute(new LoadTask.Callback<LoadTask.ResponseValues>() {

            @Override
            public void onSuccess(final LoadTask.ResponseValues response) {
                mMainThread.post(new Runnable() {
                    @Override
                    public void run() {
                        List<Task> tasks = response.getTasks();
                        if (tasks != null && tasks.size() > 0) {
                            mView.onTasksLoaded(tasks);
                        } else {
                            mView.onTasksNotAvailable();
                        }
                    }
                });
            }

            @Override
            public void onError(final int code, final String msg) {
                mMainThread.post(new Runnable() {
                    @Override
                    public void run() {
                        mView.onError(code, msg);
                    }
                });
            }
        }, true);
    }

    @Override
    public void start() {

    }

    @Override
    public void stop() {

    }
}

HomePresenter 是一个具体的实现,它实现了具体的业务逻辑即 loadTasks,并且将结果通过接口 onTasksLoaded 返回给 View 层去做展示。对于 Presenter 来说,View 是它的上一层,只能通过这种回调的方式返回数据,或者做数据更新。

Model

TaskRepository.java

与 MVC 中一致。

LoadTask.java
public class LoadTask extends BaseTask<LoadTask.RequestValues, LoadTask.ResponseValues> {

    public static final class RequestValues implements BaseTask.RequestValues {
        private boolean mRefresh;
        private Repository mRepository;

        public boolean isRefresh() {
            return mRefresh;
        }

        public void setRefresh(boolean refresh) {
            mRefresh = refresh;
        }

        public Repository getRepository() {
            return mRepository;
        }

        public void setRepository(Repository repository) {
            mRepository = repository;
        }
    }

    public static final class ResponseValues implements BaseTask.ResponseValues {
        private List<Task> mTasks;

        public ResponseValues() {
            mTasks = new LinkedList<>();
        }

        public List<Task> getTasks() {
            return mTasks;
        }

        public void setTasks(List<Task> tasks) {
            mTasks = tasks;
        }
    }

    public LoadTask(BaseScheduler scheduler) {
        super(scheduler);
    }

    @Override
    public void run() {

        getScheduler().execute(new Runnable() {
            @Override
            public void run() {
                LoadTask.RequestValues requestValues = getRequestValues();
                boolean refresh = requestValues.isRefresh();
                List<Task> tasks = requestValues.getRepository().getTasks();

                LoadTask.ResponseValues responseValues = new LoadTask.ResponseValues();
                responseValues.setTasks(tasks);
                setResponseValues(responseValues);

                notifySuccess();

                if (refresh) {
                    tasks = requestValues.getRepository().refreshTasks();
                    responseValues.setTasks(tasks);
                    notifySuccess();
                }
            }
        });

    }
}

这里 LoadTask 类使用 TaskRepository 提供的 getTasks 获取本地数据,传回给 Presenter,然后再使用 refreshTasks 获取服务端数据,传回给 Presenter,它对上层的数据返回也是通过定义的回调函数完成的,即上面的 LoadTask.Callback。

小结

  • MVP 是最符合客户端软件分层的架构
  • 上层对下层的依赖可以是直接依赖,也可以是接口依赖
  • 下层对上层只能是接口依赖
  • 使用接口依赖,可以实现各个层的独立测试,也就是 clean 架构的思想。
  • MVP 中,每一层都可以拆分成独立的组件,实现复用。一个视图可以包含多个 Presenter,一个 Presenter 的逻辑可以展示在不同的 View 上,因为每一层之间都可以只是接口依赖

最后再重申一遍

  • 架构来源于业务,并没有好坏之分。好的架构是在业务、成本、时间之间取得一个完美的平衡
  • 希望读者有自己的思考,具有怀疑和批判精神,千万不要相信本文的观点

本文中所有的例子,可以在我的 Github 上找到,项目地址:android-mvp

评论