Mindera 外科手术方案¶
必要性¶
业务驱动(功能性需求)¶
-
对外交付需求
- 予淳的用户故事:有的客户只需要指标图表模块,且需要源码,能在最短时间内独立交付吗?
-
需求的吞吐率
- 系统受技术债所累,已经严重腐化,对于新需求的吞吐效率明显不如新的技术架构。
-
产品性能问题
- 错综复杂的依赖关系,模糊的层次结构,增加了性能优化的难度,在大刀阔斧提升性能时,可能触发难以预料的BUG。
-
版本管理需求
- 前后端版本匹配问题
- 场景:当改图谱模块的固有BUG时,Mindera 的图表指标前后端开发了新的Feature;解决完图谱模块的BUG,外部带着图表指标的新 Feature 一起上线;这时外部项目的后端并不支持新Feature?
- 后端被动升级,测试反复回归,怨声载道。
-
重新审视 Mindera
- 定位:不再是单一的嘉实内部的一款个性化产品,而是被部署在1-7号项目,适配不同环境、个性化需求的
- 审视依赖管理,外部项目重度依赖的核心库。
- 不是应用(产品),而是框架(平台);所以,Mindera 是否具备了一个框架应该有的稳定性、可扩展性、可插拔性?
技术驱动(非功能需求)¶
-
可扩展性
- 面对新的需求,修改原有代码的难度极大,开发效率低还容易出BUG。
- 对外项目的Adapter层越来越厚,包含的定制化业务逻辑越来越多,已经开始逐渐偏离适配器本身的概念。
-
可维护性
- 复杂性高,心智负担大,“活文档”也不能解决所有问题
- 难以拥抱新技术(新技术理念解决新问题、团队技术吸引力)
- 代码规范也存在问题,首次开发天马行空,二次修改如坠深渊;比起一个大的系统,一个小的应用引入新的规范所带来的风险更可控,规范变更时需要的代价也更小。
-
可重用性
- 模块之间高度耦合,依赖关系难以理清,导致功能模块/组件在复用时,可能引发一些不可预知的被BUG。
- 大量 COPY/PASTE 的冗余代码实现,导致定位BUG时耗费大量时间
-
风险性
- 上线某个模块的一个小需求,整个项目需要重新构建,构建速度慢;
- 一个小的BUG,可能导致整个系统运行失败,受影响的风险范围大
-
项目质量与可测试性
- 图示
- 阶段 1:正常,都是线性增长。
- 阶段 2:需求数正常增长,业务代码行数开始增长,测试代码行数大幅度增长。
- 阶段 3:业务代码行数开始大幅增长,测试代码行数剧增(超出屏幕),而需求数开始下降。
-
后果
- 从业务最开始,到长期迭代后,复杂度提升带来的问题。做一个相同的需求,最开始可能 1 天就可以搞定,但长期迭代后,可能要 3 天,甚至更多,
- 这并不是开发人员主观上导致的,而是代码状态的维护成本太高,做到最后经常会出现牵一发而动全身。
- 关键是:抑制了业务的迭代速度,同时影响项目质量。
可行性分析¶
历史¶
-
当时的临时方案
- Bad Case
- Temprory Solution
-
Mindera 拆分成功先例
-
之前静哥对图表指标模块做过一次成功拆分
- 成本:1.5人天拆分,1人天接入
- 结果:成功存活、逻辑独立
-
但未集成回去
- 分化成为两个项目,独立进化,无法合并;
- 最终独立出来的图表指标库,版本停留在当时,无法进化。
-
思考评价
- 架构升级有一个好的开始,但缺乏历史经验,和对这种临时方案的过度自信和乐观,最终半途而废;
- 但这次尝试为架构升级的可行性论证,提供了强有力的成功经验,历史意义重大。
-
-
微前端架构的成功稳定运行
- 市面上有很多微前端方案,乾坤技术成熟度较高
- 团队成员的掌握程度高,避免了引入新技术的风险,不会带来额外的认知负担
- 运维部署方案已经成熟稳定,新的 DevOps流程已经打通
- 由于组件依赖关系的简化、性能表现更好
- 总结:收益很好、风险可控
时机¶
- 时机一:架构在腐化时,已经难以满足关键场景的关键需求,例如对用户请求的响应速度越来越慢,已经接近临界值,并且根据经验,响应速度还有可能继续降低,越来越难以维护,这时可以考虑进行架构演变,对架构进行改造和分解。
- 时机二:如果能提前预见系统的问题,在经过慎重评估后,提前进行架构演变,则也是可以的。
- 同时警惕:不要过早分解、过度分解,否则会增加成本,带来风险。
需要代价¶
-
- 前端开发,一到两个人做拆分/集成
-
- 后端,不需要
-
- 测试,全面回归
-
- 部署运维,需要重新调整打包部署方案(一次性工作)
指导思想¶
分解¶
-
概念
- 分解即 “分而治之”,将复杂的系统通过「关注点分离」进行多层次拆分,保证分解后的高内聚、低耦合,最终集成为一个整体。
- 莱布尼茨:“不讲究分解技巧,分而治之的作用就不大。无经验者对问题分解不当,反而增加解决问题的困难。”
-
原则
-
-
高内聚、低耦合
-
莱布尼茨:分解的难点在于怎么分。策略之一是按容易的求解方式来分解,之二是在弱耦合处下手,切断联系。
- 对于目前的非结构化的复杂系统,采用策略二。
-
-
-
正交原则
-
分解出的元素,应是相互独立的,在职责上没有重叠。
- 符合单一职责原则
- 根据现状,元素的粒度,是子系统或子应用。
-
-
-
抽象原则/稳定性原则
-
符合依赖倒置原则
- 将稳定部分和易变部分分解为不同的元素,稳定的不应依赖于易变的。
- 通用部分 vs 专用部分分离
- 动态部分 vs 静态部分分离
- 机制 vs 策略分离
-
-
-
复用性原则
-
Don’t Repeat Yourself。
-
-
集成¶
-
概念
- 分解完成的各个子系统、模块、组件,通过合理的接口,最后还原成一个完整的整体。
- 分解的目的是加速开发和降低问题复杂度,如果分解后无法集成,分解就毫无意义。
-
集成方式
- API网关方式;主子应用模式
实施步骤¶
现状¶
- 图示二
手术过程¶
-
STEP1. Eslint 代码检查
- 解决所有语法错误问题
- (解决一部分依赖问题)
-
STEP2. 拆分主子应用,路由重新规划
- (配合 alpha-admin)
- (或者干脆换新的权限系统)
-
STEP3. 通用代码下沉到 Mindera-packages
- STEP4. 部署方案调整
- STEP5. 测试分阶段回归
思考与启示¶
理解技术债¶
-
定义
- 技术债务是由Ward Cunningham在1992年创造的一个比喻,定义为我们有意或无意中做了错误的或不理想的技术决策所累积的债务。
- 无意的:由于经验的缺乏导致初级开发者,编写了质量低劣的代码。(开发者能力)
- 有意的:团队根据当前而非未来进行了设计选型,这种方式可能很快解决了当前的问题,但很拙劣。(临时方案)
-
成因
- 随着技术团队人员的能力提升,无意的技术债务可能会越来越少。但是在很多时候,我们欠下的技术债务往往是有意的,随着业务的高速发展,还会带来很多变化。为了快速实现业务的功能及验证商业决策,开发人员大多会采用很多临时方案。
- 毫不客气地说,这些临时方案其实就是技术债。
- 临时方案的可怕之处,不仅仅在于其不全面,更在于如果不对其及时处理,可能就永远“临时”下去了。随着团队成员的不断更替,慢慢地就没人记得这个临时方案了。一个说好只支撑两个月的临时代码,可能两年后还在线上机器里运行着。
-
后果
- 随着系统中的临时方案越来越多,我们需要还的技术债也越来越多。欠债并不可怕,可怕的是没有偿还计划。
- 技术债务和金融债务非常相似。一个人贷了款就会产生债务,如果可以定期还款,则所欠的债务是可以接受的,不会产生进一步的问题;
- 但是,如果不还款,就会以利息作为惩罚,利息还会随着不还款次数的增加而增加。如果这个人在很长一段时间内不能支付任何款项,那么应计利息会使他更难以偿还债务。在极端情况下,这个人就不得不宣布自己破产。
技术债偿还计划¶
- 技术债的存在是软件质量无法得到很好提升的一个重要原因。对于遗留的技术债,很多开发人员很想解决,但是由于各种各样的原因,技术债越积越多。
-
关于技术债的偿的观点
- 技术人员水平不行、意识不够,代码越写越乱。
- 开发者都有一颗积极向上的心,但不知道如何做才好。
- 上行下效,没有靠谱的Leader、架构师,大家便都是懒惰的,不想去还债。
- 常见业务压力太大,赶着交付,没有时间清偿技术债。
-
应对方案
- 构建质量保障体系,让开发人员有勇气动手改造。比如进行接口测试、单元测试,通过持续集成进行反馈,这样开发人员在做重构的时候就有一些基本保障。
- 不要动遗留系统中没有出问题的地方,对于遗留系统中出问题的地方,修复并补充测试代码,更进一步地,对高危功能和模块做定向增强。
- 形成好的作风,使好的工作作风和习惯影响整个团队。
- 抓住痛点发起讨论,让大家发自心底地认同,觉得必须得重写代码
解决技术债原则¶
- 通过培训、代码审查等方式杜绝无意的技术债。
- 要尽量避免有意的技术债。
- 如果一定要引入技术债,则一定不要引入不计后果的技术债。
- 要记录所有欠下的技术债,并且有偿还计划。
启示¶
- 技术债要做清偿计划。
- 不要在下雨天补屋顶。