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

在Android开发中使用MVP和MVVM模式早已不是新鲜事了,各种MVP/MVVM相关的文章、开源库也已屡见不鲜,甚至是让人眼花撩乱,那么我为什么还要在这个早已被画满涂鸦的黑板上再来涂涂画画呢?是想彰显我的存在感吗?那当然!啊不不不……不完全是!我还想要警醒读到这篇文章的各位:你们对于MVX的理解可能并不完全正确!

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

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

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

我的目的是想要纠正一些对MVX的错误认识,所以前提是你要对MVX有一些了解。为了避免有人在使用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的示例代码大概是这样的:

  1. public class ProfileView { 
  2.     void initView() { 
  3.         // 负责注册点击事件监听器,并将点击事件通知给presenter 
  4.         editStateButton.setOnClickListener(new OnClickListener() { 
  5.             presenter.onEditStateButtonClicked(); 
  6.         }) 
  7.         ... 
  8.     } 
  9.  // 显示浏览状态视图,想不到好名字,就叫showNormalState吧 
  10.     public void showNormalState() { 
  11.         // 浏览状态下编辑按钮提示文字为“编辑”,所有项不可编辑 
  12.         editStateButton.setText("编辑"); 
  13.         nickName.setEditable(false); 
  14.         ... 
  15.     } 
  16.     public void showEditState() { 
  17.         // 浏览状态下编辑按钮提示文字为“完成”,部分项要设置为可编辑 
  18.         editStateButton.setText("完成"); 
  19.         nickName.setEditable(true); 
  20.         ... 
  21.     } 
  22.  
  23.  
  24. public class ProfilePresenter { 
  25.     private State curState = State.NORMAL; 
  26.     public void onEditStateButtonClicked() { 
  27.         // 按钮被点击时,根据当前状态判断View应该切换显示的状态 
  28.         // 这就是表现层逻辑 
  29.         if (isInEditState()) { 
  30.             curState = State.NORMAL; 
  31.             view.showNormalState(); 
  32.         } else { 
  33.             curState = State.EDIT; 
  34.             view.showEditState(); 
  35.         } 
  36.     } 
  37.     private boolean isInEditState() { 
  38.         return curState == State.EDIT; 
  39.     } 
  40.     @VisibleForTest 
  41.     void setState(State state) { 
  42.         curState = state; 
  43.     } 

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

能感受到我想表达的意思吗?就是Presenter/ViewModel根据当前交互状态决定该显示什么,而View要做的是如何显示它们。再比如说下拉刷新的场景,由View告诉Presenter/ViewModel,它接收到了下拉事件,然后Presenter/ViewModel再告诉View,让它去显示刷新提示视图,至于这个刷新提示长什么样就由View来决定。当然Presenter/ViewModel也可能会判断当前网络不可用,而让View显示一个网络不可用的提示视图。

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

  1. public class ProfilePresenterTest { 
  2.     private ProfilePresenter presenter; 
  3.     private ProfileView view
  4.     @Test 
  5.     public void testShowEditStateOnButtonClick() { 
  6.         // 浏览状态下点击编辑按钮,验证View是否显示了编辑状态视图 
  7.         // 也就是验证view.showEditState()方法是否被调用了 
  8.         presenter.setState(State.NORMAL); 
  9.         presenter.onEditStateButtonClicked(); 
  10.         Mockito.verify(view).showEditState(); 
  11.     } 
  12.     @Test 
  13.     public void testShowNormalStateOnButtonClick() { 
  14.         // 编辑状态下点击完成按钮,验证View是否显示了浏览状态视图 
  15.         // 也就是验证view.showNormalState()方法是否被调用了 
  16.         presenter.setState(State.EDIT); 
  17.         presenter.onEditStateButtonClicked(); 
  18.         Mockito.verify(view).showNormalState(); 
  19.     } 

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

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

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层完成。示例代码大概是下面这样:

  1. public class RecommendBlogFeedPresenter { 
  2.     private RecommendBlogFeedView view
  3.     private BlogMode model; 
  4.     public void onStart() { 
  5.         view.showLoadWait(); 
  6.         model.loadRecommendBlogs(new LoadCallback<>() { 
  7.             @Override 
  8.             public void onLoaded(List<Blog> blogs) { 
  9.                 view.showBlogs(blogs); 
  10.             } 
  11.         }) 
  12.     } 
  13.  
  14.  
  15. public interface BlogModel { 
  16.     void loadRecommendBlogs(LoadCallback<List<Blog>> callback); 
  17. public class BlogModelImpl implements BlogModel { 
  18.     private BlogFeedRepository repo; 
  19.     @Override 
  20.     public void loadRecommendBlogs(LoadCallback<List<Blog>> callback) { 
  21.         // BlogFeedRepository.fetch()很可能是耗时操作,所以实际写的时候会在非主线程执行,这里只是示例 
  22.         callback.onLoaded(repo.fetch("recommend")); 
  23.     } 
  24. public interface BlogFeedRepository { 
  25.     List<Blog> fetch(String tag); 

什么?你这个BlogModelImpl里就这一行代码,你跟我说这是业务逻辑?大家冷静一下,把手里的板砖、砍刀、狼牙棒先放下来。BlogModelImpl类里面的逻辑虽然简单,但是它的确是业务逻辑,也正是因为业务逻辑比较简单,所以BlogModelImpl类才会很简洁。

再从Presenter的角度看一下,为什么loadRecommendBlogs()属于业务逻辑。博客这个概念毫无疑问属于业务概念,根据前面的解释应该可以判断出来“获取推荐的博客列表”不属于表现层逻辑,那么这个逻辑的实现就不是Presenter需要关心的,那就应该是Model层的职责,既然是Model层的那就应该是业务逻辑了;再者,既然博客是业务概念,那么Blog就是业务数据的数据结构,loadRecommendBlogs()涉及到对业务数据Blog的创建及组装等操作,所以也应该是业务逻辑。

看到这里,可能有些人会产生一些误解:所谓的业务逻辑处理就是网络请求、数据库查询等数据获取逻辑,即Model层就是负责数据获取的,这也是我要说的第三个错误观点。稍等,我先写个标题⬇

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

产生这种错误认识的,说白了还是没有搞懂业务逻辑。当然了业务逻辑本身就是很抽象的概念,难理解,也很难区分,我也不敢往细了去说,因为说多了怕被你们发现其实我也是在裸泳。

业务逻辑层并不负责数据的获取,数据的获取职责还要在Model层的更下层,这也是为什么我要把的BlogModel的实现逻辑写得如此简单,因为数据获取的职责全部交给了BlogFeedRepository类,Model层只处理业务逻辑。BlogFeedRepository是博客列表的仓储类,BlogModel通过BlogFeedRepository的fetch()方法获取标签为recommend的博客列表,也就是推荐的博客列表。BlogModel不关心BlogFeedRepository是如何获取对应博客数据的,它可以是从通过网络请求获取的,也可以是从本地数据库中获取的,数据源有任何改变也不应该影响到BlogModel中的业务逻辑。

那么既然BlogModel中的业务逻辑如此简单,为什么要强行增加这么一个Model层,而不是让Presenter直接使用BlogFeedRepository类去获取数据呢?

当然是有原因的!假设我们刚才介绍的“掘铁”app,在仅有一个博客列表页面的情况下,依然吸引了很多用户去使用,产品经理此时决定尝试探索变现手段,首先是在博客推荐列表中添加广告数据。再假设,由于广告数据和博客数据分属不同的后端团队,两边数据尚未整合打通,暂时由客户端负责把广告数据添加到博客列表中。这个时候,BlogModel终于凸显了它存在的必要性。表现层不负责广告数据的获取与整合,BlogFeedRepository也不能负责广告数据的获取与整合。广告数据的整合是业务逻辑,由BlogModel负责,广告数据的获取由专门的数据仓储类负责。示例代码如下:

  1. public class BlogModelImpl implements BlogModel { 
  2.     private BlogFeedRepository blogRepo; 
  3.     private AdRepository adRepo; 
  4.     private BlogAdComposeStrategy composeStrategy; 
  5.     private AdBlogTransform transform; 
  6.     @Override 
  7.     public void loadRecommendBlogs(LoadCallback<List<Blog>> callback) { 
  8.         List<BlogAd> ads = adRepo.fetch("recommend"); 
  9.         List<Blog> blogs = blogRepo.fetch("recommend"); 
  10.         // 在这里把广告数据整合到博客列表中 
  11.         blogs = composeStrategy.compose(blogs, ads, transform); 
  12.         callback.onLoaded(blogs); 
  13.     } 
  14. public interface AdRepository { 
  15.     List<BlogAd> fetch(String tag); 
  16. public interface BlogAdComposeStrategy { 
  17.     List<Blog> compose(List<Blog> blogs, List<BlogAd> ads, AdBlogTransform transoform); 
  18. public interface AdBlogTransform { 
  19.     Blog transform(BlogAd ad); 

考虑到广告和博客可能有不同的整合策略,可以按需替换不同的实现,所以把整合策略封装到了BlogAdComposeStrategy接口中。整合策略也属于业务逻辑,但是因为整合策略的实现细节这里不需要关注,所以我觉得不写出来也行,反正都是我编的。

这里我想表达的是,获取广告数据并将广告数据整合到博客列表中也是业务逻辑的一部分,如果省略Model层将会造成得把广告的整合逻辑放到Presenter或者Repository层,这必然都是不合适的。将业务逻辑放到了错误的层次里,势必会造成后续的维护性和扩展性问题。

错误四:Model层依赖Presenter/ViewModel层

还有一些人没有搞清楚Model层和上层的依赖关系,依赖关系写成了双向的,这是不对的,业务层不应该依赖表现层,而是应该反过来。

实际上应该是Presenter/ViewModel通过接口的形式依赖Model层,Model层完全不依赖Presenter/ViewModel。就像我前面的示例代码里一样,Model层必然不会出现任何presenter这样的单词,上层通过观察者模式来监听Model层的数据变化(LoadCallback接口也算是一种),Model层也不用关心上层是Presenter还是ViewModel。

最后

读到这里,不知道你们对MVX的理解是不是更深了些呢?对表现层逻辑、业务逻辑是不是也有了更清晰的认识了呢?

相关推荐