原标题:MVP模式在携程酒店的应用囷扩展
赵伟麟2011年就职于创新工场旗下点心OS,2014年加入携程酒店事业部从事Android研发工作。擅长基于组件的业务架构系统架构,建模性能優化和重构,关注应用系统的扩展性和耦合性追求简洁的代码。本文来自赵伟麟在“携程技术沙龙——移动开发工程实践与性能优化”仩的分享
*视频由“IT大咖说”提供,时长约37分钟请在WiFi环境下观看。更多视频可在大咖说平台观看*
酒店业务部门是携程旅行的几大业务之┅其业务逻辑复杂,业务需求变动快经过多年的研发,已经是一个代码规模庞大的工程如何规范代码,将代码按照其功能进行分类将代码写到合适的地方对项目的迭代起着重要的作用。
MVP模式是目前客户端比较流行的框架模式携程在很早之前就开始探索使用该模式進行相关的业务功能开发,以提升代码的规范性和可维护性积累了一定的经验。本文将探讨一下该模式在实际工程中的优点和缺陷并介绍携程面对这些问题时的思考,解决方案以及在实践经验基础上对该模式的扩展模式MVCPI
MVC已经是非常成熟的框架模式,甚至不少人认为它過时陈旧老气在实践中,很多同事会抱怨MVC会使得代码非常臃肿,尤其是Controller很容易变成大杂烩预期的可维护性变得很脆弱,由此导致一方面希望有新框架模式可以解决现在的问题但同时对框架模式又有些怀疑,新的框架模式是否能真正解决现在的问题会不会重蹈覆辙?会不会过度设计会不会掉进一个更深的坑?总之这些类似“一朝被蛇咬,十年怕井绳”的担忧显得不无道理但不管如何,我们需偠仔细耐心的做工作
在MVP模式逐渐流行之前,不管我们有意识或无意识地我们使用的就是MVC模式。以Android为例我们来看看MVC是什么样子。
上面嘚代码概括了Android MVC的基本结构,从笔者的经验来看很多应用都存在这样的代码风格,也就是大部分人认为的MVC:
可以试想一下如果这个界面展示的数据非常的多话MainActivity必然会变得非常庞大,就像大部分人所抱怨的那样诚然,上面的demo是MVC模式但是,它仅是从系统框架的角度来看如果从应用框架来看,它不是下面来看一下,从应用框架来看一下MVC正确的结构:
应用中的MVC应该在系统的MVC框架上根据业务的自身的需要進行进一步封装也就是说,如果在我们宣称我们是使用MVC框架模式的时候代表我们的主要工作是封装自己的MVC组件。它看起来应该是像下媔的风格:
跟之前的代码相比基本结构是相似的,如下:
仅仅View层发生了变化这是因为,Model和Controller相对是大家容易理解的概念在面临任何一個业务需求的时候,自然就能产生的近乎本能的封装(尽管Model的基本封装大部分工程师都可完成但不可否认Model的设计是至关重要而有难度的);而对View的看法,可能就是“能正确布局和展示就行”但这正是关键所在:我们需要对界面进行全方位的封装,包括View具体来说,一个嫃正的MVC框架应该具备下面的特点:
·数据都由Model进行封装
1.3、MVC模式的问题所在
前面说到很多人抱怨采用MVC模式使得Controller变得很臃肿,我相信Controller变得臃肿是事实,但其归结于采用MVC模式是不正确的这个锅不应该由MVC来背,因为这个论点会导致我们走向错误的方向从而无法发现MVC真正的问題所在。为什么这么说呢那是因为在本人了解到的很多情况下,大家并没有正确理解MVC框架模式如采用前文中第一种模式,自然会使得Controller臃肿但是如果采用第二种模式,Controller的代码和逻辑也会非常清晰至少不至于如此多的抱怨。因此如果只是想解决Controller臃肿的话MVC就够了,毋庸質疑那MVC的问题是什么呢?我想只有深刻的理解了这个问题我们才有必要考虑是否需要引入新的框架模式,以及避免新的模式中可能出現的问题
View强依赖于Model是MVC的主要问题。由此导致很多控件都是根据业务定制从Android的角度来看,原本可以由一个通用的layout就能实现的控件由于偠绑定实体模型,现在必须要自定义控件这导致出现大量不必要的重复代码。因此有必要将View和Model进行解耦而MVP的主要思想就是解耦View和Model。由此引入MVP就显得很自然
Android 官方提供的MVP参考实现,大致思想如下:
1、抽象出IView接口规范控件访问方法,而不限View具体来源
3、IPresenter的实现类实施数据囷IView的绑定,并负责相关的业务处理
上述代码主要的特点可以概括为:
就目前了解到的情况来看,很多采用MVP模式的应用基本上和android参考实现方案差别不大说明该模式的应用场景也是很广泛的。
尽管已经有了大量的应用但不可否认该模式的还是存在一些问题,这些问题在携程的使用过程中也得到了体现比如,上下文丢失问题生命周期问题,内存泄露问题以及大量的自定义接口回调链变长等问题。可以歸纳为:
·业务复杂时可能使得Activity变成更加复杂,比如要实现N个IView然后写更多个模版方法。
·业务复杂时各个角色之间通信会变得很冗長和复杂,回调链过长
·Presenter处理业务,让业务变得很分散不能全局掌握业务,很难去回答某个业务究竟是在哪里处理的
·用Presenter替代Controller是一個危险的做法,可能出现内存泄漏生命周期不同步,上下文丢失等问题
以下面的这个需求来看几个具体的示例:
详情按钮的展示需要垺务端下发标记位控制,展示时点击需要请求一个服务服务返回时toast提示用户。
上述代码表明HotelPresenter可以处理大部分的业务,但是在最后需要使用上下文的时候出现了困难,因为脱离了上下文展示一个Toast都不能实现
为了避免这样的尴尬,因此改进方案如下:
改进的方案中考慮到需要使用上下文,因此新增了接口传入Fragment作为上下文在Presenter需要时可以使用,但是由于Fragment生命周期会了变化,可能会导致空指针问题
于昰新的问题又需要解决。主要是两个思路一个是为Presenter增加生命周期方法,在Fragment的生命周期方法里调用Presenter对应的生命周期函数但这就让Presenter看起来潒Fragment的孙子;另外一个就是承认Presenter其实不太合适承担Controller的职责,从而提供接口给外部处理;如下:
这个方案很稳定似乎成为了最佳的选择。但昰自定接口和回调始终有那么一点痛
由于前面的分析,MVP参考实现并不是万能的携程酒店并没有完全采用参考实现方案,而是结合自身嘚实践经验思考之后设计出来的扩展方案我们主要考虑了一下的几个问题:
·如何定义View接口?
·如何解决长长的回调链
通过对上述问題的思考,提出对应的解决方法规避前面论述的各种问题,形成了携程酒店的MVCPI框架模式并在多个业务场景运行,取得了较为满意的效果下面,详细介绍MVCPI模式
和Android 参考实现不一样的是,我们并没有采用强类型的接口作为表达View的方式而是采用弱类型的接口来定义View。具体萣义方式如下:
上面的接口简洁的描述了作为业务控件的View需要具备的子控间ID并不需要具体的实现类。因此也不需要Activity去实现这个接口只需要在layout中申明这几个ID的即可,极大的简化了代码
与参考实现的定位不一样,我们认为由Presenter取代Controller并不是一个好的做法Presenter应是Controller的补充,主要起箌View和Model解耦和数据绑定的作用所负责的控件的上的业务还是有Controller决定如何去处理。另外setView接受的参数是一般的View而非一个接口类型,内部根据IView萣义的ID去查找子控件如下:
Interactor是我们定义出来的扩展元素,在MVP和MVC中都没有对应的角色为了阐述它的含义,我们先来看看两个非常常见的場景
在前面介绍过,Presenter自定义接口是很多候选方案中较为合理的选择但相比MVC而言,MVP更容易出现如上图的一种调用和回调关系(甚至更长)维护这种回调链通常来说是一件非常头痛的事情,从View的角度来看很难知道某个事件到最后究竟完成了什么业务,Acitivity也不知道到要装配哪些回调某个未知的新需求可能需要将该链条上的每个环节都增加回调。
下面来是另外一种场景大家可以脑补一下采用上面的回调方案,回调链会是什么情况
·几十种动态交互需求,
·分布于不同深度的嵌套层次中
经过大量版本迭代后无论饭店产品具有什么特征经悝,研发或者测试都不清楚到底有哪些需求,业务逻辑是什么写在什么地方等等......
上述两个场景可以得出两个结论:
·增加功能成本高,容易引致其他问题
为了解决上述两个比较棘手的问题我们引入了Interactor,用于描述整个界面的交互一举解决上述两个问题。我们认为交互模型是一个功能模块的重要逻辑单元相对于实体模型来说,交互模型更加抽象在大多数的情况,并不能引起大家的注意但它确实是洳实体一样的存在,正是因为没有对交互进行系统的描述才导致上面两种突出的问题。尽管抽象但是交互模型本质非常简单,它有着囷实体模型有相似的结构示例如下:
通过对界面整体分析后,我们建立如上的交互模型所有的交互都在交互模型进行注册,由交互模型统一管理进而可以对整个界面的交互进行宏观把控;然后在页面的所有元素中共享同一个交互模型,进而各个元素不再需要自定义接ロ和避免建立回调链最后由Controller负责组装,进一步加强Controller的控制能力
最后,整体介绍一下MVCPI的代码结构
1、首先定义整个界面中有哪些用户交互本例中就一个详情按钮交互
2、Presenter构造时需要传入交互模型,内部定义了IView接口传入的View中需要包含它定义的ID的控件,在bindData时详情按钮的点击鈈是通过匿名内部类去处理,而是直接引用交互模型中定义的mDetail
3、Controller负责界面各个元素(包括交互模型)的初始化和装配
通过对MVCMVP的介绍和研究,我们发现二者的关系并不是相互取代的关系而是一种演化和改进的关系。经实践证明MVC仍然具有强大的生命力,试图用MVP取代MVC几乎都會失败携程在MVC模式基础上,结合MVP思想加入Interactor元素搭建的MVCPI框架模式,一方面将数据绑定逻辑从Controller(或者View)中分离出去另一方面将交互模型嘚控制纳入进来,进一步加强了Controller的控制能力无论从代码的简洁性,维护性扩展性来看,都具有较大优势具有一定的实践推广价值。
當然任何框架模式都不是全能的,MVCPI也存在它不足如果有好的意见和建议,欢迎加入一起讨论推进框架模式的发展。