跳转至

04 架构演进:架构是如何跟随业务演进的?

你好,我是黄俊彬。上节课,我带你学习了提高遗留系统代码可测试性的方法,今天我们就正式进入到专栏的分析设计篇。

其实说到分析设计,这里有两个问题是我们必须要先思考清楚的。第一个问题是“从哪里来”,只有清楚了解目前架构设计的问题,才能更好地设计解决问题的方案。否则,我们怎么能保证新的设计可以解决问题呢?

第二个问题是“到哪里去”,很多时候我们都在谈重构和架构改造,却经常忽略我们到底要将原有的系统重构成什么样子。当然,这一步我们或许不用一点一滴从头开始,参考业内成熟的方案也许是更加可靠高效的方式。

所以这节课我会通过一个产品的架构演进,带你感受一下架构是如何跟随着业务来演进的。通过这节课的内容,你可以更清楚地了解遗留系统的问题和演进方向,这样在做分析设计时,你的目标会更加清晰,设计也会更加从容。

Sharing1.0:单体架构

小菜作为架构师,刚加入到一个新的产品团队Sharing,Sharing目前采用的架构是单体架构。在项目初期,团队人员少,业务也相对简单,这种架构很好地支撑了业务的迭代。后面是Sharing1.0的架构图。

单体架构是常见的架构模式之一。通常所有开发人员基于单个模块进行开发,所有业务功能都集成在一起打包发布。单体架构非常适合团队规模小、业务复杂度低的产品,在项目起始阶段能快速迭代进行验证。

但是近几年随着业务的发展,Sharing团队的成员也急剧扩展,另外业务的复杂度也不断提高。产品经常因为Bug问题导致上线延期,团队最后为了交差也只能硬着头皮发布。可想而知,最终的线上质量也堪忧,经常收到用户的投诉。

在深入地了解了Sharing的业务及团队问题后,小菜组织了一场内部的架构回顾会,总结了Sharing项目两个比较关键的问题。

1. 代码质量差

因为Sharing采用的是单体架构并且缺少相应的架构约束,随着业务的持续演进,代码不断地膨胀和腐坏,所以代码内部的耦合度很高。在这样的基础上修改代码,非常容易牵一发而动全身:修改一个Bug,又引起另外一个Bug;开发一个功能,又引起另外一个功能的异常。

例如,小菜接到一个新的需求,其中获取用户登录信息协议中的一些字段产生了变化,需要进行调整。于是他更新了登录模块中的获取个人信息逻辑和本地的缓存数据,但因为用户表也被其他模块使用了,所以这次的修改就导致了另外一个模块的数据读取异常,页面上无法正常显示数据。

另外,由于耦合度高,开发人员在扩展新功能的时候会大量采用复制粘贴的方式,导致项目中存在大量的重复代码,当有需求变化时很容易产生散弹式的修改。

比如说项目中原本有一个查看文件列表的功能,但产品需要扩展一个搜索文件列表的界面。本来基础的文件列表是可以复用的,但因为不好扩展,开发人员就通过复制多一个类来实现新功能。等再接到新需求“文件列表增加大小的展示”,由于之前复制了多个类,就必须要在几个类面同时进行相同的修改。

2. 开发效率低

在开发阶段,开发人员需要对代码编译打包,然后安装到设备上进行调试。但由于代码量大,编译打包甚至会超过10分钟,效率非常低,而且这样的过程一天可能要重复几十次,也就是10分钟乘以N次的低价值工作。

由于目前Sharing团队的规模急剧扩张,加之代码中有很多耦合问题,代码合并冲突就成了开发中的常事。开发人员需要花费大量的时间去解决,代码合入效率非常低。

此外,对于测试人员来说,每次版本迭代都只能在最后的集成阶段进行大量的手工验证,没办法对某个模块或功能做独立的功能验证,因而测试效率也非常低。

Sharing2.0:组件化架构

于是,架构师团队对Sharing架构重新进行了设计,这次他们采用的是组件化架构。

组件化架构采用多个模块开发,将业务和基础功能拆分成多个独立的组件。这样一来,开发人员可以基于不同的模块进行独立开发和编译调试,最后发布时再集成所有组件。而且测试人员可以提前对组件进行功能验收,更快推进测试,避免将所有的测试工作都堆积在最后的集成阶段。

但需要注意组件化后,复杂度会转移到基础设施之上。这时候我们需要引入路由、注入等框架解决组件依赖及通信,并且还需要引入自动化流水线来降低组件集成的复杂度。

总之通过新的设计,Sharing拆分了多个独立的组件。由于组件都是采用二进制的形式集成,依赖上都是采用接口注入的形式,避免了直接的依赖,这也有效地减少了修改代码引起的新问题以及代码合并冲突的问题。

Sharing3.0:插件化架构

随着Sharing2.0组件化的改造,这种架构很好地支持了团队很长时间的版本迭代。但随着业务的发展,产品需要增加一个特性,那就是部分的功能需要让用户能够动态地升级和卸载。

经过架构团队的评估,因为有了2.0组件化的基础,目前的组件都是相对独立的,所以只需要基于当前的架构引入组件的动态管理机制则可,于是团队设计了新的插件化架构,后面是Sharing3.0的架构图。

插件化架构通常是基于组件化架构演化来的,目的就是支持组件的动态加载和升级。这种架构适用于业务复杂度高且需要独立迭代、快速触达用户的产品,它一方面可以有效减少应用的体积,另一方面可以让业务插件进行独立的规划演进。

但需要注意的是,目前大部分的开源框架都在采用hook系统API的方式进行插件加载,可能会出现版本兼容性和性能的问题。另外,插件化架构还需要额外引入插件的管理平台,支持插件的版本管理、动态下载等机制。这相比组件的集成管理,复杂度更大。

