一 背景和问题
我个人平时会比较慎用“架构”这个词
兵无常势,水无常形。所以,我个人的观点是:以要解决的问题为出发点,去讨论我们要采用的架构模式(技术方案)。
另外,由于我们是站在很多巨人肩膀上的,讨论时可以站在一些如SOLID等软件设计/开发原则的基础上。
写这篇文章,我也是从解决一些问题的目的出发的:
- 最近和团队同学讨论了相关话题,虽然大多数同学在实践上基本一致,但具体到话术、名词概念和具体使用的理解和实践上有些差异(这是很正常的,因为业界对同一个模式的理解和实践也不同)。我结合一些实际编码场景做了一番陈述,为了避免后续重复大费口舌,所以打算写下来,以后有需要直接发文章链接。
- 由于我个人的认知和实践有限,所以也希望能抛(huan)砖(ying)引(lai)玉(pen),让我学到更多。
- 虽然同一个架构模式在不同业务/技术领域的实施会有区别,但同一个团队内应该保持一致性,因为这样有助于日常的code review、功能模块的交接backup等活动,尤其是有利于使用统一的单测建设方案来保障我们的产品质量。
- 实际问题:我最近在开发商家合并发货的功能,但由于之前基础发货功能的界面和逻辑并不是我开发的,所以我在修改原有代码、支持有非常多细节逻辑的合并发货能力时,就在担心对原有发(zhong)货(yao)能力的影响。而这时候,如果有单测的保障,我就可以更放心地进行功能升级改造了 —— 别说更复杂的合并发货能力了,而这类诉求在复杂的交易场景里很普遍。
提炼一下我遇到的具体问题:
在由不同开发人员持续迭代、进行功能升级的软件开发活动中,如何保障具有复杂逻辑的商家经营工具的产品质量。
软件开发活动是整个流程的核心环节:接收产品和视觉设计需求/变更作为输入,然后输出客户可用的终端产品。
而统一的软件开发架构模式,则是我们保障软件开发质量的基础。(这里就不具体展开WHY了)
由于讨论的是具体面向客户使用的业务场景,少不了客户操作交互的视图层(View),所以我从MVC开始谈起。
二 从表现层的MVC谈起
虽然我平时比较慎用“架构”这个词,但我平时喜欢随手拍一些建筑物。因为建筑之美,会让我联想到软件的架构也应该有美感,毕竟Software Architecture这个概念也是起源于Architecture。
这时候,架构这个词就会给我一种接地气的感觉:有多少块砖,每块砖做什么用、放到哪里去,这块砖 和 那块砖怎么黏在一起或互相支撑。当然,由于软件的可移植性、可复用性,从某些角度来讲,软件架构相比建筑架构有其更复杂的地方。
MVC诞生至今已经超过40年了(Since 1979),10多年前就得到过很广泛的讨论和实践,穿越时空到今天肯定有其反脆弱性和内在核心价值。虽然如今乍看起来好像已经过气、被讨论过千百遍了,但仍然有很多程序员会有不同理解和看法,或多或少。这是很正常的,上面也提到了部分原因,这里具体再展开下。
1 MVC在经典三层架构里的位置
MVC是一种通用架构模式
上面这三个场景的应用,都是面向客户的,需要交互表现的。
从MVC命名中的View(视图)也可以看出,MVC模式应用在软件系统架构里的表现层。
在业界某知名公司的官方文档里,也明确把MVC放在Web Presentation Patterns下。
我之所以没有在上图中对M-V-C添加箭头线条,是因为在这一点上,不同程序员也有不同理解和实践。
这是第一个需要明确的点:MVC架构模式在多层系统架构里的应用范围。
左侧 业务表现层-业务服务层-基础服务层 是移动端三层架构模式,未涉及到 C/S 交互;右侧是Web B/S场景的三层架构模式。
因为有些应用会比较简单,根本不需要业务服务或基础服务层,纯粹靠一个MVC(或者VC)就能交付出一个Mobile/Web App;
而且在一些业务系统里,Web前端/桌面客户端/移动App 也可能会被简化为 大前端/大终端表现层;
所以可能基于不同信息,不同程序员对此会有不同认知。
但随着用户终端应用的重要性和复杂度的提升,已经从简单应用发展到复杂多团队协同的平台型或航母级应用,仅靠一个MVC来完成交付是不合适的。
我们也可以反过来想,程序员会把以下代码放在客户端代码的哪一层:
2 业界基于MVC模式的不同实践
前面提到不同程序员对MVC模式的理解和实践存在差异
业界大厂亦然,以下会结合业界一些知名且有影响力的公司在MVC模式上的实践,做进一步的展开讨论。
知名公司A
知名公司A在指导开发者使用MVC时,推荐下图方式:
可以看出在他们的实践上:
- Controller可以引用View和Model。
- View可以引用Model。
- 这里的Model倾向于是Passive。
同时,他们建议:
知名公司B
说到Massive View Controller,知名公司B在移动互联 方兴未艾的时候,推荐下图所示的MVC实践方案:
上图呈现出:
- Controller引用View和Model。
- Model通过一些松耦合方式来触达Controller,如广播通知、callback等,驱动Controller做出响应。
- View通过代理模式等方案弱依赖Controller,由Controller对各种用户操作、UI渲染诉求做出响应。
- 而View和Model之间是隔离的,Model变化后对View的更新操作全部由Controller负责。
不过相应的官方文档已经被声明是过期文档了,并备注不一定是目前的最佳实践。
是的,随着移动互联 蓬勃发展,十年前的“最佳实践”被一路多种挑战 —— 在采用这种方案的开发领域中,如何重构Massive View Controller为Lighter View Controller已经成为了一个专题。
对比和思考
A和B的异同点
一些问题和思考
Martin Fowler
作为
- MVVM模式诞生时参考引用的技术专家。
Martin Fowler给的MVC模式图如下:
和上面知名公司A和B的图,又不一样了,不过他这里也是认为View可以引用Model的。
MVC和DDD
也就是说,大到MVC各个模块的依赖引用关系,细到Model中的代码设计方式,业界都有不同的理念和实践。
Java Web开发领域也对Model的设计产生过非常激烈的讨论。
小结
先抛开具体模块的代码设计方案,基于以上几种业界大厂或专家的描述,我小结了以下这张图并标注了待解问题:
问题一:如何解决MVC中Controller的膨胀臃肿问题?
要回答如何解决,需要先思考为什么膨胀。
问题二:View能否引用Model?
问题三:存在View -> Model,那么是否可以反过来存在 Model -> View?
和问题二在描述上相反但又有关联,如果对问题再进一步提问的话:
如同文章开头所说,以上问题需要结合具体场景来展开(见 实际案例结合),尽量从务虚到务实。
3 Redux-like Architecture and Framework
随着前后端分离得更彻底,终端设备性能和用户体验重要性的提升,前端领域也得到了蓬勃发展,开发方式也有了比较大的变化,MVC-like方式不再是主流:
和MVC延伸派生出的MVC Family一样,Redux提出或重新带火了数据流、状态管理等概念,开始影响其它平台领域,并诞生了一些框架。
比如ReSwift、
swift-composable-architecture,以及SwiftUI里的State and Data Flow。
虽然我也写过点React,但并没有怎么实践过Redux。
不过这些变化和影响,是我们在解决问题的过程中需要结合考虑的。
三 实际案例结合
1 常见的数据结构定义和使用
程序 = 数据结构 + 算法。
—— Nicklaus Wirth,Pascal之父,图灵奖获得者
这句话乍看起来可能会有点面向过程设计的感觉,但OOP中的对象其实也是由数据+方法组成,而FP则更不用说了。
在编码开发活动中,会存在以上3种数据结构定义和使用方式:
- 原生数据结构,比如list/array、map/dictionary、tuple等。
- 类似MyContact的数据结构定义,由服务端返回的数据进行转化,并可能根据业务逻辑按需加上一些标志位给Controller消费。
- 类似ContactViewModel这样的纯粹为视图View服务的数据结构定义。
补充:
(1)MyContact 和 ContactViewModel 只是特意区分的命名,实际上 MyContact 也可以是纯粹为视图View服务的数据结构定义。
(2)但是,合适的命名有助于帮助我们思考和编码,从表达上呈现出我们的倾向和重点。
“There are only two hard things in Computer Science: cache invalidation and naming things.”
—— Phil Karlton
2 常见的多复杂卡片的列表场景
这个场景可以部分回答问题一:为什么Controller会膨胀,以及如何解决。
其它部分答案则落在复杂页面场景的多delegate、target-action、notification-observer等视图交互响应的处理逻辑上。
我认为,之前反对View引用Model,就是导致MVC变成Massive View-Controller的一个原因。
另一个原因我认为是工具链只提供了ViewController这样的Controller模板,隐式教导开发者都在这里写代码。
这也可能是因为十几年前移动互联 还没发展起来,移动App的复杂度低,所以提供了在当时简单够用的方案。
当只能由Controller 持有-> Model的时候,那么在多复杂卡片的列表场景中,必须由Controller来更新每个View的属性/状态。
- MyViewController需要为ContactCell更新它的各种相关属性,类似的还有AddressCell、PackageCell等。
- MyViewController在更新AddressCell展示前,可能还需要先为它计算出合适的富文本展示内容。
- MyViewController需要响应不同Cell的点击交互行为,包含但不限于按钮点击、输入框变化、富文本跳转、键盘起落等。
- MyViewController需要响应CollectionView/TableView的DataSource/Delegate各种方法实现。
- MyViewController需要响应Model层的变更通知,或者是另外一个ViewController抛过来的广播通知。
- ……
然后MyViewController就爆炸了。
针对这种场景,我的解法是:
1.通过让View->Model,基于工厂模式,把组件化Cell基于数据更新的布局逻辑交给View负责,如
contactCell.configUIWithModel( contactModel )。这样有点类似上面DDD提到的充血Model,具备高内聚的特点,带来好处:
a.和减轻控制器负担、推入域模型类似,通过把数据驱动布局的代码推入组件域内,减轻了MyViewController的负担。
b.利于做这部分组件化Cell的UI测试。
c.利于这些组件化视图复用到其它场景,比如交易管理场景的订单卡片可以复用到搜索场景中,不用在SeachViewController里复制粘贴一大堆代码,只需要从Model取一个数据对象丢给组件化Cell即可。
2.基于ViewController,拆分出不同职责的扩展,比如MyViewController+Delegate专门复杂响应代理事件处理。
3.定义出其它类型的Controller,比如MyDataSourceController,专门为TableView提供数据源,可以类比参考Android中ListView的Adapter+ViewHolder。
工厂模式下,产品的刷漆、烘干、印花等操作会在内部完成,不会丢一个模型让客户去自己贴logo。MVC的每一部分都可以用不同的设计模式来组合实施,实现解耦或动态灵活的目标。
对应下图:
到这里已经回答了前面的问题一和问题二。
更多解法可以参考上面提到的相关建议,比如lighter view controllers
这里采用了 VIew -> Model 的方案,用来参与解决Massive View-Controller的问题,并且让View更容易复用和做UI测试,带来了好处。
可以结合前面提到的
“当控制器由于责任过多而变得过于复杂时,需要将业务逻辑从控制器移出并推入域模型中。”
再进一步讨论下。
我的理解和举例:
- 存在一个输入框,让用户提交物流单 。
- 用户在输入过程或者完成输入后,由View通过delegate模式路由给Controller做校验,而Controller可能还要进一步依赖Model去做更完整的校验(如 络请求到服务端,因为物流单 的规则很多而且可能动态更新)。
- 当Controller责任过多、代码膨胀、过于复杂时,就将物流单 这块业务逻辑推入 物流(单 )域模型中,即由View直接通过delegate模式交给 LogisticsModel来做校验。
- 也是 View -> Model 。
不一定对,抛(huan)砖(ying)引(lai)玉(pen)。
声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!