MVVM后,下一代开发模式在哪?

简介: 写在前面讨论下一代开发模式的演化、优化方向和可能,不一定正确希望和感兴趣的读者交流。任何模式的选择一定要根据当时的开发需要来决定。比如:实验性、迭代很快的简单需求,一般会先选择MVC尝试,待明确方向后,再考虑改为MVVM。

写在前面

讨论下一代开发模式的演化、优化方向和可能,不一定正确希望和感兴趣的读者交流。

任何模式的选择一定要根据当时的开发需要来决定。比如:实验性、迭代很快的简单需求,一般会先选择MVC尝试,待明确方向后,再考虑改为MVVM。

背景

首先,看下iOS首推的MVC模式。

  • M:单纯的从网络获取回来的数据模型
  • V:视图界面
  • C:ViewController

image.png

ViewController负责View和Model之间调度,View发生交互事件会通过target-action或者delegate方式回调给ViewController,与此同时ViewController还要承担把Model通过KVO、Notification方式传来的数据传输给View用于展示的责任。

随着业务越来越复杂,视图交互越复杂,导致Controller越来越臃肿,负重前行。福报修多了的结果就是,不行了就重构,重构不了就换掉。


为了解决MVC带来的问题,MVVM出现了。

image.png

把View和Controller都放在了View层(相当于把Controller一部分逻辑抽离了出来),Model层依然是服务端返回的数据模型。而ViewModel充当了一个UI适配器的角色,也就是说View中每个UI元素都应该在ViewModel找到与之对应的属性。除此之外,从Controller抽离出来的与UI有关的逻辑都放在了ViewModel中,这样就减轻了Controller的负担。

从以上的架构图中,我们可以很清晰的梳理出各自的分工。

  • View层:视图展示。包含UIView以及UIViewController,View层是可以持有ViewModel的。
  • ViewModel层:视图适配器。暴露属性与View元素显示内容或者元素状态一一对应。一般情况下ViewModel暴露的属性建议是readOnly的,还有一点,ViewModel层是可以持有Model的。
  • Model层:数据模型与持久化抽象模型。数据模型很好理解,就是从服务器拉回来的JSON数据。而持久化抽象模型暂时放在Model层,是因为MVVM诞生之初就没有对这块进行很细致的描述。按照经验,我们通常把数据库、文件操作封装成Model,并对外提供操作接口。(有些公司把数据存取操作单拎出来一层,称之为DataAdapter层,所以在业内会有很多MVVM的变种,但其本质上都是MVVM)。
  • Binder:MVVM的灵魂。可惜在MVVM这几个英文单词中并没有它的一席之地,它的最主要作用是在View和ViewModel之间做了双向数据绑定。如果MVVM没有Binder,那么它与MVC的差异不是很大。

需要注意一点,View持有ViewModel,ViewModel不能持有View(即ViewModel不能依赖UIKit中任何东西)。有两个原因:一是为了ViewModel可测性,即单元测试方便进行;二是团队人员可分离开发。可总结为:更加干净的解耦。

存在的问题

MVVM公认的问题

  1. 学习成本高
  2. 绑定数据比较繁琐,容易引起crash
  3. 犹豫要不要引入RAC

问题1

任何新技术,新模式都是有学习成本,MVVM也不例外,但是并不是非常高,阅读相关文章,以及技术大咖做的demo,经过一两个需求就基本掌握了。

问题2

在不使用RAC的情况下,一般使用KVO、类KVO来解决,需要在初始时绑定,dealloc移除绑定,还要避免主动修改viewModel值造成UI的循环响应修改。的确,相对繁琐,但已经有其他开源方案可以解决,既方便,又安全。

问题3

(个人意见)如果非必需,建议不要引用RAC,有两个原因,一是因为RAC学习成本需要颠覆之前的开发方式;二是RAC对性能影响非常大,一个简单的数据响应也会生成非常多信号量。

问题不止这些

