专栏名称: 安卓开发精选
伯乐在线旗下账号,分享安卓应用相关内容,包括:安卓应用开发、设计和动态等。
目录
相关文章推荐
开发者全社区  ·  97岁李嘉诚 ·  18 小时前  
开发者全社区  ·  北京的普普通通有钱人 ·  23 小时前  
开发者全社区  ·  字节跳动出手:直接辞退353人! ·  昨天  
开发者全社区  ·  UCL色魔博士被抓 ·  2 天前  
开发者全社区  ·  人大发TT了 ·  2 天前  
51好读  ›  专栏  ›  安卓开发精选

关于 MVC/MVP/MVVM 的一些错误认识

安卓开发精选  · 公众号  · android  · 2019-10-20 19:50

正文

(给 安卓开发精选 加星标)

转自: wfwf

juejin.im/post/5d762cd9e51d4561e0516b7d

在 Android 开发中使用 MVP 和 MVVM 模式早已不是新鲜事了,各种 MVP/MVVM 相关的文章、开源库也已屡见不鲜,甚至是让人眼花撩乱,那么我为什么还要在这个早已被画满涂鸦的黑板上再来涂涂画画呢? 是想彰显我的存在感吗?


那当然! 啊不不不……不完全是! 我还想要警醒读到这篇文章的各位: 你们对于MVX的理解可能并不完全正确!

注: 这篇文章里我将使用 MVX 做为 MVC 、MVP 以及 MVVM 的统称。

我们都知道 MVX 的进化过程是从滚球兽进化到 MVC ,然后从 MVC 进化到 MVP,再从 MVP 超进化到 MVVM。 那么接下来,按照常规的套路,我应该要介绍什么是 MVC,什么是 MVP,以及什么是 MVVM,并且分别介绍M、V、C/P/VM 各自的职责了。


我的目的是想要纠正一些对 MVX 的错误认识,所以前提是你要对 MVX 有一些了解。 为了避免有人在使用 MVX 时走上弯路,所以决定对我看到的一些关于 MVX 的错误认识进行总结以及纠正。


会产生这些错误认知的原因,经我分析,其实是: 没有真正领会到将表现层和业务层分 离 。



表现层和业务层分离


表现层和业务层分离,Matin Fowler 称之为 Separated Presentation。 这里的表现层就是 VX,业务层就是 M。 如果有人看到这里发现了和你认为的 MVX 不一样的话,那么你对 MVX 的认识很可能就存在错误!


从表现层和业务层分离的视角来看,M、V、X不是平等的身份,应该是 M 和 V-X。


自始自终 M 的职责都没变,变的是 V-X,随着软件开发技术的发展、交互形式或者交互媒介的不断改变,表现层的逻辑也越来复杂,MVX 的进化过程就是一个不断探寻处理表现层复杂逻辑的过程。


当然从一个形态进化到另一个形态,并不一定是为了解决更复杂的交互逻辑,也可能是有了一种“更优雅”的方式来处理表现层逻辑。


既然已经有表现层和业务层分离的概念了,那么第一个错误观点就很好解释了。



错误一: Presenter 或者 ViewModel 负责处理业务逻辑


这是一个很常见的错误观点,很多介绍 MVP 或者 MVVM 的文章都这么说过。


正如前面所说,业务逻辑是属于 M 层的,那 Presenter 或者 ViewModel 是干什么的,处理表现层逻辑的吗?


是的,或者说大部分表现层逻辑都是在 Presenter 或者 ViewModel 中处理的。 之前我将业务层之上的这些逻辑称之为视图逻辑,现在为了统一就叫做表现层逻辑吧(加个吧字怎么感觉怪怪的)。


我在这里就简单说一下什么是表现层逻辑,以及 View 和 Presenter/ViewModel 又是如何分工的。 假设你的应用有一个个人资料的 profile 页面,这个页面有两种状态,一种是浏览状态,一种是编辑状态,通过一个编辑按钮触发状态的转换,编辑状态时,部分信息项可以进行编辑。


那这里就有一个明显的表现层逻辑,那就是点击按钮切换浏览/编辑状态。


现在的 MVP 的流行形态(或者变种)叫做 Passive View,它和 MVVM 一样现在都倾向于将几乎所有的表现层逻辑交给 Presenter 或者 ViewModel 处理,View 层需要做的事情很少,基本上就是接受用户事件,然后将用户事件传递给 Presenter 或者 ViewModel。


