当前位置: 首页 > 科技观察

关于MVC-MVP-MVVM的一些误解

时间:2023-03-13 06:37:29 科技观察

在Android开发中使用MVP和MVVM模式已经不是什么新鲜事了。各种MVP/MVVM相关的文章和开源库也是屡见不鲜,甚至让人眼花缭乱。那我为什么还要在这块已经满是涂鸦的黑板上作画呢?是为了显示我的存在感吗?当然!啊不不不……不完全是!还是要提高警惕,看完这篇文章的各位:你们对MVX的理解未必完全正确!注意:在本文中,我将使用MVX作为MVC、MVP和MVVM的总称。我们都知道MVX的进化过程是从RollingBeast到MVC,再从MVC到MVP,再从MVP到MVVM。那么,按照常规套路,我应该先介绍一下什么是MVC,什么是MVP,什么是MVVM,并介绍一下M、V、C/P/VM各自的职责。我的目的是纠正一些对MVX的误解,所以前提是你对MVX有一定的了解。为了避免有人在使用MVX时走弯路,我决定总结并纠正我看到的一些关于MVX的误解。造成这些误解的原因,经过我的分析,其实是:我没有真正理解MVX主义的核心价值!其实MVX的核心思想也很简单。不要误会我的意思。与业务层分开。表现层和业务层的分离表现层和业务层的分离被MatinFowler称为SeparatedPresentation。这里的表现层是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是怎么划分的。假设您的应用程序有一个个人数据的配置文件页面。这个页面有两种状态,一种是浏览状态,一种是编辑状态。状态转换由编辑按钮触发。编辑状态时,可以编辑一些信息项。然后这里有一个很明显的表现层逻辑,就是点击按钮切换浏览/编辑状态。当前流行的MVP形式(或变体)称为被动视图。和MVVM一样,现在倾向于把表现层的几乎所有逻辑都交给Presenter或者ViewModel处理。View层需要做的事情很少,基本就是接受用户事件,然后将用户事件传递给Presenter或者ViewModel。以上面的个人资料页面为例进行说明,View层负责接收编辑按钮的点击事件,然后通知Presenter/ViewModel,然后Presenter/ViewModel通知View是否显示该视图浏览状态或视图的编辑状态。MVP的示例代码大概是这样的:publicclassProfileView{voidinitView(){//负责注册点击事件监听器,并将点击事件通知给presentereditStateButton.setOnClickListener(newOnClickListener(){presenter.onEditStateButtonClicked();})...}//显示浏览状态视图,如果想不出好听的名字就叫它beeditededitStateButton.setText("edit");nickName.setEditable(false);...}publicvoidshowEditState(){//浏览状态下,编辑按钮的提示文字为“完成”,部分项目应该设置为可编辑editStateButton.setText("Complete");nickName.setEditable(true);...}}publicclassProfilePresenter{privateStatecurState=State.NORMAL;publicvoidonEditStateButtonClicked(){//当按钮被点击时,判断按钮的状态View应该根据当前状态切换显示//This是表示层的逻辑if(isInEditState()){curState=State.注意:此示例代码只是为了展示表示层的逻辑。不涉及Model层,编译不会通过!你能感受到我想表达的意思吗?这意味着Presenter/ViewModel根据当前的交互状态决定显示什么。View需要做的是如何显示它们。比如下拉刷新场景,View告诉Presenter/ViewModel自己收到了下拉事件,然后Presenter/ViewModel告诉View显示刷新提示视图。至于刷新提示是什么样子的,是由View决定的。当然Presenter/ViewModel也可能判断当前网络不可用,让View显示网络不可用的提示视图。为什么要让Presenter/ViewModel处理几乎所有的表现层逻辑呢?主要是为了提高可测试性,将尽可能多的表现层逻辑纳入单元测试范围。因为对视图控件的显示等进行单元测试难度太大,View基本无法进行单元测试,而Presenter/ViewModel是完全可以进行单元测试的:在浏览状态下点击编辑按钮,验证View是否显示编辑状态视图//即验证是否调用了view.showEditState()方法presenter.setState(State.NORMAL);presenter.onEditStateButtonClicked();Mockito.verify(view).showEditState();}@TestpublicvoidtestShowNormalStateOnButtonClick(){//在编辑状态下,点击Finish按钮,验证View是否显示浏览状态view//即验证view.showNormalState()方法被调用presenter.setState(State.EDIT);presenter.onEditStateButtonClicked();Mockito.verify(view).showNormalState();}}你看,这些表示层逻辑可以是un它测试了!也许你明白我的意思?好的,现在您知道了表示层,那么业务层是做什么用的呢?现在我们来谈谈M。什么是M?M指的是那些喜欢从受虐中获得性爱的人……哦,对不起,我糊涂了!哎~见多识广是麻烦!M人,Model,再长一点就是DomainModel,中文名是DomainModel。我们看一下维基百科上对领域模型的定义:在软件工程中,领域模型是一个包含了行为和数据的领域的概念模型。这个怎么样?容易理解吗?当然不是!理解为Model层处理业务逻辑,现在又多了一个MMM...Domain,不知道该往哪想了!Domain,简单理解为业务,我觉得没什么问题。我在这里引用这句话主要是为了强调Model层包含业务数据和对业务数据(行为和数据)的操作,也是为了导致第二种错误的观点。误区二:模型是静态业务数据我们在开发业务模块的时候,往往会定义一些数据结构类。比如个人数据可能对应一个UserProfile类,一个订单数据可能对应一个Order类。这些类没有任何逻辑。只有一些简单的getter和setter方法。有些人认为像UserProfile或Order这样的数据结构类是一个模型。我们强调了Model层包含业务数据和对业务数据的操作。像UserProfile或Order这样的数据结构类的实例甚至不能称为对象。你可以阅读UncleBob的文章Classesvs.DataStructures。对象有行为。数据结构实例没有行为。连objects都调用不了,怎么能代表Model层呢!静态业务数据不能代表模型层。业务数据和对业务数据的操作共同构成了Model层,也就是业务逻辑。让我再举一个例子。假设您正在制作一个名为“DigIron”的应用程序。这个应用程序现在只有一个页面,用于显示推荐博客列表。OK,MVP的形式应该怎么写呢?让我们忽略与模型层没有交互的视图。Presenter层除了处理表现层的逻辑外,还会向Model层发送业务指令。注意Presenter并没有处理业务逻辑,真正的业务逻辑还是由Model层完成的。示例代码大概如下:}})}}publicinterfaceBlogModel{voidloadRecommendBlogs(LoadCallback>callback);}publicclassBlogModelImplimplementsBlogModel{privateBlogFeedRepositoryrepo;@OverridepublicvoidloadRecommendBlogs(LoadCallback>callback){//BlogFeedRepository.fetch()很可能是一次消耗操作,所以实际写的时候会在非主线程中执行,这里只是一个例子;}什么?在你的BlogModelImpl就这行代码,你告诉我这是业务逻辑?众人静下心来,先放下砖头、砍刀、狼牙棒。BlogModelImpl类中的逻辑虽然简单,但确实是业务逻辑。正是因为业务逻辑比较简单,所以BlogModelImpl类非常简洁。从Presenter的角度来看,为什么loadRecommendBlogs()属于业务逻辑。博客的概念无疑是一个商业概念。根据前面的解释,应该判断“获取推荐博客列表”不属于表现层的逻辑,所以这个逻辑的实现不是Presenter需要关心的,应该是模型层。责任,既然属于Model层,就应该是业务逻辑;而且,既然blog是一个业务概念,那么Blog就是业务数据的数据结构,loadRecommendBlogs()涉及业务数据Blog的创建、组装等操作,所以也应该是业务逻辑。看到这里,可能有人会有一些误解:所谓业务逻辑处理就是网络请求、数据库查询等数据获取逻辑,也就是Model层负责数据的获取。这也是我要讲的第三个错误观点。等一下,我先写个标题?误区三:Model层负责数据的获取,这种误区。说白了还是不懂业务逻辑。当然,业务逻辑本身就是一个很抽象的概念,很难理解,也很难区分。我不敢细说,怕你发现我在裸泳。业务逻辑层不负责数据的获取,数据获取的责任在Model层的下层。这也是为什么我把BlogModel的实现逻辑写得这么简单,因为所有数据获取的职责都交给了BlogFeedRepository类,模型层只处理业务逻辑。BlogFeedRepository是博客列表的存储类,BlogModel通过BlogFeedRepository的fetch()方法获取到标记为推荐的博客列表,即推荐博客列表。BlogModel并不关心BlogFeedRepository如何获取对应的博客数据。它可以从网络请求或本地数据库中获取。数据源中的任何更改都不应影响BlogModel中的业务逻辑。那么既然BlogModel中的业务逻辑这么简单,为什么还要强行添加这样一个Model层,而不是让Presenter直接使用BlogFeedRepository类来获取数据呢?当然是有原因的!假设我们刚刚推出的“挖铁”应用,在只有一个博客列表页面的情况下,仍然吸引了很多用户使用。产品经理此时决定尝试探索变现方式。首先,将广告数据添加到博客推荐列表中。进一步假设,由于广告数据和博客数据属于不同的后端团队,双方的数据还没有整合,客户端暂时负责将广告数据添加到博客列表中。这时,BlogModel终于凸显了它存在的必要性。表现层不负责广告数据的获取和整合,BlogFeedRepository不负责广告数据的获取和整合。广告数据的整合是业务逻辑,由BlogModel负责,广告数据的获取由专门的数据存储类负责。示例代码如下:publicclassBlogModelImplimplementsBlogModel{privateBlogFeedRepositoryblogRepo;privateAdRepositoryadRepo;privateBlogAdComposeStrategycomposeStrategy;privateAdBlogTransformtransform;@OverridepublicvoidloadRecommendBlogs(LoadCallback>callback){Listads=adRepo.fetch("recommend");Listblogs=blogRepo.fetch("recommend");//将广告数据整合到博客列表blogs=composeStrategy.compose(blogs,ads,transform);callback.onLoaded(blogs);}}publicinterfaceAdRepository{Listfetch(Stringtag);}publicinterfaceBlogAdComposeStrategy{Listcompose(Listblogs,Listads,AdBlogTransformtransoform);}publicinterfaceAdBlogTransform{Blogtransform(BlogAdad);}考虑到广告和博客可能有不同的整合策略,可以根据需要定制不同的实现被替换,所以集成策略被封装到BlogAdComposeStrategy接口中。集成策略也属于业务逻辑,但是因为集成策略的实现细节这里不需要关注,所以我觉得不写出来也可以,反正我是编的。这里想表达的是,获取广告数据,将广告数据集成到博客列表中,也是业务逻辑的一部分。如果省略Model层,会导致广告集成逻辑放在Presenter或Repository层,这肯定是错误的。合适的。将业务逻辑放在错误的层中,必然会导致后续的维护和扩展性问题。错误四:Model层依赖于Presenter/ViewModel层。有些人还没有搞清楚Model层和上层的依赖关系。依赖关系写成双向的。这是错误的。业务层不应该依赖于表现层,应该反过来。其实Presenter/ViewModel应该通过接口依赖Model层,而Model层根本不依赖Presenter/ViewModel。就像我之前的示例代码中,Model层肯定不会有presenter这样的字眼。上层通过观察者模式(LoadCallback接口也是一种)监听Model层的数据变化,Model层不需要关心上层。Presenter仍然是ViewModel。终于读到这里,不知道你对MVX有没有更深入的了解呢?是否对表现层逻辑和业务逻辑有了更清晰的认识?其实关于MVX还有更多值得讨论的地方,比如有些人认为Model层并不是真正处理业务逻辑的地方。它只是业务模块的上层封装层。我觉得不无道理。在复杂的业务模块中,存在业务层。MVX中的Model层是所有业务层中的最顶层。在刚才提到的业务层下面还有一个数据层,这是一个典型的三层架构概念,即表现层、业务层和数据层。逻辑有层次,架构也要分层。MVX可以成为我们从代码到业务,甚至到架构探索的开始。