经过长时间的使用,发现MVVM的viewModel的确可以抽离V/VC中的业务逻辑,但是不同VM由于对外接口不统一,相似的业务,如果通过VM之间的继承,可以解决当时问题,但是随着迭代,判断逻辑越来越多;如果创建各自独立VM,部分能力又会重复造轮子。

例如,页面A某个功能需要,先判断登陆,再上传修改;页面B某个功能需要,先判断登陆,再查看信息。

方案一
viewModelB继承viewModelA,viewModelA声明一个nextActionAfterLoginCheck public方法。viewModelA登录判断后调用nextActionAfterLoginCheck,而viewModelB重写即可。

image.png

但后期需要页面A在登录增加封禁账号不能修改信息,那么页面A就不用调用nextActionAfterLoginCheck,但是页面B需要。这时可以再声明一个-(bool)shouldToDoNextAction 方法,在登录判断后通过这个方法获取A、B各自返回值,决定是否调用nextActionAfterLoginCheck,既保证nextActionAfterLoginCheck业务独立性(不涉及账户相关判断),又保证继承关系不变。但是登录模块代码引入了新的判断,VM臃肿化前兆现象。方案二
页面A、页面B各自拥有自己的viewModel,这样登录逻辑便重复,但是日后修改,两个页面独立,互不影响。

有没有更好的解决方案呢?

分析

对比一下前后功能
* 开始时,页面A、B所包含的功能:登录判断、上传修改、查看信息。
* 后期迭代,页面A增加:封禁用户判断。

表面上,造成VM臃肿隐患的原因,是既要保留公用登录模块的前提下,完成页面A新的业务需要,又要最小影响页面B,则需要增加判断完成。那进一步分析业务链路,发现最终的形态如下:

image.png

根本上,VM自身承担的业务定义、执行,造成了各个功能无法灵活组织,随时可变,每一个功能修改都有可能影响到其他业务。

解决方案

如果VM只承担业务定义呢?通俗一点的说法,V/VC告诉VM要做一个什么业务,VM定义这个业务需要哪几个功能,具体功能实现不在VM实现,那么VM在后期迭代中只需要更新定义即可,无非是增删改定义的几个功能而已。用这个思路分析上面case,既然不用实现功能,那么viewModelB就不用继承viewModelB,两个页面独立,viewModelA业务定义:登录判断、封禁判断、上传修改,viewModelB业务定义:登录判断、查看信息。如此分析,可以发现,每个功能都被拆分成了相对“干净”的最小功能,那么这些功能在哪里实现呢?暂且叫它业务中心(Service)吧,新MVVMS模型图如下:

image.png

技术调研

若要实现上面的方案,必须解决3个问题:

  • 如何定义
  • 结果如何返回
  • Service应该如何设计

需要一套语法解决定义

个人觉得VM最终目标只需要传入一串字符、参数,就能完成一个具体业务。那么定义了字符,就涉及到解析、翻译,借鉴编译原理,我们需要一套定义业务的语法。

将相对独立、闭环的功能,称为原子功能,每个原子功能为一个编码,相关功能为一个模块,一个模块为一个编码区。例如:账户为模块Account,则登录判断(Account01)、登录(Account02)、封禁判断(Account03);信息模块Info,上传修改(Info01)。

功能之间执行先后顺序、串行、并行、是否执行,通过操作符来决定。类似四则运算中,括号()决定执行的顺序,+、-、×、÷决定两侧的运算结果。那么可不可以定义+表示只有左侧执行成功,才执行右侧,-表示左侧执行失败再执行右侧呢?

那么按照上面的case,页面A的业务定义为:

(Account01-Account02)+Account03+Info01。

响应数据变化代替某个接口结果

通常我们是通过一个方法返回拿到结果,如果要拿到几个功能执行后的返回结果,这种方式便行不通。再加上原子功能要保持闭环,独立,不能相互依赖值传递,就更不能走传统方式。

将Model改造一下,使用监听者模式,页面需要监听哪些数据,就在初始化时注册,原子功能执行时,根据参数修改对应数据,页面实时响应数据变化即可。

Service设计