以上面的 profile 页面的例子来解释的话就是,View 层负责接收编辑按钮的点击事件,然后通知 Presenter/ViewModel,然后 Presenter/ViewModel 通知 View 是显示浏览状态的视图还是编辑状态的视图。 MVP 的示例代码大概是这样的:


public




    
 class ProfileView {

    void initView() {
        // 负责注册点击事件监听器,并将点击事件通知给presenter
        editStateButton.setOnClickListener(new OnClickListener() {
            presenter.onEditStateButtonClicked();
        })
        ...
    }

    // 显示浏览状态视图,想不到好名字,就叫showNormalState吧
    public void showNormalState() {
        // 浏览状态下编辑按钮提示文字为“编辑”,所有项不可编辑
        editStateButton.setText("编辑");
        nickName.setEditable(false);
        ...
    }

    public void showEditState() {
        // 浏览状态下编辑按钮提示文字为“完成”,部分项要设置为可编辑
        editStateButton.setText("完成");
        nickName.setEditable(true);
        ...
    }
}


public class ProfilePresenter {

    private State curState = State.NORMAL;

    public void onEditStateButtonClicked() {
        // 按钮被点击时,根据当前状态判断View应该切换显示的状态
        // 这就是表现层逻辑
        if (isInEditState()) {
            curState = State.NORMAL;
            view.showNormalState();
        } else {
            curState = State.EDIT;
            view.showEditState();
        }
    }

    private boolean isInEditState() {
        return curState == State.EDIT;
    }

    @VisibleForTest
    void setState(State state)  {
        curState = state;
    }
}

注: 这个示例代码只是为了展示表现层逻辑,没有涉及到Model层,编译也不会通过的!

能感受到我想表达的意思吗? 就是 Presenter/ViewModel 根据当前交互状态决定该显示什么,而 View 要做的是如何显示它们。


再比如说下拉刷新的场景,由 View 告诉 Presenter/ViewModel,它接收到了下拉事件,然后 Presenter/ViewModel 再告诉 View,让它去显示刷新提示视图,至于这个刷新提示长什么样就由 View来决定。 当然 Presenter/ViewModel 也可能会判断当前网络不可用,而让 View 显示一个网络不可用的提示视图。


为什么要让 Presenter/ViewModel 处理几乎所有的表现层逻辑呢?


主要是为了提高可测试性,将尽可能多的表现层逻辑纳入到单元测试的范围内。 因为对视图控件的显示等等进行单元测试太难了,所以 View 是基本上没法进行单元测试的,但是 Presenter/ViewModel 是完全可以进行单元测试的:


public class ProfilePresenterTest {

    private ProfilePresenter presenter;
    private ProfileView view;

    @Test
    public void testShowEditStateOnButtonClick() {
        // 浏览状态下点击编辑按钮,验证View是否显示了编辑状态视图
        // 也就是验证view.showEditState()方法是否被调用了
        presenter.setState(State.NORMAL);
        presenter.onEditStateButtonClicked();
        Mockito.verify(view).showEditState();
    }

    @Test
    public void testShowNormalStateOnButtonClick() {
        // 编辑状态下点击完成按钮,验证View是否显示了浏览状态视图
        // 也就是验证view.showNormalState()方法是否被调用了
        presenter.setState(State.EDIT);
        presenter.onEditStateButtonClicked();
        Mockito.verify(view).showNormalState();
    }
}

你看,这些表现层逻辑就都能进行单元测试了吧!大概懂我意思了吧?



OK,现在你已经知道表现层了,那业务层又是干什么用的呢?现在我们就要开始谈到 M 了。


M 是什么?M 是指那些喜欢从受虐中获得性……哎呀,不好意思,搞混了!哎~学识渊博就是麻烦!M 者,Model 也,再长一点就是 Domain Model,中文名字叫领域模型。


我们看一下维基百科上对 Domain model 的定义:

In software engineering, a domain model is a conceptual model of the domain that incorporates both behaviour and data.

怎么样,是不是很通俗易懂呀?


当然不是!刚刚开始有点理解Model层是处理业务逻辑的,现在又来了个抖MMM…… Domain,我都不知道该往哪里去想了!


