本文整理自百度技术专家、推荐产品研发部架构师李哲浩在 2022 年 8 月 ArchSummit 全球架构师峰会北京站的演讲分享,主题为“百度 App Feed 流业务架构变迁思考和升级实践”。
本次分享主要从五个部分展开:第一部分介绍我们过去架构的概况,第二部分介绍在经历一些环境的变化后,架构上有哪些改变,第三部分介绍我们团队对于架构设计的一些思想和原则,第四部分例举几个架构场景的实例,最后介绍架构维护控制相关的内容。
过去的架构漫步
我们过去一开始的架构是一个个小作坊,比方最开始的时候有一个频道页叫频道 A,然后假设我们突然想做一个进攻的项目,比如想去创建一个垂类频道 B,这时候我们就会把整套代码拷贝过去,因为我们过去采用的是 MVC 架构,如图所示,整个架构都拷贝了一份,架构只剩下了概念,没有实体架子支撑,由于这时往往都是一种“征战”的状态,时间比较紧迫,当时的用例也不多,迭代的深度也不够,觉得还好,可以实现快速开发,基本够用。
后来我们出现了人手不够的情况,PM 对发版有了更高的要求,不再满足于随版,而希望去“非随”,或者说双端变成单端想加快迭代效率,这种背景下,我们很多频道已经开始采用类似 RN 的方案,或者基于 Hybrid 上 H5 的方案去创建。
再这之后,中台的概念在各大公司飞起,大家一块见证,我们也不例外,从公司层面做中台化的架构调整和延伸,我们的业务也搭建中台组件平台。由于之前并没有梳理和建设好合理的耦合边界,各种似同非同的架构,想要响应公司号召,短期的输出方式,我们基本上只能选择整体输出(文档也没法看),也就是说,某个 APP 要创新孵化的时候,我们会把整套组件打包成一个大的组件(或者说 SDK)提供给用户,他们也想快速上线也很急,那就让他们基于原码去定制改造并使用,就是真实的共建仍未产生。
我们在实践后期发现,当在频道 A 里做了一些实验,证明了某个行为有正向的收益后,频道 B 也想用怎么办?如果从频道 A 里把这个能力复制到频道 B,当这种操作多了以后,就会出现很多捉襟见肘的情况,系统会变得比较脆弱,当代码一直在拷贝复用时,很快会变得臃肿,但一直都没有去做大的重构或者架构升级,所以遗留代码、技术债也就堆积如山。以上是过去架构的状况。但不是说拷贝复用一定是坏的,咱后面再述。
大环境的冲击
后面大环境发生了变化,首先肯定是公司内部的组织变化带来的老问题加剧,新问题产生,然后是业界技术和架构上的成果对我们视野上的变化。后来我就潜心研究了一下行业内先进的架构模式,看是否能帮我们解决存在的问题。不过这部分我们放在第三部分重点详述。还是先介绍下现在内部组织结构的情况。下图是 19 年架构升级时的组织结构。仅作为示意。
我们是中间部分的 Feed 团队,中间部分并立的还有一个搜索团队。
上游有两个团队依赖于我们这两个团队,比如说商业团队和其它产品线的一些团队会依赖到我们的 Feed,比如,我们上游的产品线期望能有快速孵化 app 工厂的能力,同时又对包体积比较敏感,希望 Feed 这边可以能找到组件动态裁减和拼装,在这种情况下,我们需要把粒度做的更细。商业这边是独立的团队,他们有他们的关注点和任务,Feed 迭代又快,不清楚 Feed 内部代码的很多细节,直接改容易出问题,但又有很大的概率要织入到 Feed 代码来实现一些效果,所以非常痛苦。这就要求 Feed 这边将各种时机做成稳定的回调接口,或者挖个空让他们填下。
另一方面,可能每个公司都会搞一些类似 RN 的定制方案,我们的叫 Talos,现在已经完全跟原来的 RN 完全不一样了,但一开始起步就是这样开始的,像美团有一个 MRN,阿里有 Cube,大家都做差不多类似的定制研究,并作出一定的创新。再比如我们也研究 Hybrid,他们可能也在研究 Hybrid,我们有一个 OEM Mapping 的方案叫 Crius,他们可能也有一个类似的方案,我们也会调研现在比较前沿的 Flutter、KMM、Jetpack Compose 这些东西,去关注并持续落地到某个场景中去。所以慢慢地,我们就有了自己的组件化、动态化、插件化这些一整套框架,这些大部分是我们依赖的下游的基础平台提供的,比如说他们现在就在做 DevOps 大平台,当然也有业务团队下沉的基础设施。
那作为业务团队,我们没有必要重复造轮子,只需要运用这些基础设施的能力来助力业务的快速发展。
另一个问题,我们现在整个 Feed 团队有几十上百号人,怎么合理分工,另外测试人员越来越少,整个研发流程主要依赖手工测试,这样每次发版迭代的时候就会突显出一些问题,比如会阻塞整体的开发效率,但是有的人却仍处在空闲。不仅如此,团队间的责任边界意识会越来越清晰,经常扯皮决定哪方去改,但是代码上的边界可不一定是这样清晰,经常你中有我,我中有你。
所以,我们想来一次业务架构的升级换代。去拥抱新的变化。
采用的架构思想和原则
架构影响因素
我们在架构升级的时候,考虑了哪些因素呢。首先围绕业务价值是必须的。否则所有的工作都失去了开展的必要。
组织架构:组织结构决定了整个边界,比如说我们一开始有 Feed 团队和视频团队,原来是一个团队,但后来被分开了,分开以后就会有一些边界划分,或者说两个库之间会脱的越来越开,这个时候应该做出合理的共建和隔离权衡。
开发方式:比如我们是不是在做敏捷开发模式,一个真正敏捷的团队,它的日常重构是比较容易实现的,比如可能他们不仅有完备的测试覆盖,团队成员每个人也有非常好的代码洁癖和设计追求。
但有的敏捷可能是伪敏捷,比如是领导人说了算的一个敏捷,比如他完全无视利特尔法则,不去关心在制品的消化,或者人月神话,而是以人力是否空闲,甚至以团队成员的工作时长作为指标,那架构优化将变得没有意义。所以开发模式和开发流程量化到什么程度,是以可定义级别还是以可量化级别的流程为标准,都是会影响到整个架构的设计导向的。
团队的成熟度:成熟的团队更加能接受解耦,模块化的价值,很多不成熟的团队倾向于在一个文件中维护一整个业务模块。这个后面再展开。
环境行业特征:我们整个 Feed 流是以广告展现的收入为导向,我们会更关注如 DAU、 时长等指标,为了指标的增长,某些方面会变得越来越复杂,比如实验越来越多,再比如我们的内容是不会重复展现的,但另一方面可能会变得很轻量级。不同 APP 和行业可能很不一样,比如说如果主营外卖业务,能想象商品列表不会重复展现吗?而且他们更注重从下单到用户收货的整个流程,但我们更注重列表的刷新策略,推荐策略。
技术约束:这个在另一个领域叫人的视野受限于生产力水平,比如 ArkUI 之类的成为主力,和没有产生这样的技术,对展示层的开发模式会有很大的不同,这也是影响架构的因素。
干系人的构成:有些事情驱动力来源于团队成员,比如他们表示某某某非常难用,再比如,PM 和领导会上重要的干系人,我们要让 PM 认同你的架构,让你的领导认同你的架构,但很可能他们不同等技术,那我们需要积攒一些原始的数据,用可以量化的指标来做事。把量化纳入到架构设计的开始处,以终为始。
投入成本比
说到量化,我们展开聊聊投入成本比,比如我们的节奏大概是这样的,一个需求发布了,我是做这个需求的人,今天周一,我评估大概 4.5 天差不多能完成,但是我会给自己留一些 Buffer,所以名义上可能将此任务估成六天工作量,但是可能下周二就需要上线,这样 QA 说他们需要一天的人力测试,这种情况下,我只有本周 5 天的时间可以安排了,倒排以后我只能按 5 天来排期,言外之意我预计要自己加班一天,实际开发的时候我发现 4 天就搞定了,搞定了以后我并没有闲着,因为这之间,虽然出了线上问题,我还在做上一个版本的 bugfix。
这一周下来后,测试花了一天的时间,当然大量的时间可能是在借调人力,因为 QA 人力也不足,不过整体下来平台上统计的可能是名义粗估和排期,但真正的实际粗估以及实际真正所用天数可能是无法体现的,那么这到底怎么去衡量?如果无法衡量,那比如说我们的架构,或者说我们的某个设计得到了一些改进之后,它的效率有没有提高?维护性有没有变好?怎么确定?(当然事实上无法准确量化也是共识)
我们希望重塑原始数据的积累情况,比如说刚刚那个例子,假设说需要一个人投入一个 Q,但是如果领导和 PM 感知不到效率的提升,就不会投入资源,如果没有这些干系人的持续投资,很多上马的研发项目可能也会停摆,造成更大的岔口。而我们的平台统计恰恰显示没有提升。而有些事不做技术债会越来越多,直到系统性风险。
可是由于没有采用故事点或者理想人天等形式,而是直接采用了排期这种手段,总不应该去怪团队成员怎么不把真实的估计填上去。那是因为他们也无法准确的估计工作量,而且也要适应请假、应对突发情况,无尽的会议。如果没有 buffer,这个压力强度就太高了。
或者让团队成员去列下粗估的构成,让其他人都看看是否合理,但是考核什么就会得到什么,这种检验不信任已经造成了不信任,不仅无法揪出不合理,而且会造成士气低下。
也可能以概率替代全局,比如某个任务原来需要 20 天,我们做了某种改进,然后再执行相似任务只花了 4 天。然后把这个收益摆上去要投资。完全不谈命中,这让人信服吗?任何的改进都有概率会命中失败,比如某个端能力缺失,开发这个端能力需要 8 天,那优化收益就变成 12 天。仅把最佳情况列上去是有问题的。更何况,有的情况是,我们做了改进,一年都用不上。
在第五部分维护控制阶段,我再谈谈我们有限解法。
团队成熟度
在团队成熟度方面,以我这几年在多个团队的经历和变迁。我发现,一开始新业务会处在原型阶段,可能会有很多需求迭代,这时通过继承也好,或者拷贝也好,怎么快怎么做。
在后面的阶段,业务会发生扩张,遍地都是“黄金”,大家就开始做一些通用化的建设,但这个时候在一个比较大的范围内,还是没有人管的,还没有非常好的治理手段,导致大家会有重复造轮子的现象,或者说核心的部分,大家都不想让别的团队来掺和,于是处于一种野蛮的生长状态,大家都会去争抢业务边界。
再后面大家可能会意识到内耗,去搞这么多框架这么多组件来占用人力维护是一笔不小的成本,而且从上而下,可能架构师会去做一些合并去重的事情,这时候的节奏就是争抢标准,争抢输出,比如说我输出多少个组件,你输出了多少到哪,这样我的组件或者框架可能变得安全一些(不会被合并),这时候大家的想法是你造一个汽车,我就造一个航母,我这个能力更强,各方面会更好。此时此刻,对 ROI 尚且宽容。
到最后就会进入到一个萎缩阶段,因为我们可能从一个业务线,或者说一个进攻的重点,迁移到另外一个重点,原来那个重点上只会处在维护的阶段,这时候各方面都已经成熟落地了,再做一点点改进的时候都要考虑 ROI,当真的有较好收益时才会被允许去做一些重构或者升级。
业务复杂度
我们团队也经历了从 MVC 这种小作坊的模式到后面能驾驭更大复杂度设计的转变。我们一直在说复杂度,什么是业务复杂度?
比如说我们在做一个转换器的插件,将 RGBA 的颜色数组转换成 YV12 格式,可能技术很复杂,可能会用汇编,可能会用 C++,可能会用 Erlang 运作多处理器,或者是通过 OpenCL 充分利用 GPU 的能力,但它的业务复杂性却很低,因为我们很明确知道它就是要做这个单一的事情。可以类比问题域的复杂度很低,但解决方案空间的复杂度很高(为了追求某种质量属性,这里是性能)。
再比如我们在有些情况下做一个简单的弹窗,这种弹窗只是调用系统 API 就可以了,技术复杂度很低,工作也就是拼接一个字符串,也非常简单,但由于要考虑上百种不同的情况,或者不同情况组合之间有一些互斥,在有些协调和相互作用的情况下,文案都不一样,这种情况下业务复杂度会变得很高。也就是说我们分支很多,熵会很大。
很多业务在经过深入迭代以后,要实现一点微小的数据的提升都要做很多实验,很多微小路径的改变都会导致整个业务复杂度增加,它的分支会越来越多,这难以避免。我们称这为“业务雪球”。
再比如,文件写入也可能有很多异常要处理,但文件处理本身是你要专注的业务吗?如果我们本身是写一个文件处理软件,那可能就是,如果对于这个产品来说,你告诉我是否成功就可以了,至于有多少种错误码我不关心,我只 try catch 一下就可以了,也就是说,它是不是你的核心业务是一方面。
团队成熟度和业务复杂度
说到团队成熟度和业务复杂度,如果我们用平面切割思维,它可以组合成很多方式:
比如说是简单的业务,或者说复杂的业务。当然这里的边界不做量化的定义。度量衡交给您。就我们基础体验小组,大家都能认可刷新业务是复杂的,首先出现问题影响极大,所以开发者强度高,压力大,而且没有单测覆盖,也不会轻易去做改动,业务逻辑分支,几屏幕的思维导图放不下。没有人能不看源码枚举出所有 case。
横轴是我列的三种人设,管理学上有经济人、社会人、复杂人假设。你可能会想,这人怎么这样,带有色眼镜看别人,是不是心理太阴暗?这倒不是说对某个具体的人做出假设或影射。而是想简化架构服务人群,进而挑选更合适的质量属性和选型决策。而且一个人在不同的阶段,面对不同的项目,可能表现为某种人设成分多一些,即一个真实的人是复杂的,可能同时映射到多种假设。事实上两边的人设都是理想态极值。你可以类比星座。我们稍微展开下。
第一种是自由人,有一些人不太服从团队的指挥,他们非常愿意去钻研,或者说实践一些新的东西,也可能就是单纯喜欢自由的空气,他们普遍非常聪明,但他们普遍也看不上工程化的价值,觉得是束缚,对于大部分的设计,他们认为是华而不实,花里胡哨,也可能不会让别人污染侵犯自己的代码“领地”。在我工作的这十多年,不同范围内的不同团队里,总能看到这一类人,这种情况下,如果是简单业务就放任他,能有个人做就不错了,而且你要相信,这类中的大部分的人 bug 率一直都是很低的。如果是复杂业务,这种人多了很容易失败,我们很多局部的架构已经因为这种情况而失败过了。表现为离开他,很难有人接起来,除非重写。
第二种是工具人,工具人基本上就是处于服从的状态,你让我怎么搞就怎么搞,我们有一套规则,我就按照这个规则来做就好了,我不管它复杂还是不复杂,繁琐不繁琐,这种情况下,如果是简单业务,我们给他打造一块样板间就可以。如果是比较复杂的业务,最好是前期时做一些稍微的、适当的过度设计,但这个度还是要把握好,不是做太大的过度设计,不然维护成本会激增,但肯定要稍微过度设计一下,因为他只会拷贝你的代码或思路来叠加他们的逻辑。
上百号人的团队,有时候很多东西真的是管不过来的,哪怕培养了一批中间力量来承上启下。有很多的 CR 情况没办法感知到,当真正感知到的时候已经晚了。比如说我们原来的 Feed 流它有一些卡片,这些卡片有一百多张,当时解析数据的 Model 用的是安卓的一个框架,发现这个框架可能有一些问题,即便是有一些问题,但没有人去关心,很多人就开始迭代这些代码,到最后出现问题,你会发现根本就改不过来了,现在的话,由于 ROI 不高也不会去改它,但是每个新人入职看到这个都会嗤之以鼻一下。
最后一种是敏捷人,这是一个比较理想的状态,敏捷人和自由人有什么相同点?我觉得他们的特点都是不去遵守规则,而是坚持他们认为正确的原则和设计理念。比如我们原来的规矩是每个人 cr 数都要达到多少多少。你知道的,古德哈特定律,考核什么就会得到什么。对于这两种人,他们都会反对盲目的 cr 数考核,但是自由人的原则往往 cr 没有用,阻碍他们写代码。而敏捷人更能认识到“有 cr 比没有 cr 好,没有 cr 比有 cr 好”的这种观点,进而聚焦到“提升质量”这件事本身。这意味着敏捷人不止于打破规则,而且要进行推广并影响更多的人,使新的实践成为新的规则。而自由人让你看到的是多套不同的凌乱的东西。
我们针对简单业务做简单设计,简单设计是 Kent Beck 提出来的,就要遵守四个原则,即通过测试、消除重复,最小元素表达和更少代码元素,这样就会得到一个相对可以的设计,因为我们大多数人不是设计大师,不是搞设计艺术的人,能得到一个平庸的,大家都认可的,可以量化的设计即可。
如果是复杂业务,我们更多的还是看看能不能建立领域模型,Martin Fowler 的企业应用架构模式里提到了像事务脚本、领域模型、表驱动等概念,因为我们某些领域比较复杂,在这种情况下需要知道如何梳理我们的领域,领域模型可能是一个前进的方向,它会对整个业务的梳理起到一些作用。
什么应该是模型
那什么是领域模型?什么是业务模型?业务模型一般认为是状态(包括场景或环境、数据表示)、策略(包括错误处理)和操作的应激集合。
我先说下模型,对于一个简单的 MVC 架构,那 M 就是一个模型,我工作的这十多年里,我发现大多数人会把 Server 下发的数据完全当成成一个模型。
我举个例子,比如说 is_vip 这个字段,is_vip 下发了 1,就会有对应的一个字段是 is_vip 为 1 这样的字段,但这种在我们看来只是一个 DTO,即一个数据传输对象。
还有另外一种是把数据库里面的字段作为一个真正的模型的一部分,比如说安卓经常推广一些框架,比如 Room 框架,你如果用 Room 框架会发现,框架会让类里面每一列是一个字段,在这样的情况下,很多时候列名和业务模型就不一致性了,比方说我们现在这个 case 里边只存了两列,第二列是一个 Json,我们要在这个字段里面定义一个 String 这样一个字段吗?可能不是。也有可能会把这个展现模型作为一个模型,如果是 VIP,就要标个红色,如果不是就标黑色,红和黑可能会是一种展现特有的特点,把这个也会作为一个模型,但这些可能都不是业务模型。
我们希望的业务模型是能够和 PM 沟通的时候,能够作为一个概念去说的,比如我们希望如果是 VIP 就要做解锁,做刷新,这种情况下,这几个东西 PM 能听的懂,业务逻辑里也能够写明,我们把这个规则表达好,具体这个规则是怎么实现的,就不是那么重要了。我们只需要知道要存一个数据,具体是怎么存的,不是业务需要关注的东西,“我们要去存它”这件事本身以及“什么时机”可能更重要。
我们设想一个简单业务,随着它变得复杂,关于模型边界这块的问题,我们在做什么:
首先,我们在协调存储模型和业务模型不一致的矛盾。比如当对一个列表进行增删改成时,不太容易直观将这些反映到存储设施上,除非全删全存。再比如当对性能有较高要求时,数据库的列和模型的字段往往对应不上,因为可能需要预读。
其次,我们在协调下发模型和业务模型不一致的矛盾。比如 server 下发了三批数据,但是 server 可能是无状态的,不太容易知道给某个设备下发了这三批数据,并根据展现上报情况做展现去重,即便知道可能也有延迟,但是端有此上下文。再比如业务模型需要一个布尔概念,但是 server 下发了数值或字符串概念。
再次,我们在协调展现模型和业务模型不一致的矛盾。比如业务模型可能表现为多个布尔求值,但是展现可能归结合并为某个控件的显隐或者字体粗细。
比如可复用视图的控件(RecyclerView)可能要求滑动窗口模型(去适配它的 Adapter)。再比如展现模型是按照窗口页面的粒度组织,但是业务模型可能要求连贯的跨越这种粒度。
然后呢,我们也可能会去协调对内业务模型和对外业务模型不一致的矛盾。
比如对外提供了一套 API 和能力集,需求变化导致内部已经使用了新方案,但仍需要兼容使旧方案工作。比如非完整复用带来一定的扩展定制诉求,要求在你的业务模型上做一些埋点和钩子供外部定制,以匹配外部方的业务模型,而这些埋点和钩子不存在于你的真实业务流程。
我们也可能会去协调不同业务模型间需要转换翻译的矛盾。比如 SDK 基于自己的模型提供了几个必要通知,但是你的模型对此无明显感知,需要做同样的处理。
再比如不同的领域团队间的代码基可能是松散耦合的、并行开发的,关注点也不同,使用的数据结构也可能不同。
最后,我们极可能也在协调技术模型和业务模型的矛盾。这体现在协调性能要求对业务模型泄露的影响。比如业务模型设计为以页面为上下文,即页面创建时生成数据,销毁时丢弃数据。但性能原因可能在冷起阶段进行提前处理、预处理部分逻辑,这要求页面创建前生成数据。这也体现在人的理解和计算机的矛盾。比如你的意图是交换两个值,但是你需要定义第三个变量来存储临时值。
Martin Fowler 将业务模型设计拆解为概念模型、契约模型、实现模型。你的意图,即交换两个值,这是概念模型,是领域语言,可以与其他角色共享;然后你设计了一个表明意图接口层 API 叫作交换,这是契约模型了,因为此时可以与模型的其他组成部分产生关联,与多个模型产生协调,这里已经很好的隐藏了技术信息;最后我们通过临时变量存储的方案完成了交换功能,这属于实现模型。
在这类问题上,我们倾向于在模型驱动开发。假设我们做一个弹窗,可能有很多不同的规则,可能有的是几天弹一次,有的是每次冷起弹一次,对于很多端上同学,将采用 UI 驱动开发的方式,这势必在 UI 里或者从 UI 导到控制器里边去做 if-else 判断。但是如果说模型驱动,那么首先它会去写个类来描述这个规则,弹窗可以简单的用 show 接口方法代替,弹什么此时不重要,把这个 UI 细节往后推,推到最后才去考虑这个事情。如果是这样驱动,那么 TDD 测试驱动开发是可能的,容易的。让一个程序员把所有的逻辑跑通再事后去写单测是乏味的,因为他知道结果是啥,所以写单测也是草草找个场景敷衍。
另一个是这个东西其实依赖于程序员的开发习惯,他们需要简单的业务去产生慢慢改变自己,如果总是去支援一个特别着急的需求,它可能迫于时间压力倾向于采用他所熟悉的开发方式,那其实根本就不可能达到这种效果。
KMM 的启示
我们再看下 KMM,KMM 是 Kotlin 推出的一个跨平台的东西,它最推荐你共享的是业务逻辑,像平台访问、前端交互它都希望你遵守原来平台的规范,或者说 UI 展示之类相关的不推荐共享,如果我们的业务逻辑是最大的,我们的 UI 是薄的,或者说我们客户端访问数据存储是薄的,它可以比较快速地实现复用,我们得出了业务逻辑不要去依赖基础设施的结论。
和上面我们要打磨业务模型的结论也是一致的。
前端状态管理方案的启示
然后把视角拉向前端状态管理方案,我们研究了大部分的前端框架,一个很有意思的地方吸引力我们,就是除 Vue 等几个 MVVM 模式的框架外,出现了一些单向数据流的设计理念,Redux 就是这样一个例子,它可能是因为要配合 React 使用,而 React 是一个 UI 框架,这个 UI 框架是以不可变为导向的,前端 JS 借鉴了很多函数式编程的优点,可能更推荐不可变,当然可能是为了整体 UI 更新的时候更可靠的做一个 Diff 算法,才设计的不可变。
总之前端会在倾向于声明式 UI 的情况下,更多地去选择不可变的思路来做 Diff 刷新,这时候我们可能会有一些状态管理的方案出现,除了 Redux,比如说像 Flutter 有 BLoC 这个东西,像 Vue 有 VueX,都有一些类似的状态管理方案。
我们从这里大概了解到 UI 是被动的渲染,它不能更改状态,它只是传递了要做什么的意图,剩下的事情都不在 UI 层里操作。很显然,UI 变成了谦卑对象,这非常有助于写单测。从第四部分可以知道,我们的架构从 Redux 中汲取了一些经验。
后端前沿架构的启示
后端的架构情况方面,可以发现无论是 6 角形架构,还是 Clean 架构,或者是领域驱动设计的其他相关模式,都是以领域为核心,领域层里有一些比如实体类、值对象等,根据建模方式的不同而不同,比如可能用的是四色原型,或者说可能用的是 DCI,也或者其它简单的一些像 ECB 这种架构模式来建设、划分领域层的一些对象,但一定是以领域为核心的,领域里的类不会再去依赖外边的类,外边的类可以去依赖领域内的类,但可能并不会实线依赖。可以发现图中这个是六角形加上 DDD 的架构,从这里边可以看到有一个端口和适配的概念,领域层有一个端口,可以认为是一个接口,我们有进行适配,这里适配的就是真正的技术实现相关的东西,然后去实现领域里边的接口,使得依赖方向反转,这样的好处是我们整个领域模型边界清晰,完整干净,第二个就是我们整个领域模型是完全可测的。基础设施变更了,对我们来说也可以非常好的平滑过度。这些都在我们的架构中有所体现。
快速开发的思考
我们再从快速开发的角度看一个问题,比如我们现在有几十上百号人,如果我们的 Feed 里有好几个团队,不是每个团队在当前都有任务要做的,这时候我们就希望能够把这些人都用起来,支援其它团队的开发,这时候就会涉及到快速支援的问题,比如说我们当时分析了一下这个模型,新入职的同学以及外部的同学跟 Feed 内部的同学到底有什么差异,对于特别核心的业务,有很大历史包袱的业务,真的只有 Feed 内部的人才能去做,但是其它支援的同学可以做一些更普适的工作,如果这些工作做得好,也能非常好的发挥相应的价值,最后的结论就是希望能让中间这部分可独立扩展,可动态裁减,让独立的模块变大变多、标准化,这样能让边界清晰,开发起来也比较方便,不需要太深入业务认知。这也要求我们打造一个微小的内核,大的可扩展边界,将更多的时机以标准化的接口方式暴露出去。
组件化
从公司和整个 APP 层面我们也在做组件化的工作,聚焦在物理组件化层面。但是这里对于组件的边界,抽象程度,依赖形式,我们走过比较多的路。
比如说我们有一个壳工程,存在一些业务组件和基础组件,我们最开始的时候对业务组件的认知是,假如有 A 组件和 B 组件,如果二者之间不想交互,我们就再搞一个 C 组件,让 A 和 B 都去依赖这个 C,就是所谓的“下沉”。
后来我们采用的是另外一种方式,把每个组件分成了两部分,一部分是有一个接口层的模块,另部分有一个实现层的模块,接口层之间可以互相依赖,实现层之间不再互相依赖,是通过其他的接口层 IoC 方式进行间接依赖。这样可以去解决一些类似循环依赖的问题。
这里拓展一下,比如组件 A 如果想扩展,比如第三方的一个业务想插进一个时机做点事情,我们需要给它增强一个能力,一个配置口子。会有这么几种情况,
第一种是直接作为组件的一部分去用,比如在组件 A 里还有扩展层,因为扩展出的能力也是通用的。
第二个是我们把扩展层独立地打包和发布,扩展是独立的,这样能保持组件的动态组合性,但给别人输出的时候也可能会被遗漏。
第三个是我们过去都在用安卓 XML 写布局文件,这种情况下我们需要布局文件里有一个 View 的实体(真身),XML 布局是天然反接口设计的,它是面向实现的。为了能使用这个实现,必须要依赖实现层的模块。
并且过去的时候,安卓推荐 public 字段,不要使用枚举等一系列为性能反设计的内容,,直到现在官方也不太推荐抽象化的技术,但这就导致我们很多数据类都已经做成公开的了,这些繁多的退化类暴露着数据字段,没办法通过接口化的方式包装后透出去,所以不得不又把整个把实现模块暴露出去。
现在我们希望改变这些做法,它们不利于组件化也不利于大型组织,所以我们选择用声明式的 UI ,去 XML 化,并且去减少贫血模型,用接口化的方式去做一些改进。
下图中,左边这个图是整个 APP 当时做组件化的时候划分的架构层次,这个层次的划分依据是按照变化的维度去划分,比如越是与业务相关,那就越容易变化,所以组装层 /App 工厂在最上层,业务在上层,越往下越通用,越是业务无关。比如说网络库,可能没有必要依赖任何东西,它就是一个很底层的模块。
当我们按照前面说的后端六边形架构,DDD 架构,上下之间不能是直接依赖的关系,而是一个反向依赖的关系,比如说网络库可能不需要依赖业务,但业务也不应该直接依赖网络库,中间我们有适配层模块,我们依赖这个适配层的 Port,即接口层,适配的接口实现包装了网络库,从而实现依赖反转,这种做法和思路至少覆盖主要的,核心的,复杂的,需要测试保证的模块。
组件我们可以根据集成方式进行划分,最常见的组件形式是提供某种服务,你需要调用它来获得这种能力,比如工具类的组件,图片库之类的。但也有很多组件是已约束的方式做了某些事,并在很多时机给你留了很多模板方法或者配置参数,希望你通过泛化的方式来扩展和使用,比如框架类组件、提供最小集的中台类组件经常提供基类。最后像 UI 组件和数据类的这种裸奔的组件,很可能需要去包含它,比如你的业务 View 类里包含好些个 TextView,比如我们的卡片里包含一个关注按钮。
然后我们再加上业务相关性这个维度。可以发现,最右侧的业务组件,几乎不被复用。另一个是注意服务化程度代表接口层提取的容易程度,越偏向使用 / 调用关系的,越容易提取,侵入性也越小。这有助于我们甄别所依赖的组件中某种集成方式的占比是否健康。
提到健康,也需要知道怎样判断组件能力是否完善?对此我们列有一个组件能力模型。组件既有物理组件也有逻辑组件,当然这个约束的主要是逻辑组件。
第一层级,逻辑组件明确对外依赖以及自己提供的服务;第二层级,不仅仅停留在明确,逻辑组件以最小的依赖代价提供相应合理的服务;第三层级,则是在第二级的基础上,承诺自己的依赖和服务不会轻易发生变更,并在升级后仍较长时间向下兼容这些明确部分。我们可以看下有多少组件到达了哪个层级,还有组件管理上,身份、生命周期、通信机制是否健全、灵活。这些都可以采用评分的方式,最终纳入健康度雷达和组件评优平台。
边界问题
组件这块,有个共建边界问题,我想分享一个 case,这个 case 也是我们之前遇到过的场景,App 都有 Push 的能力,比方说手机百度 App 里有内容推送过来,点击这个推送以后,它直接会进到视频落地页中,不会进入到首页当中,为了快速启动跳过了首页逻辑,这种情况下,我们进入到第二个视频落地页,我们有个需求是当返回的时候要创建这个首页,同时要把刚才访问的视频落地页的相关条目插入到首页里面去,比方说插到第三楼去。我们的想法是它的数据结构和列表的数据结构不一样,我们需要单独通过 Server 的其他接口把落地页内容条目的 ID 传上去,拿回匹配首页列表 UI 能解析的这套数据下来,因为数据的使用者是首页,落地页去请求不合理,而如果返回的时候再请求已经晚了,因为返回的时候就需要使用数据了,还不知道这个东西是什么,如果再单独去请求肯定会延迟,延迟会导致一开始插不上这个第三楼,所以我们必须得在每进入一个落地页就去做请求并缓存到首页列表能 cover 的范围内。
这个需求涉及到首页模块,也涉及到视频落地页模块,首页会说我提供了一个通用的能力告诉你怎么插入,只要按照规则去插就好。视频落地会说每进到一个视频落地页我会按我的通用格式周知,前提是你得先注册。这个注册的时机在首页不好找,需要在 application 启动时做。这时基础平台会说在 application 加一个只有两个业务方需要的“定制连接”,会影响 TTI,不会同意。而因为一些打点的问题,需要区分条目来源于推送还是原始数据请求。
这时候你会发现这个工作没人愿意去做,第一个原则肯定是好的,每个组件都是独立的,都会提供一些通用的能力,互不依赖,所以应用第二个原则,谁受益谁去做这个事,谁的 PM 去提的需求,谁来做这个脏活,做这个适配。首页的 PM 提的,首页的人单独搞一个模块去解析视频落地页的通知格式,然后拼装去请求 server。即我们建立了一个胶水组件来做这些不太通用的事情。
刚才这个例子里面也有另外一个问题,拿视频通知格式来说,比如这次是首页提的,那首页去做胶水,后面是电商提的,然后电商去做胶水,它们两个都会去解析同样的数据,做类似的事情,这是一种重复逻辑的扩散。所以视频落地页也做了个胶水层组件,把这部分有重复隐患的代码收敛起来,便利大家。
这样你会发现,最后的解决方案可能都是会偏向于我们既有组件,也会有一个类似胶水的组件互相穿插连接。
多产品线方案选型
还有多产品线的一些方案,比如我们采用的方案是分支的方案,当时的考虑是两个团队的发版节奏不一样,如果想搞通用的组件,去做真实的复用,当几个团队之一要做扩展的时候就需要原来做组件的维护者去做一个接口或建设一种能力,一起合作共建。但版本如果匹配不上,这种支援会经常得不到回应,导致这个需求产生 Delay,所以采用了这种分支方案,也就是对于矩阵内的产品、一个新孵化出的创新 App,可能会直接修改分支去做定制化。只要做好组件化,其实大部分组件都是无需修改就能复用的,只有一部分是有差异的,而有差异的部分我们也有自动化 merge 工具来降低工作量,所以这种方案是可以接受的,尤其在效率上,虽然有一定的概率导致组件的通用性不足。这有点像 DDD 里 Context Map 中的跟随者和独立自主的模式。
当然,插件的方式来实现组件设计的真实复用才是理想态,但现在看条条大路通罗马,遗留代码基太大,没必要换这种方案,收益不高。
最右边是构建方案(DDD 里 Context Map 中的共享内核的模式),这种可以基于 android productFlavor 实现,事实上 KMM、JNI 的外观上都很类似于这种映射方案。
最后的一个关注点,就是怎么升级。
架构升级假设
我们有一个假设,当时想做升级的时候,因为升级动作比较大,所以考虑什么样的升级方式能够更好的应对不一样的变化,当时的想法是我们整个业务中有不同的功能单元和能力单元,其复杂度是不一样的,后面的变化和迭代情况也不一样的,我们想实现一些细粒度的定制和升级动作,比如说修改其中的一个能力单元进行架构升级时,其它模块不会有太大的改动,这样比较平滑,最小化架构升级和业务迭代并行导致多套并存的混乱程度。另外一种思路是做一个大一统的标准化架构,并且推行这个架构。但这种架构对很多不常迭代的业务来说比较复杂,犹如大炮打蚊子。所以最后我们还是决定用细粒度的方式做升级,去做组件化和架构模式的应用。只要保留在想升级更重量级架构时能够升级这个可选项。这样下来,整个工程上,有不同量级的可升级的架构,很像一个梯田。
迁移和升级准备
我们之前做中台化的时候也做过一个升级的动作,但最后失败了,原因是我们有一个非常复杂的列表,其中有很多各种刷新,各种实践,各种分支逻辑,比如锚点逻辑,搜索和插入,等非常复杂的逻辑在里面,但我们当时做中台的时候希望中台是通用的,不需要那么复杂,所以当时就另起炉灶去搞一个干净无包袱的组件,设想了三层输出,就是最小可以有什么,通用级可以有什么,业务饱和级列表可以有什么,业务饱和就是带上 Server 的协议和策略。然后在应用这个中台组件时,比如说我们当时在做付费项目时,发现完全是另外一套策略,业务饱和级成了累赘,通用级为了通用,非常的薄,我们需要填补很大的空白逻辑。另外,由于它是独立出来开发的,很难再反哺到一开始所说的真正复杂的列表中去,导致升级中断。
这次升级,不仅注意到这些,而且我们针对架构升级影响到的那些团队做了很多培训和推广,来应对架构升级中的一些设计问题。
有一些人会觉得引进新的时髦的东西就是好的,比如 Android 规范推了这个,又推了那个,他会不亦乐乎的追随和集成。再比如这次 Android 的架构导向偏 MVVM,但是,他发现 Redux 不错,所以在 MVVM 里再攒一个类似 Redux 的设计模式。我们觉得这不太合理,而且大家对这些会有一些自己的理解,会产生歧义。再比如说 MVC 就非常普适,但我发现前端、Android、iOS,不同工龄背景的人,没有多少能达成一致的见解。
所以,这次,如刚刚介绍的思考和原则,又是前端,又是后端,虽然我们也是引入,但重新定义出了一套独有的概念,并予以规范化的解释,使之出处一致,避免扯皮这些。比如我们有个 Processor 的概念,有点像 Redux 的 Producer,但是不直接引用这个名字,因为有差异。
若干场景的架构示例
上一 part 我们集中描述了思考,具体落地只是顺理成章的事,现在举几个场景来做下示例。如下图,这里我采用了 C2 描述语言去描述,不采用 C4 或者 4+1。
我们的组件里 Processor 承担业务处理的逻辑单元,Ability 作为基础设施能力的抽象单元来参与 Processor 里的业务逻辑,比如说我们的 UI 控件,或者几个 UI 控件被包装成 Ability 使用。
再说说 Extension 概念,比如一个处理加载相关逻辑的 Processor,在加载失败时,可能不同频道有不同的微调处理,那么这个加载失败可以定义为扩展点,由不同的频道注入 Extension 来解决。是的,我们的 Processor 基本上所有的方法都是私有的,也就是虽然可以泛化,但是以 Extension 组合的方式来表达扩展。注意,这里的扩展点是面向实现的。也就是说强依赖刚刚说的 Processor。如果是面向接口协议的,一些通用的时机,我们的选择是发 Action,我们马上介绍。
上面说的组件,需要通过连接件组合组装起来,连接件也要作为容器处理组件间,父子容器间的交互诉求。比如我们的展示页面,用 Page 接口抽象,Page 组合管理很多 Ability 。ProcessContext 作为 Processor 的容器会去处理很多 Processor 间的交互逻辑,也会把父 ProcessContext 的 Action 转派给 Processor, 比如通过 Action 的方式去发消息给 Processor,Processor 也可以使用另一个 Processor 的能力,但需要以 Assistant 接口包装。一般来说,解决通信,提供消息机制、服务调用机制、共享内存机制三者中的一个就够了,所以最后一种我们没提供。
我们定义了很多约束原则,这些原则都是事先就规范化的。鉴于篇幅,不再叙述。
说到 Action,我们不得不再提下 Redux。只是这次以子系统视角。下图这是列表子系统,Feed 还有视频落地页子系统、图文落地页子系统等。每个子系统都是独立的,我们不希望这个系统被污染,我们是按照系统思考的方式去做,拿这个例子来说,我们的系统是 Action 应激,没有其他的方式使用这个系统。而且我们摆脱安卓原来的低级事件,比方说安卓点击这个东西,弹什么东西,我们把点击这种低级事件已经转化成了一种高级事件,虽然名字可能叫 Click 事件。这样我们可以基于 Action 的录制和回放来实现自动化测试。尤其在应对复杂的业务时,实现逻辑的自动化校验会很有帮助。Redux 有很多的优势,但我们最看重的是事件溯源,CQRS+ 单向数据流的好处。另外这些也有助于非脆弱性单测的编写。
架构升级的过程中,我们把机制作为框架下沉,包括编排引擎、插件打包。见下图
我们复杂的业务不是很多,但也有一些,比如刷新,图文落地页,对于这种复杂的业务,我们倾向于引入 DDD 战术来改造,比如把真正的实现从外部注入进来,注入进来以后我们会走到一个比较干净的应用层和领域层里边去,这个应用层的目的是去接收外部的 Action,比如搜索(和 Feed 平行,未使用这套架构组件)过来一个事件通知,会通过 event bus 发过来,在应用层会被转成 Action 再进来,我们会在概念上维护 PM 提需时的用例,这些用例会去配置策略参数,然后进一步传递领域层进行更改,比如说我们的显示列表层就是一个领域层聚合,它以数据的方式完全代表最终的显示状态。我们可能还有一些还原历史的诉求,历史是单独的一个概念,也是 PM 和各方面都能认可的通用领域语言概念,这是领域层的一些实例。数据处理这块我们采用 Repository,不是 Android 的 Repository,而是 DDD 里的概念,Repository 和 DAO 最大的不同技术,Repository 是以集合的方式而不是以数据库语言或者网络协议请求方式来维护聚合,Repository 的接口层在领域层,实现在基础设施层,基于前面说的端口和适配器的概念,实现依赖反转。
展示层我们应用了三种模式:
第一种模式是应用了逻辑图层概念,这不仅仅是 Z 轴,而且体现在两个无真实 UI 视图的层,比如手势层,谁在上谁先消费或过滤,这对于事件消费的管理有帮助,另外某些 UI 控件想同显同隐,可以放在一个层上来简化管理。
第二种模式是将页面分区分成命名卡片槽位的方式,这种方式可以应对局部的组件化,比如说我们这个运营槽位可以作为独立的一个组件使用,供别的频道更细粒度的进行复用。
第三种模式是 UI 技术实现,比如我们说列表过去依赖了 ListView,现在依赖 RecyclerView 这个特定控件,未来我们使用了 Jetpack Compose 来实现列表,他们局限于某种技术细节和开发方式,我们把这层实现和要提供的接口能力隔离开来,接口可以使用 Ability 架构组件,真正的实现在业务组装配置的工厂里注入进来。这里的 UI Ability 不同于把页面分区,也不同于一个 UI 控件包装成一个 Ability。比如都是用 RecyclerView 作为实现载体,但是我们的 Adapter Ability, Footer Ability 和 Update Ability 都需要它。
最后,这三种模式可以进行组合嵌套,你中有我,我中有你。比如我们先用逻辑图层,然后它的某一层,可能嵌套使用分区命名卡片槽位,槽位里边也可以嵌套配置图层,或者将某个槽位组件做成 Ability 或 Ability 的容器 Page。也可能是反过来,比如整个 Page 里很多的 Ability,其中的某个 Ability 的载体是列表控件,列表条目是卡片槽位,某个槽位进行了图层化处理。
当然这需要 Ability 设计时考虑它的复用方式,比如是否支持槽位,如果可以支持,就将槽位管理器引入进来,然后由工厂注入时配置开关。
架构维护控制
在架构维护控制方面,我当时看了《演进式架构》,很受启发,演进式架构的一个道理是想利用适用度函数尽可能管理和降低架构裂化程度,我们也定义了一揽子适用度函数。比方说像 McCable 圈复杂度,稳定性度量等。
有一次我们去参加 ATAM 的培训,它是一种架构权衡的分析方法,类似以评审会的方式对质量属性进行评价,进而对架构进行评价。也讨论了哪种方式可以作为长期的适用度函数,但总体上这种方式开展的不频繁,和团队对冲突管理的好坏有关系。有一段时间,我们也研究了 Google 提供的 GSM 这种方式,了解到除了定量还可以通过观察开发者是不是产生了某种行为来衡量收益和结果。这种方式的实施仍和管理者理想中对于目标的掌控方式有很大关系。之前也提过,如果管理者的想法是让每个人都忙起来,工作饱和起来,被管理者可能对提效这件事不再敏感,因为无事可做,仍要假装很忙到 11-12 点,那还不如慢慢做。我听过一个故事,说老板希望员工很忙,后来他做到了,以至于有顾客进店都没人去理他,因为大家真的忙起来了。
除此之外,不同的团队,比如不同阶段,不同环境和文化,不同规模,不同业务形态,在架构的选型上,以上的架构实践方式是否适合,需要自己权衡。
可以做一个架构能力评估模型来评估下,自己当时的架构是什么水平的,可以做成什么水平,平台支撑是不是足够好。比方服务端能不能做微服务?并不是每一个团队都适合做微服务。比如我们当时非常关心的一点是,是不是有一个最佳的决策路径,我们的一线开发者遇到问题时,可以助力设计决策。其他部分详见下图。
另外我们需要做好防劣化,这块有几点印象比较深刻,比如说像 Emacs 编辑器可以煮咖啡吗?也许能,也许不能,我对此的感受是,程序员希望做一件事时,没有其他的干扰,比如编码时,他不希望又从浏览器收藏夹的某个链接进入网站进行编辑搜索文档。最好不要离开 IDE,不要离开开发者手边的东西,离它越近的东西越有价值,比如我们定义一个编码规范,在 IDE 里直接提示应该怎样做,开发者可能懒的去找规范,而不是提交完了,再气冲冲的去改问题。
第二点就是我们会在很多地方做埋点去监控东西,比如说静态内部类是单例是最好的写法吗?不是,最好的写法是,你只要去标一个注解说这是单例,不管里边怎么实现的,是不是真的单例都没关系,就可以在团队级别起作用,大家得以自觉的不再去创造新的实例了,怎么能让这些契约成为武器,那就让他们尽可能在埋点的地方,进行自动生成。
第三点,我们的钩子里也有助于我们知道有哪些单例,有哪些架构组件,有哪些卡片等等,这些数据统计会对做架构决策有帮助。
下面这是我们的几个示例,这几个东西都是 IDE 上的,都是基于开源框架去改的,它有各种衡量,比如像复杂度的衡量和抽样度的衡量,架构决策上是引导,lint 检查。我们自己在做 IDE 插件,我们的模板卡片怎么统计,它到底是什么卡片,什么类型,它的数据结构是什么样的,大概长的什么样子,能不能复用,这些东西都可以在 IDE 的工具里找到。
还有就是平台,我们基础平台团队在 DevOps 平台帮我们检查很多指标,像组件接口的依赖化的程度是不是合适的。后面这部分我们还在整合,不方便展示,包括健康度雷达,组件的健康度指标等。
作为一个业务开发团队,要体现业务价值,所以我们的业务目标是业务数据,比如 DAU、时长、留存、转化率,因业务形态、App 形态而异。这是必须直面的部分。
但这些业务数据往往不是一个业务开发团队能左右的,且不说里面有天时地利,而且需要 PM,策略,开发,市场等一众团队合力解决的。
所以另一个间接目标可能会是这个业务开发团队的口碑,一种公司内的“品牌”。这意味着虽然数据指标仍不如意,但我们已经尽最大的努力做到做好,比如我们做的需求数同比环比提高了一个量级,或者比公司其他的相似团队有质的提升。因为后者对开发团队本身更可控些。
有了这种用研发效率间接体现业务价值的思路,我们可以从需求数量(多)和需求质量(好)上着手继续拆解。
需求数量,可以用一个版本需求吞吐量衡量。那为什么需求可以突然做得多?可以大体拆成需求,人,效率三因素来看:
也许是 1)需求拆的小或者需求简单;
也许是 2)大家加班肝出来或者人员给力水平卓越;
也许是 3)效率提升,命中了之前做的改进措施;
那相反,为什么做得慢,需求复杂或者人员躺平或者没命中改进措施。很显然,需求拆小可能是“考核什么就会得到什么”的古德哈特定律对团队的反噬。也可能不是,我们可以把需求类型拆解为:
一期攻坚需求;要求快,投入人力往往较多,需求复杂。这种对复用性和协作分工性提出较高要求;
后期迭代需求;这种需求偏小,但迭代频。会对不同团队成员的接力,进而对可读性提出要求;
UE 需求;这种需求频率小,复杂度低,但波及范围广。会对组件的规范性提出要求;
研发技术需求;这种我们不展开。
从这里也可以看出,提炼质量属性以及多维度指标是可行且可刻画需求本身(因为下面说的命中和此有关)。
看起来效率提升是可持续发展的正确思路。但拿技术手段来说,这里都有命中和没命中之说。比如改进措施:
1)架构边界清晰、可协作分工,设计通用、机制健全、可高度复用(用质量属性度量)。 因为我们不是在白纸上工作,都是在一个大的遗留代码中进行改进,没个几年不可能翻新一 遍代码,也无法保证翻新的收益以及翻新过程中间新出现的负收益。所以那些只谈从 20 天变成 5 天的,不谈命中概率的,我是持怀疑态度。
技术上的改进措施还可能是:
2)采用了自动化或者低代码技术(自动化率衡量)
3)动态化技术使双端需求变成了单端需求,比如用了 H5、RN、KMM 等技术(随版转非随率,人力投入比衡量)。
后面两者(2、3)依然存在命中概率,所以做得省、做得快不代表需求做得多,因为可能把人投资在了建设和维护这些如低代码平台,动态化能力完善上,改善架构和设计上。尽管如此,相比非技术手段的改进措施,如流程得到优化;梯队合理、培养了 backup,导致不需要有人从旁指导,接手速度快;等等吧,技术手段还是更研发可控些。
需求质量,那肯定业务数据提升代表需求质量好,数据提升的前提是:
1)体验好;2)线上稳故障少;3)我们的数据指标统计准。
虽然后者无法覆盖前者,但在一定程度上能间接刻画。而这些间接指标我们有些是定性,比如说团队成员有了数据驱动意识,有些可以拆解到研发可控的质量属性上:
1)性能;2)可维护性、可用性(鲁棒性);3)可维护性,可预测性(平台监控、工具检查、测试覆盖等)。
说了这么多,就是想说我们可以通过建设健康度雷达来刻画质量属性,从而可以间接量化架构优化和重构升级的收益,这是一种不够严谨但研发可控的方式。但量化的收益是需要考虑命中率的。可以使用工具来识别迭代热点等来提高命中率,虽然如股票一样,这些指标仍代表的是过去,但对趋势研判是有助力的。另外就是架构、基建、机制这种往往是每块业务都避不开的,天然能命中。健康度雷达的另一个好处,我之前有看过,说这些质量属性上的提升,也会提升开发者的安全感和幸福感。
回顾架构过程,我们做架构升级需求分析的时候,并行开发快速支持其它业务、业务动态下发、尽可能免测来提高研发效率、提高质量是我们的需求。我们甄别了几个除性能之外的质量属性,我把可观测性加在里边去了,我们很多东西是为了它去做设计的,然后我们进行了架构风格的选择,我们可以从内部自下而上进行改造,但是我们也想尽量和行业接轨,推行了很多风格的模型去做选择和借鉴。
最后就是维护的几个指标和平台建设的情况,我们整体获得了一些收益,比方说像输出复用的收益,还有并行开发、动态化这方面得到的一些改善,测试体系也得到了一些保障。很多时候在真实的复用场景中,如果复用的东西出现问题,影响面会非常广,我们希望有一个测试的体系来保障这些是可以改的,如果没有,有的时候确实还不如拷贝的方式,因为不希望影响到别的业务,所以想要真实复用,我们必须要建设测试体系。
开发效率的提升来自于免测的情况,还有一个是非单点依赖,我们原来的业务很复杂,非常依赖某几个人,现在有了测试可以很好地应对这个问题,通过测试来知道改动对某些 case 是否有影响,这样我们就有信心去重构。
在决策自由度的改善上,比如从业务维度进行隔离后,各个业务参更好地根据自己的特点进行决策,因为它的粒度更细了,无论怎么说,细粒度化和积木化确实是大型 APP 的走向。
评论 1 条评论