在 Android 开发中使用 MVP 和 MVVM 模式早已不是新鲜事了,各类 MVP/MVVM 相关的文章、开源库也已家常便饭,甚至是让人眼花撩乱,那么我为何还要在这个早已被画满涂鸦的黑板上再来涂涂画画呢?是想彰显个人存在感吗?那固然!啊不不不……不彻底是!我还想要警醒读到这篇文章的各位:大家对于MVX的理解可能并不彻底正确!html
注:这篇文章里我将使用 MVX 作为 MVC 、MVP 以及 MVVM 的统称。java
咱们都知道 MVX 的进化过程是从滚球兽进化到 MVC ,而后从 MVC 进化到 MVP,再从 MVP 超进化到 MVVM。那么接下来,按照常规的套路,我应该要介绍什么是 MVC,什么是 MVP,以及什么是 MVVM,而且分别介绍M、V、C/P/VM 各自的职责了。数据库
达嘎,口头瓦鲁!后端
个人目的是想要纠正一些对 MVX 的错误认识,因此前提是你要对 MVX 有一些了解。为了不有人在使用 MVX 时走上弯路,因此决定对我看到的一些关于 MVX 的错误认识进行总结以及纠正。会产生这些错误认知的缘由,经我分析,实际上是:没有真正领会到 MVX 主义的核心价值观!其实 MVX 的核心思想也很简单,不要误会,不是富强、民主、……而是 将表现层和业务层分离 。网络
表现层和业务层分离,Matin Fowler 称之为 Separated Presentation。这里的表现层就是 VX,业务层就是 M。若是有人看到这里发现了和你认为的 MVX 不同的话,那么你对 MVX 的认识极可能就存在错误,严重者还多是走了修正主义路线!数据结构
从表现层和业务层分离的视角来看,M、V、X不是平等的身份,应该是 M 和 V-X。自始自终 M 的职责都没变,变的是 V-X,随着软件开发技术的发展、交互形式或者交互媒介的不断改变,表现层的逻辑也越来复杂,MVX 的进化过程就是一个不断探寻处理表现层复杂逻辑的过程。固然从一个形态进化到另外一个形态,并不必定是为了解决更复杂的交互逻辑,也多是有了一种“更优雅”的方式来处理表现层逻辑。架构
既然已经有表现层和业务层分离的概念了,那么第一个错误观点就很好解释了。app
这是一个很常见的错误观点,不少介绍 MVP 或者 MVVM 的文章都这么说过。正如前面所说,业务逻辑是属于 M 层的,那 Presenter 或者 ViewModel 是干什么的,处理表现层逻辑的吗?是的,或者说大部分表现层逻辑都是在 Presenter 或者 ViewModel 中处理的。以前我将业务层之上的这些逻辑称之为视图逻辑,如今为了统一就叫作表现层逻辑吧(加个吧字怎么感受怪怪的)。dom
我在这里就简单说一下什么是表现层逻辑,以及 View 和 Presenter/ViewModel 又是如何分工的。假设你的应用有一个我的资料的 profile 页面,这个页面有两种状态,一种是浏览状态,一种是编辑状态,经过一个编辑按钮触发状态的转换,编辑状态时,部分信息项能够进行编辑。那这里就有一个明显的表现层逻辑,那就是点击按钮切换浏览/编辑状态。ide
如今的 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),也是为了引出第二个错误观点。
咱们作业务模块开发时,会常常定义一些数据结构类,好比我的资料可能会对应一个 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层就是负责数据获取的,这也是我要说的第三个错误观点。稍等,我先写个标题⬇
产生这种错误认识的,说白了仍是没有搞懂业务逻辑。固然了业务逻辑自己就是很抽象的概念,难理解,也很难区分,我也不敢往细了去说,由于说多了怕被大家发现其实我也是在裸泳。
业务逻辑层并不负责数据的获取,数据的获取职责还要在 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 层,这必然都是不合适的。将业务逻辑放到了错误的层次里,势必会形成后续的维护性和扩展性问题。
还有一些人没有搞清楚 Model 层和上层的依赖关系,依赖关系写成了双向的,这是不对的,业务层不该该依赖表现层,而是应该反过来。
实际上应该是 Presenter/ViewModel 经过接口的形式依赖 Model 层,Model 层彻底不依赖 Presenter/ViewModel。就像我前面的示例代码里同样,Model 层必然不会出现任何 presenter 这样的单词,上层经过观察者模式来监听 Model 层的数据变化( LoadCallback 接口也算是一种),Model 层也不用关心上层是 Presenter 仍是 ViewModel。
读到这里,不知道大家对MVX的理解是否是更深了些呢?对表现层逻辑、业务逻辑是否是也有了更清晰的认识了呢?
其实关于 MVX 还有更多能够讨论的,好比有些人认为 Model 层并非真正处理业务逻辑的地方,它只是业务模块的一个上层封装层,我以为也不无道理,在复杂业务模块中,业务是存在层次的,MVX 中的 Model 层是全部业务层中的最上层。
还有我刚刚提到的业务层之下还有数据层,这是典型的三层架构的概念,即表现层、业务层和数据层。逻辑存在分层,因此架构也必然要进行分层,MVX 能够作为咱们从代码到业务甚至到架构的探索的开端。