声明: 本文仅用于学习和交流. 请勿随意转载以用于商业用途. 文章有引用一些书籍和资料的言论, 如不慎涉及侵权, 请第一时间联系笔者, 笔者会对文章内容进行调整
前言
早期的玩物得志架构
玩物得志架构探索之旅
玩物得志流式布局的封装及使用治理
结语
架构没有好坏之分, 只有更加适合自己业务的架构模式。设计模式没有好坏之分, 只有更加适合自己业务的设计模式。
时代演进, 技术也随之发展。时至今日, APP俨然已成为绝代多数互联网企业用来获取用户的核心渠道。而作为核心渠道之一的iOS已有10年有余的历史, 10多年的时间对iOS技术圈来说足够产生相当可观的沉淀。与此同时, 伴随着公司业务量的增长,愈来愈大、愈来愈多的APP也在不断地、持续地挑战着每一个移动端研发人员的知识深度,而我们移动端技术人员也在这个不断接受挑战的过程中,成就了今天的移动互联网时代。
玩物得志移动APP就是这样一个挑战多用户量、多业务量,在接受着更多多元化用户的同时,默默地、不断地演进着移动端的架构。
关于架构的探索是永无止境的,而且应用更是五花八门,见仁见智。同时,这也是很大的一个话题, 笔者不可能用短短的一篇文章就将其描述的面面俱到。正所谓 一斑窥豹,接下来笔者就从 玩物得志iOS核心业务层的架构探索及演进_ 以及 _玩物得志iOS流式布局的封装与使用 来为大家简单介绍一下。 同时也会将在探索期间,沉淀出来的一些干货,开源出来, 供大家下载使用, 并欢迎大家踊跃提出自己的意见和建议。希望借助玩物得志平台,笔者可以为iOS的技术发展,贡献出一部分力量!
在玩物得志发展早期, 为了快速抢占市场,满足高速增长的用户的需求, 传统的 MVC 架构成为了 "短平快"思路的首选。
首先简单介绍下MVC 原本该有的样子
如下图:
在这种架构下,View 是无状态的,在 Model 变化的时候它只是简单的被 Controller 重绘;就像网页一样,点击了一个新的链接,整个网页就重新加载。尽管这种架构可以在 iOS 应用里面实现,但是由于 MVC 的三种实体被紧密耦合着,每一种实体都和其他两种有着联系,所以即便是实现了也没有什么意义。这种紧耦合还戏剧性的减少了它们被重用的可能。
显然,传统的 MVC 已经不适合当下的 iOS 开发了。
而 APPLE 理想的MVC 又是什么样子的呢?
如下图:
View 和 Model 之间是相互独立的,它们只通过 Controller 来相互联系。但是这种情况下, Controller 是重用性最差的,因为我们一般不会把冗杂的业务逻辑放在 Model 里面,那就只能放在 Controller 里了。有人甚至把 MVC 叫做重控制器模式。另外 关于 ViewController 瘦身 已经成为 iOS 开发者们热议的话题了。
如下图:
Cocoa MVC 鼓励你去写重控制器是因为 View 的整个生命周期都需要它去管理,Controller 和 View 很难做到相互独立。虽然你可以把控制器里的一些业务逻辑和数据转换的工作交给 Model,但是你再想把负担往 View 里面分摊的时候就没办法了;因为 View 的主要职责就只是讲用户的操作行为交给 Controller 去处理而已。于是 ViewController 最终就变成了所有东西的代理和数据源,甚至还负责网络请求的发起和取消。
显然,MVC 是重控制器模式。
对MVC模式的综合评价:
随着玩物得志业务量的不断增大, 用户量的持续增长, 需求不断完善和改进, 在MVC模式下, Controller 变得越来越臃肿, 其可测性和代码易用性方面越来越差。 一些原本简单的模块 正在变得越来越复杂, 维护也越来越难。核心问题总结如下:
基于上述核心问题, 架构改进已经刻不容缓。那么, 一个好的架构应该具有哪些特征呢? 在上述对MVC的评价里, 实际上已经用到了这些特征。下面我们将其总结抽象出来。
用严格的定义的角色,平衡的将职责划分给不同的实体:
当我们试图去理解事物的工作原理的时候,划分可以减轻我们的脑部压力。如果你觉得开发的越多,大脑就越能适应去处理复杂的工作。但是大脑的这种能力不是线性提高的,而且很快就会达到一个瓶颈。所以要处理复杂的事情,最好的办法还是在遵循 单一责任原则 (https://en.wikipedia.org/wiki/Single_responsibility_principle)的条件下,将它的职责划分到多个实体中去。
测试可行性通常取决于 职责划分的程度:
单元测试可以帮助测试出新功能里面的错误,帮助找出重构的一个复杂类里面的 bug。这意味着单元测试可以帮助在开发者在程序运行之就发现问题。而如果这些问题,在被发现之前就提交了用户设备上,而修复这些问题,至少需要花两天时间先通过AppStore的审核。同时还得想办法让用户升级新的版本已解决这些问题。这些成本可以说是很高了。
正所谓,最好的代码是那些从未被写出来的代码。一般来说,代码写的越少,问题就越少, 越容易被其他人所理解, 同时发现问题的成本就会越低。所以实现一个新的需求或者功能的时候, 千万不可忽略其维护成本。
基于玩物得志 app的业务特性, 以及历史原因, 在业务迭代与维护之余,笔者就架构方面做了如下尝试:
对于相对比较简单的业务模块(比如设置模块)直接沿用改进后后的MVC架构。这个架构在上面已经详细描述过, 不再赘述。原因很简单, MVC 通俗易懂, 逻辑层级较少, 在业务和交互逻辑不复杂的情况下, 这种架构模式依然是首选。
MVVM 是MV(X)系列架构里新兴的, 也是这个系列里最为出色的。ps: MVP 属于MV(X)系列里的一种, 像就像传统 MVC 架构一样, 其虽然做了一些改进, 但是依然存在不小的瑕疵。这里也不赘述。
如下图:
理论上, Model - View - ViewModel , View 和 Model已经众所周知, model作为中间人的就角色都已经比较熟悉。在这里 中间人的角色被ViewModel所置换。
ReactiveCocoa(或者同类型的框架)会让你更大限度的去理解 MVVM。
对于较为复杂的业务 比如收银台, 对公转账等的中等模块, 应用该架构模式之后:
MVVM 魅力之处在于,因为它不仅结合了上述几种框架的优点,开发者不需要为视图的更新去写额外的代码(因为在 View 上已经做了数据绑定),另外它在可测性上的表现也依然很棒。
VIPER_S架构是笔者在传统VIPER架构基础上所做的改进。
首先来看一下传统的VIPER 架构, 用这种架构就像搭建乐高积木一样,来进行开发和设计,VIPER是一种全新的设计模式, 并不属于MV(X)架构系列的一种, 如下图:
VIPER 从另一个角度对职责进行了划分,这次划分了 五层。
VIPER 模块可以只是一个页面,也可以是应用里整个的用户使用流程- 比如说登录这个功能,它可以只是一个页面,也可以是连续相关的一组页面。
相比于MV(X系列), VIPER 有以下区别:
如何正确的使用导航(doing routing)对于 iOS 应用开发来说是一个挑战,MV(X) 系列的架构完全就没有意识到(所以也不用处理)这个问题。而VIPER 突破性的将其考虑进去, 并形成一种模式。
对于高等复杂的业务模块, 比如首页,分类和搜索模块。展示多样, 交互复杂, 链路复杂。应用该架构模式之后:
VIPER 是用 代码量和代码的易用性和可读性来换取 代码的可维护性和可测试性。对于一些简单的业务模块,万万不可一股脑使用该模式。否则可能会造成 过犹不及的情况。会造成, 拿着大炮打蚊子的尴尬情况。
针对于VIPER_S 特性, 笔者总结出来一套脚手架(Template), 并将其公开在github上:
地址:https://github.com/waterwang1992/VIPER_template
在VIPER的 Interactor层, 可能会用到一些服务和管理(Services and Managers), 而这些服务和管理类或者实体由于其高复用性,在整个业务模块中又有着相当重要的地位。所以, 笔者将 S 单独拿了出来进行着重强调。并将其独立出来。
如下面简图所示:
玩物得志将 VIPER中的R 抽象成 WireFrame 进行路由的管理, Service 单独拿出来, 作为一个基础组件。Interactor 和 Presenter 以及 Interface(UIView/UIViewController)层 保留其原有的特性。
以首页为例:
众所周知, 对于任何一款app, 首页都是最受重视的模块之一, 从用户的角度来说,用户打开app, 登录注册之后, 最先映入其眼帘的便是首页, 从开发者(公司)的角度来说, 由于首页之于用户的重要性, 会更加重视首页的导流和视觉效果以及交互体验等. 总之首页的重要性不言而喻. 这里不过多赘述.
综合各方面原因, 首页的开发及维护对于开发者来讲, 便是重中之重.
笔者初来公司, 拿到首页代码, 整个人就蒙掉了. 暴力使用MVC模式, 将核心逻辑一股脑的写入controller, 其他核心文件散落"世界各地", 模块之间耦合及其严重.虽然功能是实现了, 但是却给后续维护带来了极大的困难. 本来可以以极简的代码和极少的代码实现需求迭代. 结果却问题频出, 从而浪费了大量的开发资源以及测试资源等.
一张老代码的工程截图感受一下:
使用VIPER之后, 工程文件分布如下:
众所周知, 流式布局(即我们所熟知的feed流)的使用在 移动端开发中的重要地位。 尤其是像 玩物得志 这种集 IM、社区、短视频、直播、商城于一体的大型电商平台 更是如此。
关于流式布局用的最为频繁的系统组件 便是系统 UIKit 框架下的 UIColectionView 组件。几乎90%以上的流式布局都是基于此。它是实现 移动端 诸如首页、分类、搜索、商品详情等核心或者其他边缘页面的流式布局的基础。因此, 拥有一个复用性高、可定制程度高、维护性便捷的流式布局组件便显得尤为重要。
遗憾的是, 由于公司成立初期, 开发者将更多的精力放在了新功能的完成和新业务的开发上, 而忽略了对一些基本业务组件的封装、复用及使用。其中流式布局的管理便是该类型问题中最突出的之一。
如下图所示:
几乎所有的流式布局的使用都是使用最原始的模式。除了Cell/View层级的复用之外, 其他几乎无封装和复用可言。导致每新增一个模块, 都要在业务层的Controller里实现大量的重复代码, 去处理各自独立的feed流的展示及交互逻辑。导致无意义的重复代码散落在世界各地。毫无维护性可言。比如feed流功能实现之后,后续维护过程中, 想要在所有页面增加一种feed流卡片的样式并且定制其交互。那么问题就来了, 开发者需要在所有的业务模块的Controller里 添加各自独立的代码去实现, 而这代码的核心逻辑几乎都是一模一样, 导致维护成本成几何倍数增加。
改进后的核心代码, 笔者同样并将其公开在github上:
地址:https://github.com/waterwang1992/CollectionManager
基于上述的痛点, 笔者便在开发之余研究所有feed流的共性以及特点,致力于feed流管理类的抽象和封装。最终,笔者结合流式布局在玩物得志中的使用, 将所有流式布局抽象成一个管理类。
DZCollectionManager 对应VIPER_S 架构中的 Service
其大致核心实现逻辑如下图所示:
将所有feed流中的元素,抽象成对应的几种Item, 外部调用只需要结合业务逻辑, 关注如何生成对应的Item 即可。 其他逻辑, 调用者无需再关心。
完美解决流式布局在玩物得志应用上的痛点。大大提高流式布局的复用性, 稳定性以及维护性。
牛年邀牛人
一起战斗、一起成长
技术、产品、UED、运营、职能等海量岗位
玩物得志期待你的加入
VIPER脚手架:https://github.com/waterwang1992/VIPER_template
流式布局管理: https://github.com/waterwang1992/CollectionManager
《GoF设计模式》:http://ccftp.scu.edu.cn/Download/038c70a8-c724-4b86-91c3-74b927b1d492.pdf
《Refactoring: Ruby Edition》 :https://martinfowler.com/books/refactoring.html
《重构列表》:https://refactoring.com/catalog/
《ios architecture patterns》:https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52#.1eqt5p75r