Service分为两部分:语法解析、原子功能模块注册+执行。考虑到后期模块积累过多,造成加载耗时,要支持分桶加载模块能力,例如:现在有A、B、C,3个模块,但是页面只需要调用A、C模块,那么在初始化时,可以指定自定义模块桶为:AC。

可行性分析

目前,2020年优酷相关业务建设中,有相关雏形,可以完成上述部分设计,但必须申明一点,框架还有不完善之处,后续会朝着这个方向继续探索。后续完善后,会及时更新本文档,以及对应技术文档。

好处是明显的,框架升级后,琐碎繁复的代码,再也不需要重新copy+修改,原子功能完成只需一次,但可多次使用,并且随着加入开发的同学增多,后续业务使用时,会越来越方便。

写在最后

MVVM优化后框架并不是最终解,就像文章开头讲的,本文只讨论开发模式的可能演化、优化方向;希望大家在评论区留言指正、交流。

相关文章
|
存储 并行计算 Java
Python读取.nc文件的方法与技术详解
本文介绍了Python中读取.nc(NetCDF)文件的两种方法:使用netCDF4和xarray库。netCDF4库通过`Dataset`函数打开文件,`variables`属性获取变量,再通过字典键读取数据。xarray库利用`open_dataset`打开文件,直接通过变量名访问数据。文中还涉及性能优化,如分块读取、使用Dask进行并行计算以及仅加载所需变量。注意文件路径、变量命名和数据类型,读取后记得关闭文件(netCDF4需显式关闭)。随着科学数据的增长,掌握高效处理.nc文件的技能至关重要。
2665 0
|
9月前
|
Windows
Windows硬盘扩容
如果云服务器扩容硬盘或新加盘未生效,可按以下步骤操作: 1. 新加硬盘:右键硬盘选择“联机”。 2. 扩容硬盘:进入“计算机管理”>“磁盘管理”,右键要扩展的分区,点击“扩展卷”。 3. 增加分区:右键未分配空间,选择“新建卷”。 通过这些步骤可确保硬盘变更生效。
313 0
|
API 开发者
鸿蒙next版开发:ArkTS组件通用属性(浮层)
在HarmonyOS 5.0中,ArkTS的浮层属性(overlay)允许开发者在组件上增加遮罩文本或叠加自定义组件,实现丰富的界面效果。本文详细解读了overlay属性的用法,并提供了示例代码,包括静态和动态浮层的应用。通过本文,读者可以掌握如何在UI开发中有效利用这一功能。
632 6
|
9月前
|
JavaScript API 数据安全/隐私保护
淘宝店铺订单相关API接口详解
本文详细介绍了淘宝店铺订单相关的三个关键API接口:订单列表、订单详情和订单物流。通过这些接口,开发者可以获取订单信息、买家详情、商品清单、支付信息及物流轨迹,支持多种筛选条件和复杂参数传递。此外,文章还强调了接口权限申请、数据安全处理及调用频率限制等注意事项,帮助开发者高效集成这些接口,提升电商系统的功能和用户体验。供稿者:Taobaoapi2014。 (239字符)
Electron——node_modules\ffi-napi\build\Release\ffi_bindings.node is not a valid Win32 application.
Electron——node_modules\ffi-napi\build\Release\ffi_bindings.node is not a valid Win32 application.
217 0
|
JavaScript 算法 前端开发
国标哈希算法基础:SHA1、SHA256、SHA512、MD5 和 HMAC,Python和JS实现、加盐、算法魔改
国标哈希算法基础:SHA1、SHA256、SHA512、MD5 和 HMAC,Python和JS实现、加盐、算法魔改
1577 1
|
分布式计算 Hadoop Java
Hadoop MapReduce 调优参数
对于 Hadoop v3.1.3,针对三台4核4G服务器的MapReduce调优参数包括:`mapreduce.reduce.shuffle.parallelcopies`设为10以加速Shuffle,`mapreduce.reduce.shuffle.input.buffer.percent`和`mapreduce.reduce.shuffle.merge.percent`分别设为0.8以减少磁盘IO。
274 1
|
前端开发 JavaScript Java