MVX移动端架构讨论

cover

上周和团队讨论了MVVM在Android应用中的实现,通过讨论一些实际应用上遇到的场景,也回顾了自己之前写过的一些代码,让我对MVX这类设计的理解更清晰了。下面分享一些我对MVX这个话题的看法。

MVX的意义

MVC,CLEAN,MVP,MVVM。在我看来,这些架构的出现,都是为了解决一个问题:使用一种普适的组织方式,将软件根据逻辑职责分成不同的部分,以便构建更健壮的软件

健壮的一个方面是指逻辑的严密性。比如在语言层面,强类型语言相比于弱类型语言虽然缺失灵活性,但是健壮性显然更好。因为语言的逻辑和规则限制了开发者无法将一个int类型的值赋给String类型的变量。

另外一个方面,如果这个结构是可测的,那么我们可以编写测试代码对软件进行自动化的测试。对于未来的改动,测试代码能验证软件行为是一致的。这也是健壮性的体现。

如果一个模式能严格定义程序不同部分的职责,并且不同部分的耦合度足够低,不同部分都是可测的,那么这个模式就能构建更健壮的软件。

几种MVX的区别

我们在设计一个应用架构时,不能为了实现某种模式而设计一种结构,而是要理解这些模式的思想,并让其为我们所用。

不管MVX中的X是什么,都是将软件分成了三个部分:展示层(View),数据模型层(Model),逻辑控制层(Controller)。所以我们不要纠结于名字,要借鉴这个思想,设计适合的模式。

除此之外,移动端架构(Android、iOS)相比于服务端架构有一个明显的区别,有一个必不可少的角色:Context上下文。这个问题在Android上尤为突出,因为访问任何系统组件和资源,包括完成页面跳转都需要Activity这个View层组件。所以,Activity不仅仅承担View层的角色,同时带有“上下文控制器的角色”。

开篇列出的MVC、MVP、MVVM都将程序分成了三个部分(层),主要区别在于展示层和逻辑层(Controller、Presenter、ViewModel)的职责定义和通信方式。所以在我看来,这三种模式的内在逻辑是一致的,并没有本质上的区别

确定MVX架构

经过上面的讨论,我们可以总结出一种结构,不妨就还称作MVC。我将从三个方面来进行定义,分别是职责设定、数据和事件绑定、数据处理。

各部分职责

  • View
    • 负责将数据进行展示,并且在数据变化时进行相应。
    • 响应用户操作,并将操作传递给Controller。
    • 负责上下文切换。
  • Controller
    • 执行业务逻辑,更新业务数据。
    • 将数据变更通知给View层。
  • Model
    • 定义数据模型和操作接口。

figure1

数据or属性

数据是业务的基础,是逻辑操作的对象。如果没有数据,那么就不会形成”功能“。但是数据这个词语本身具有误导性,很容易被错误的扩大含义范围,典型场景是把业务数据和展示数据混淆。所以我把业务数据称作数据,而展示数据,称作属性,予以区分。

只有业务数据应该放到Controller中,由Controller维护。想法,展示数据应该只停留在展示层。举个常见场景的例子,发送短信验证码场景中,会有一个逻辑是发送之后进行倒计时,倒计时过程中禁用发送按钮,并显示倒计时时间。

这个逻辑中,”等待验证码送达“和”倒计时剩余时间”是业务数据,而“按钮是否被禁用”和“按钮上的倒计时文案”是展示数据——属性

figure2

数据和事件绑定

我们可以借鉴MVVM中的通信方式,将数据和事件在View和Controller之间进行双向绑定。这个绑定通过DataBinding可以相对容易的实现。

然而,我认为数据绑定和事件绑定还有个细微的区别:数据绑定通过响应式的方式进行绑定,而事件绑定通过方法调用绑定。也就是Activity通过订阅Controller中的数据Observable完成数据绑定;通过调用Controller的方法进行事件绑定。

为什么这么说呢?因为事件绑定本身存在局限性。Controller暴露的本质上是行为,也就是执行任务的方法。这个方法的触发方式可能有很多,比如用户操作,生命周期方法,系统事件等等。如此多的绑定关系在如果由Controller来完成,将污染Controller的逻辑。但是绑定关系又要由观察者来完成,所以这里就出现了矛盾。Controller作为观察者,缺不适合作为多个绑定关系的场所

所以,我认为Controller应该暴露可观测数据任务执行方法这两种接口。在数据和事件绑定中使用两种不同的通信方式。这种形式可以带来一个好处:Controller的逻辑会非常“干净”。

figure3

几个场景的实现讨论

理论结合实践才有意义,实际情况总要更加负责,下面是几个场景,通过这几个场景的讨论,对上面概念的理解应该会更加明确。

错误处理

网络请求的错误处理是第一个场景,一个页面可能在发出多个网络请求,在同一时间可能有多个请求成功或者失败。那么对于网络请求的错误,该如何处理呢?

  1. 对于View层来说,不存在网络请求的概念。View层在触发某个事件时,都是调用Controller的一个方法来执行任务,至于这个任务是一个还是多个网络请求,View层并不应该关心。
  2. View层只从Controller暴露的可观测数据来监听任务执行的结果。
  3. Controller调用Model层的方法完成网络请求,如果请求出错,Controller应该将对应任务的状态设置为失败,这样View就能根据失败的情况进行响应。

页面状态

页面在数据请求过程和用户操作引起的变化过程中,可能出现多种状态:Placeholder、正常、失败等等,这些状态的切换应该如何控制呢?

  1. “页面状态”本质上View层的概念,所以Controller完全不需要关心,也不需要控制。
  2. 页面状态的切换,是通过数据源的变化来触发的,而数据源的变化,是任务的执行引起的,所以在View层,应该处理将“任务状态”转换成“页面状态”的逻辑。

页面跳转

页面间的跳转和切换,同样是View层的逻辑。前文也提到,Activity承担了上下文控制器的角色,所以页面的切换,应该由Activity承担。而Controller组件,仅仅作用于单个MVC组件内,对于MVC间切换,并不应该关心。

那么,有这样一个场景:多页面表单。用户在点击页面上的一个按钮时,需要切换到下个页面,下个页面可能是另一个表单,或者一个校验结果。这种情况下,跳转的逻辑难道不应该写到Controller里面,在校验完成之后,执行跳转操作吗?

我的观点是,Controller暴露的只有数据和任务。在这种情况下,用户触发的事件调用了Controller的一次任务执行;而任务执行一定要匹配一个任务执行结果,这个结果,就是View层应该监听的数据。所以是否需要跳转,是数据的监听逻辑需要处理的。而不是Controller执行的任务本身。

总结

  1. 不管是哪种分层架构,目的都是解耦各个模块,构建更健壮的应用。几种结构没有本质上的区别。
  2. 在这个话题中,健壮性主要体现为两点:逻辑清晰和可测试。
  3. 在逻辑上,Controller是一个MVC单元的核心;但是在上下文范畴上,View层是整个单元的核心。
  4. Controller暴露两类接口:可观测数据和任务执行方法。