关于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 各自的职责了。
我的目的是想要纠正一些对 MVX 的错误认识,所以前提是你要对 MVX 有一些了解。为了避免有人在使用 MVX 时走上弯路,所以决定对我看到的一些关于 MVX 的错误认识进行总结以及纠正。
会产生这些错误认知的原因,经我分析,其实是:没有真正领会到将表现层和业务层分离 。
1表现层和业务层分离
表现层和业务层分离,Matin Fowler 称之为 Separated Presentation。这里的表现层就是 VX,业务层就是 M。如果有人看到这里发现了和你认为的 MVX 不一样的话,那么你对 MVX 的认识很可能就存在错误!
从表现层和业务层分离的视角来看,M、V、X不是平等的身份,应该是 M 和 V-X。
自始自终 M 的职责都没变,变的是 V-X,随着软件开发技术的发展、交互形式或者交互媒介的不断改变,表现层的逻辑也越来复杂,MVX 的进化过程就是一个不断探寻处理表现层复杂逻辑的过程。
当然从一个形态进化到另一个形态,并不一定是为了解决更复杂的交互逻辑,也可能是有了一种“更优雅”的方式来处理表现层逻辑。
既然已经有表现层和业务层分离的概念了,那么第一个错误观点就很好解释了。
2错误一: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),也是为了引出第二个错误观点。
3错误二: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<Blog> blogs) {
view.showBlogs(blogs);
}
})
}
}
public interface BlogModel {
void loadRecommendBlogs(LoadCallback<List<Blog>> callback);
}
public class BlogModelImpl implements BlogModel {
private BlogFeedRepository repo;
@Override
public void loadRecommendBlogs(LoadCallback<List<Blog>> callback) {
// BlogFeedRepository.fetch()很可能是耗时操作,所以实际写的时候会在非主线程执行,这里只是示例
callback.onLoaded(repo.fetch("recommend"));
}
}
public interface BlogFeedRepository {
List<Blog> fetch(String tag);
}
什么?你这个 BlogModelImpl 里就这一行代码,你跟我说这是业务逻辑?
大家冷静一下,把手里的板砖、砍刀、狼牙棒先放下来。BlogModelImpl 类里面的逻辑虽然简单,但是它的确是业务逻辑,也正是因为业务逻辑比较简单,所以 BlogModelImpl 类才会很简洁。
再从 Presenter 的角度看一下,为什么 loadRecommendBlogs() 属于业务逻辑。
博客这个概念毫无疑问属于业务概念,根据前面的解释应该可以判断出来“获取推荐的博客列表”不属于表现层逻辑,那么这个逻辑的实现就不是 Presenter 需要关心的,那就应该是 Model 层的职责,既然是 Model 层的那就应该是业务逻辑了;
再者,既然博客是业务概念,那么 Blog 就是业务数据的数据结构,loadRecommendBlogs() 涉及到对业务数据 Blog 的创建及组装等操作,所以也应该是业务逻辑。
看到这里,可能有些人会产生一些误解:
所谓的业务逻辑处理就是网络请求、数据库查询等数据获取逻辑,即Model层就是负责数据获取的,这也是我要说的第三个错误观点。
稍等,我先写个标题⬇
4错误三:Model 层就是负责数据获取的
产生这种错误认识的,说白了还是没有搞懂业务逻辑。
当然了业务逻辑本身就是很抽象的概念,难理解,也很难区分,我也不敢往细了去说,因为说多了怕被你们发现其实我也是在裸泳。
业务逻辑层并不负责数据的获取,数据的获取职责还要在 Model 层的更下层,这也是为什么我要把的 BlogModel 的实现逻辑写得如此简单,因为数据获取的职责全部交给了 BlogFeedRepository 类,Model 层只处理业务逻辑。
BlogFeedRepository 是博客列表的仓储类,BlogModel 通过 BlogFeedRepository 的 fetch() 方法获取标签为 recommend 的博客列表,也就是推荐的博客列表。
BlogModel 不关心 BlogFeedRepository 是如何获取对应博客数据的,它可以是从通过网络请求获取的,也可以是从本地数据库中获取的,数据源有任何改变也不应该影响到 BlogModel 中的业务逻辑。
那么既然 BlogModel 中的业务逻辑如此简单,为什么要强行增加这么一个 Model 层,而不是让 Presenter 直接使用 BlogFeedRepository 类去获取数据呢?
当然是有原因的!假设我们刚才介绍的“掘铁” app,在仅有一个博客列表页面的情况下,依然吸引了很多用户去使用,产品经理此时决定尝试探索变现手段,首先是在博客推荐列表中添加广告数据。
再假设,由于广告数据和博客数据分属不同的后端团队,两边数据尚未整合打通,暂时由客户端负责把广告数据添加到博客列表中。
这个时候,BlogModel 终于凸显了它存在的必要性。表现层不负责广告数据的获取与整合,BlogFeedRepository 也不能负责广告数据的获取与整合。
广告数据的整合是业务逻辑,由 BlogModel 负责,广告数据的获取由专门的数据仓储类负责。
示例代码如下:
public class BlogModelImpl implements BlogModel {
private BlogFeedRepository blogRepo;
private AdRepository adRepo;
private BlogAdComposeStrategy composeStrategy;
private AdBlogTransform transform;
@Override
public void loadRecommendBlogs(LoadCallback<List<Blog>> callback) {
List<BlogAd> ads = adRepo.fetch("recommend");
List<Blog> blogs = blogRepo.fetch("recommend");
// 在这里把广告数据整合到博客列表中
blogs = composeStrategy.compose(blogs, ads, transform);
callback.onLoaded(blogs);
}
}
public interface AdRepository {
List<BlogAd> fetch(String tag);
}
public interface BlogAdComposeStrategy {
List<Blog> compose(List<Blog> blogs, List<BlogAd> ads, AdBlogTransform transoform);
}
public interface AdBlogTransform {
Blog transform(BlogAd ad);
}
考虑到广告和博客可能有不同的整合策略,可以按需替换不同的实现,所以把整合策略封装到了 BlogAdComposeStrategy 接口中。整合策略也属于业务逻辑,但是因为整合策略的实现细节这里不需要关注,所以我觉得不写出来也行,反正都是我编的。
这里我想表达的是,获取广告数据并将广告数据整合到博客列表中也是业务逻辑的一部分,如果省略 Model 层将会造成得把广告的整合逻辑放到 Presenter 或者 Repository 层,这必然都是不合适的。将业务逻辑放到了错误的层次里,势必会造成后续的维护性和扩展性问题。
5错误四:Model 层依赖 Presenter/ViewModel 层
还有一些人没有搞清楚 Model 层和上层的依赖关系,依赖关系写成了双向的,这是不对的,业务层不应该依赖表现层,而是应该反过来。
实际上应该是 Presenter/ViewModel 通过接口的形式依赖 Model 层,Model 层完全不依赖 Presenter/ViewModel。就像我前面的示例代码里一样,Model 层必然不会出现任何 presenter 这样的单词,上层通过观察者模式来监听 Model 层的数据变化( LoadCallback 接口也算是一种),Model 层也不用关心上层是 Presenter 还是 ViewModel。
6最后
读到这里,不知道你们对MVX的理解是不是更深了些呢?
对表现层逻辑、业务逻辑是不是也有了更清晰的认识了呢?
其实关于 MVX 还有更多可以讨论的,比如有些人认为 Model 层并不是真正处理业务逻辑的地方,它只是业务模块的一个上层封装层,我觉得也不无道理,在复杂业务模块中,业务是存在层次的,MVX 中的 Model 层是所有业务层中的最上层。
还有我刚刚提到的业务层之下还有数据层,这是典型的三层架构的概念,即表现层、业务层和数据层。逻辑存在分层,所以架构也必然要进行分层,MVX 可以做为我们从代码到业务甚至到架构的探索的开端。
Android学习PDF+架构视频+面试文档+源码笔记