Domain,简单点就把它理解成业务,我觉得都没啥问题。我这里引用这句话,主要是想强调,Model 层包含了业务数据以及对业务数据的操作 (behaviour and data),也是为了引出第二个错误观点。



错误二: Model 就是静态的业务数据


我们做业务模块开发时,会经常定义一些数据结构类,比如个人资料可能会对应一个 UserProfile 类,一条订单数据可能会对应一个 Order 类,这些类没有任何逻辑,只有一些简单的 getter、setter 方法。


有些人会认为像 UserProfile 或者 Order 这样的数据结构类就是 Model。


我们已经强调了,Model 层包含了业务数据以及对业务数据的操作。


像 UserProfile 或者 Order 这样的数据结构类的实例甚至都不能称之为对象,可以看一下 Uncle Bob 的 Classes vs. Data Structures 这篇文章,对象是有行为的,一个数据结构实例没有行为,连对象都称不上,怎么能代表 Model 层呢!


静态的业务数据不能代表 Model 层,业务数据以及针对业务数据的操作共同构成了 Model 层,这也就是业务逻辑。


再举个例子说一下吧,假设你在做一个叫“掘铁”的 app,这个 app 现在只有一个页面,用来展示推荐的博客列表。


OK,我们如果用 MVP 的形式该怎么写呢?


我们就先不管和 Model 层完全没有交互的 View 了,Presenter 层除了处理表现层逻辑外,还要向 Model 层发出业务指令,注意,Presenter 并不处理业务逻辑,真正的业务逻辑还是由 Model 层完成。 示例代码大概是下面这样:


public class RecommendBlogFeedPresenter {

    private RecommendBlogFeedView view;
    private BlogMode model;

    public void onStart() {
        view.showLoadWait();
        model.loadRecommendBlogs(new LoadCallback<>() {

            @Override
            public void onLoaded(List blogs) {
                view.showBlogs(blogs);
            }
        })
    }

}

public interface BlogModel {

    void loadRecommendBlogs(LoadCallback> callback);
}

public class BlogModelImpl implements BlogModel {

    private BlogFeedRepository repo;

    @Override
    public void loadRecommendBlogs(LoadCallback> callback) {
        // BlogFeedRepository.fetch()很可能是耗时操作,所以实际写的时候会在非主线程执行,这里只是示例
        callback.onLoaded(repo.fetch("recommend"));
    }
}

public interface BlogFeedRepository {

    List fetch(String tag);
}

什么?你这个 BlogModelImpl 里就这一行代码,你跟我说这是业务逻辑?


大家冷静一下,把手里的板砖、砍刀、狼牙棒先放下来。BlogModelImpl 类里面的逻辑虽然简单,但是它的确是业务逻辑,也正是因为业务逻辑比较简单,所以 BlogModelImpl 类才会很简洁。


再从 Presenter 的角度看一下,为什么 loadRecommendBlogs() 属于业务逻辑。


博客这个概念毫无疑问属于业务概念,根据前面的解释应该可以判断出来“获取推荐的博客列表”不属于表现层逻辑,那么这个逻辑的实现就不是 Presenter 需要关心的,那就应该是 Model 层的职责,既然是 Model 层的那就应该是业务逻辑了;


再者,既然博客是业务概念,那么 Blog 就是业务数据的数据结构,loadRecommendBlogs() 涉及到对业务数据 Blog 的创建及组装等操作,所以也应该是业务逻辑。


看到这里,可能有些人会产生一些误解:


所谓的业务逻辑处理就是网络请求、数据库查询等数据获取逻辑,即Model层就是负责数据获取的,这也是我要说的第三个错误观点。


错误三: Model 层就是负责数据获取的


产生这种错误认识的,说白了还是没有搞懂业务逻辑。


当然了业务逻辑本身就是很抽象的概念,难理解,也很难区分,我也不敢往细了去说,因为说多了怕被你们发现其实我也是在裸泳。







请到「今天看啥」查看全文


推荐文章
开发者全社区  ·  97岁李嘉诚
18 小时前
开发者全社区  ·  北京的普普通通有钱人
23 小时前
开发者全社区  ·  字节跳动出手:直接辞退353人!
昨天
开发者全社区  ·  UCL色魔博士被抓
2 天前
开发者全社区  ·  人大发TT了
2 天前
慈怀读书会  ·  心疼别人,也终会被这个世界心疼
7 年前