下面我们以Google官方的Play Feature Delivery为例,它的整个工程结构如下。

Play Feature Delivery使用了 App Bundle的多种高级功能,支持配置安装时分发、按条件分发、按需分发等多种插件更新方式。Android Studio也支持对整个工程的管理以及编译调试功能,不过唯一的缺点是需要使用Google Play Core的库来进管理升级。更多Play Feature Delivery的使用及原理,你如果感兴趣可以参考官网的文档介绍

Sharing4.0:容器化架构

随着市场的日益竞争,团队决定让Sharing能够跨平台支持更多的系统,从而满足更多用户的使用场景。为了提高开发的效率,同时控制好投入成本,架构师团队进行了容器化架构的设计,Sharing4.0架构图如下。

移动互联时代下,很多产品都需要支持多个平台,如Android、iOS等。为了减少跨平台的开发和维护成本,容器化跨平台技术成了目前主流的架构形式之一。在移动应用领域,常见的跨平台技术有H5、ReactNative和Flutter,这些技术都支持编写统一的插件,然后通过容器技术让插件在不同的平台下运行。

虽然跨平台技术能方便我们复用组件,减少了开发工作,但跨平台的性能和体验通常比原生的要差,并且需要开发人员掌握特定跨平台技术的框架及语言,有一定的学习门槛。当涉及到对本地的硬件能力的访问时,同样也需要使用原生的代码来开发。

我们以Flutter为例,一起来看看它是如何进行跨平台设计的。Flutter重写了一整套包括底层渲染逻辑和上层开发语言的完整解决方案,不仅可以保证视图渲染在Android和iOS上的高度一致性,在代码执行效率和渲染性能上也可以媲美原生App的体验。Flutter的整体框架设计如下图所示。

对照框架示意图,我们分别看一下Flutter划分了哪三层。

  • Framework:用 Dart 语言实现的UI SDK,从上到下包括了基础组件库、图形绘制、手势识别、动画等功能。
  • Engine:用C/C++语言实现 FLutter引擎,主要包括渲染引擎、文字排版、事件处理、Dart 运行时等功能。
  • Embedder:操作系统适配层,负责实现渲染 Surface 设置、线程设置等功能。

那么原生的平台是如何通过容器来集成Flutter的模块呢?Flutter为各个平台都提供了统一的FlutterEngine API,包含了上述Flutter运行需要的所用框架和环境。换句话说,这套框架环境就是我们所说的容器。

基于这个容器,我们可以非常方便地将部分业务作为独立的模块用Flutter来开发,开发完成后这个模块就可以直接运行在不同的平台上。更多关于Flutter的内容,你可以参考官网的文档介绍

总结

从Sharing项目早期的问题来看,遗留系统带来的问题不仅仅降低了研发过程中的效率和质量,最终还导致产品的响应力和表现力都很差。由于代码耦合严重,无法拆分独立的组件。这样导致所有的需求需要统一规划发布, 灵活性差。另外,由于缺少足够的自动化测试覆盖,往往只能在最后的交付阶段进行人工测试,版本发布周期非常长。

软件的架构就好比一座房子的地基,决定着这座房子能盖多少层楼。同样地,对于遗留系统来说,架构决定着软件产品能走多快、多远。要想解决遗留系统中一系列的问题,架构改造是关键的举措。

通过Sharing的例子我们可以看出,在实际的项目中需要结合团队的规模、产品的发展阶段及定位进行合适的架构选型。随着业务及团队的发展,更合理的架构设计是能持续地演进,而不是一成不变。下表列举了几种架构的的优缺点,你可以对比参考一下各种架构之间的优缺点。

正如Google在Play Feature Delivery概览中说到:在进行插件化之前,首先需要将这些功能从基础应用中分离到功能模块中。所以整个组件化架构的改造是后续插件化、容器化演进的基础,这也是我们专栏的重点内容。下节课我们将通过一个案例,带你一起进行架构分析和架构设计,迈出遗留系统重构的第一步,敬请期待。

思考题

感谢你学完了今天的内容,今天的思考题是这样的:你现在的项目架构有没有什么坑?你是如何来解决的呢?

欢迎你在留言区与我交流讨论,也欢迎你把它分享给你的同事或朋友,我们一起来高效、高质量交付软件!

精选留言(5)
  • Geek_90e087 👍(3) 💬(1)

    我已经迫不及待了,快更

    2023-02-17

  • Geek_2b5326 👍(1) 💬(1)

    老师,请问我们常用的mvc mvp mvvm等架构,跟您说的单体,组件化,插件化有什么区别和联系?

    2023-03-10

  • xx鼠 👍(0) 💬(1)

    对于出海app,google不允许热更dex、so库,所以插件化热更、flutter热更都玩不转啊

    2023-09-18

  • 刘军 👍(0) 💬(2)

    什么体量的App适合容器化?

    2023-02-20

  • peter 👍(0) 💬(0)

    请教老师几个问题: Q1:复杂APP有多少人开发?按什么分配任务? 我只做过比较简单的APP,都是自己一个人完成的。老师您经历过的复杂一点的APP,会有多少开发人员?会超过五个人吗?如果是多人开发,按什么分配任务?按功能或者业务吗? Q2:组件具体是什么? 组件是功能模块吗?好像不太可能。组件是一个独立的软件吗? 比如是一个可执行文件或是库。更具体地讲,用AS开发时,组件是一个独立的module吗? Q3:实例APP是组件化还是插件化架构? 本专栏采用的示例APP是什么架构? Q4:容器化与特定语言有关吗? 从文中的介绍来看,容器化是由特定语言实现的,比如flutter。安卓、iOS自身都不支持容器化。这样理解对吗?

    2023-02-17