正交架构 低代码架构 低组件架构 超类架构
比耦合架构更好的架构 https://yanhaijing.com/program/2021/07/17/coupling-and-composition/
背景
本文用上传组件作为例子,一般业务中的上传组件都有自己的逻辑,现在假设我们有如下多个不同的上传组件:
- 文件上传组件,通用的上传组件
- 图片上传组件,有自己的展示逻辑和图片处理等
- 音频上传组件,有自己的展示逻辑和音频处理等
- 路径上传组件,有自己的展示逻辑且参数也不同
现在让我们想一下,实现这些逻辑,代码该如何设计,在往下看之前,建议先看一下我的另一篇文章——图解7种耦合关系,下面会用到这里面提到的知识
耦合架构
耦合架构就是把代码都写在一起,这样就实现了公共逻辑只存在一份;然后内部存在很多条件判断,来实现不同的分支逻辑
相信大家都能看出来耦合架构的问题,心里也都有自己的答案,但耦合架构是怎么形成的呢?耦合架构大部分都是业务演进的结果,开始时业务只有一种,当来了新的业务时,可能开人员偷懒了,也可能是缺乏重构的勇气,就慢慢积累了下来,然后尾大不掉,渐渐失去了所有人的控制
耦合架构的优点大概只有节省设计时间了,因为不用设计,业务线性演变即可
耦合架构的问题在于难以维护,不同业务的逻辑紧紧交织在一起,系统复杂度会随着分支的数量指数级增长,一般随着时间的积累,很可能会逐渐失控,演变成下图这样
耦合架构对于改动非常不友好,极有可能牵一发而动全身,改不动,理不清;耦合架构对于修改Bug非常不友好,如同盲人摸象,难以排查;耦合架构对于阅读非常不友好,理清逻辑犹如管中窥豹,错综复杂
耦合架构演进到最后,很可能被弃坑,特别是人员变更,恰巧来了一个新的功能时,极有可能放弃前人的积累,另起炉灶
拷贝架构
耦合架构演进到最后很可能变为拷贝架构,这也是我在项目中看到的问题,同学们告诉我,这个有点复杂哦,建议你拷一份,别修改之前的代码
拷贝架构就是各个组件完全自成一份,每个都包含完成的功能,在写业务之前,就把之前的拷贝一份
拷贝架构的优点就是相互隔离,业务之间的演进不会相互影响
拷贝架构的缺点也很明显,公共逻辑被复制了多份,违反了DRY原则;特别是如果公共部分自身改动的话,可能会有研发效率加倍,或者逻辑不一致问题
超类架构
面对前面的问题,大家会做出不同的改进,超类架构是一个典型的方向,面向对象思维建议用继承来解决泛化问题,通过抽象一个上传基类,可以解决公共逻辑的问题,不同业务的差异,可以通过子类来泛化实现
超类的优点在于解决了泛化和公共的问题,通过抽象继承隔离了两部分逻辑
超类的问题在于,继承本身就脆弱,大部分前端的OOP能力也良莠不齐,存在四不像设计和啥都往超类塞的问题;超类虽然隔离了公共逻辑,但还是会将公共逻辑已接口的方式暴露给子类,子类需要感知父类的设计;父类很难设计的良好,面对发散的业务,后面父类的迭代会越来越痛苦
超类架构依赖于良好的抽象,如果抽象不好,也会存在弃坑的问题,很有可能拷贝一份而放弃了超类的维护
组合架构
组合架构是更适合前端UI组件的架构,通过将页面拆分成功能独立的组件,再将组件组合到一起从而实现页面功能,在我们的例子中,可以将公共逻辑提取为一个功能单一的上传组件即可,其只包含上传逻辑处理,对外提供属性和回调函数
从架构图上来看和超类是相似的,但继承和组合是两个概念,组合是自下而上的,可逆的,上下层组件之间的耦合是松散的;继承是自顶而下的,不可逆的,父子组件之间的耦合是紧密的
组合架构依赖于设计良好的组件,需要遵从单一职责和开闭原则,设计糟糕的组件会让系统变的脆弱,组件要做到高内聚,低耦合,组件要做到面向参数设计,剥离业务
总结
本文通过一个例子,介绍了前端的4种设计模式,希望能够帮助大家,在解耦合的道路上有更多的选择
一般一个模块人人都觉得复杂,却又没人提出改进,那往往是设计存在问题,一般好的设计都是简单的
多子类型业务架构演进 https://yanhaijing.com/program/2021/07/22/decoupling-the-large-subtype/
耦合架构
像这种把子类型的逻辑都写在一起的模式,我称其为耦合架构,需要注意的是耦合也十分级别的,同样是耦合松耦合和紧耦合也是不同的,如果对于耦合的分类感兴趣,可以看下我的另一篇文章——图解7种耦合关系
PS:我的另一篇那文章也提到了耦合架构,感兴趣的可以看看——比耦合架构更好的架构
一般听到耦合,第一反应就是解耦,但是先思考一个问题,我们的代码是如何失控的?
系统很有可能是这样演进的,在业务开始时
- PM提了一个子类型需求M1,M1包含标题和音频
- PM提了一个新的子类型M2,在M1的基础上多了一个视频
- PM提了一个新的子类型M3,在M1的基础上多了一个图片
写下第一行代码的同学很快就完成了M1的开发,当面对M2和M3时,优先会考虑复用M1的逻辑,极有可能选择在M1中加上M2和M3的逻辑
整个系统就这样慢慢的演进,随着时间的积累,慢慢走向失控
- PM提了一个子类型M4
- …
- PM提了一个子类型M50
- …
- PM提了一个子类型M100
终于有一天变成了现在这样,也许早就有同学发现了系统的问题,但未能及时解决问题,相信不少业务会有这种年久失修的问题
现在整个系统紧紧耦合在一起,巨量代码和逻辑交织在一起,整个系统蕴藏着巨大危机,开发效率低下,几乎无法并行开发,因为没法解决代码冲突啊;维护风险很高,真正的牵一发而动全身
整个系统已经到了岌岌可危的处境,急需一个人挽狂澜于既倒,扶大厦于将倾
如果想解决问题,首先我们要理清问题,这个系统目前存在如下问题:
- 新人上手成本(看不懂)
- PM让我修改旧类型(改不动)
- PM说这个可以复用另一个(理不清)
但是这些都只是问题导致的结果,导致这些问题的根源是,一个类型的需求,要面对全部类型的逻辑,系统复杂度:O(n^2)
其实前人们也一直在努力解决问题,只是努力的方向是,在现有架构下修修补补,比如总结文档,代码上面进行归类抽象等
正交架构
正交架构的思想其实非常简单,既然要解耦,那就直接把每个子类型的逻辑分开实现不就好了吗,代码上彻底隔离
正交架构其实就是分治思想,分治思想提倡将大的问题,分离成多个小问题,从而分别解决,在多子类型系统中,子类型就是一个绝佳的分治媒介
正交架构带来的直接好处就是,子类型解耦了,子类型内部逻辑是自治的,相互之间没有关联,从而使每个子类型的复杂度降低了,虽然系统里的逻辑和代码量并没有减少,但系统的整体复杂度降低了,在我们这里例子里面,用正交架构替换耦合架构收益如下:
- 复杂度 n^2 => n,增加子类型时,系统复杂度线性增长
- 代码量 2个数量级,修改一个子类型时,代码量由万行级别 => 百行级别
- 分支数量 3个数量级,系统内部逻辑分支由千级别 => 个位数
既然正交架构整么好,那为何不用正交架构替代耦合架构呢?没错,如果是新系统的话,我建议你从一开始就选择正交架构,但对于存量系统就很麻烦,因为有巨大的历史包袱,在这种情况下,如果时间紧迫,我建议先从新类型切到正交架构,历史包袱先保留不动,正可能需要一点小的代码设计才能实现,但我相信难不倒你的
接下来说说正交架构的问题,通过把子类型的代码分开,带来一个明显的问题,在耦合架构中,类型之间的公共逻辑和组件是复用的,但在正交架构中是重复的,特别是对于复杂的逻辑和复杂的组件,问题尤为明显,一旦这部分功能要统一修改时,那可能要在每个子类型都要修改下
这个问题可以通过将公共组件和公共逻辑抽象出来的方法来解决,一般如果2个子类型重复的部分,就应该提取出来,在业务迭代中,如果你想复用另一个类型的功能时,就是抽象的合适时机,一般PM会提醒你这个事情的,PM可能的对话如下
小颜同学,这个新类型的这块就和之前的一样,我就不用在描述了吧 —— 传说中的一句话需求
关于公共组件化,还有一个问题不得不提,有两种抽象公共组件思路,一种是,组件提供配置,不同子类型使用组件时,配置不同开关;一种是,把子类型作为参数,传递到公共组件,组件内部判断子类型实现不同逻辑;对于前一种,我称为纯组件,对于后一种,我称为非纯组件
我发现不少同学在提取组件时,会写出来非纯组件,其实非纯组件只是把代码物理隔离了,逻辑上并没有隔离,在你的公共组件里其实还是一个小型的耦合架构,所以建议大家选择纯组件
除了上面的非纯组件问题,正交架构还有个最大的问题就是组件化是可选的,这其实给拷贝代码提供了可能,让重复代码有继续存在的温床,提取组件这个事情就是一个最佳实践,属于弱约束的事情,而弱约束一般只能通过代码评审发现……
后面可能发生的事情大家都懂了吧
低代码架构
整个系统在正交架构下跑了一段时间,顶住了业务的压力,但我一直在思考有咩有更好的架构,最近低代码如火如荼,多子类型有咩有转低代码的可能?
说来也巧,刚好业务要对系统进行大的改造重构,在这个过程中,我落地了低代码架构,对于多子类型业务,如果其类型之间存在一些相似或重复部分,那么可以考虑低代码架构
对于单个子类型来说,只需要将正交架构中的代码,全部替换为配置,配置可能会对应一套DSL语言,需要搭配一套渲染器,渲染器读取配置,将每个配置渲染成组件库里的组件,就形成了完整的闭环
我发现低代码刚好解决了正交架构系统可能存在的两个问题,低代码架构强制系统必须全部组件化,这就避免了正交架构可选的组件化可能带来的同样逻辑,实现不同问题;同时低代码架构下,强制要求组件面向配置开发,这恰巧杜绝了非纯组件的问题
低代码这一套铺下来,整个系统的复杂度和子类型数量的解耦,整体收益如下:
- 复杂度:O(N) => O(常数),N是子类型数量,常数和组件数量相关
- 开发效率:80% ↑,新类型大概率不需要开发了
- 强制纯组件化
但低代码也不是没有缺点的,首先整个系统的架构会比正交架构复杂,开发难度更大;低代码系统会增加额外的开发工作,比如配置系统,DSL语言和渲染器的开发等;如果遇到系统不支持的组件,需要额外的开发成本,且开发成本会大于这个组件在正交架构下的开发
低代码架构中有几个关键的技术难点,这里简单提一下
- 绑定数据能力
- 联动能力
- 组件可扩展能力
- DSL可扩展能力
- 校验如何设计
- 组件设计原则
如果对低代码架构的实现细节感兴趣,可以继续关注我的后续文章,您的回复和打赏,是我继续写下去的动力
低组件架构
整个系统在低代码架构美好的演进了一段时间,但很快遇到了一些困难
首先是业务的多样性,导致了某些组件的配置爆炸式增加,有几个典型的组件有几十个开关,其自身的复杂度也需要关注了
有些子类型其功能不具有复用性,对于这种,我们扩展了类型组件的支持,也就是给这个子类型开发一个组件,其逻辑都在一起,就和正交架构的实现区别不大了
最大的问题,有些类型,其校验逻辑和联动关系非常复杂,通过配置化来实现这些联动和校验时越来越困难,维护的同学表示很难受
关于上面的问题,我思考了许久,提出了低组件架构,这个名词是我发明的,低组件架构其实是融合了正交架构和低代码架构,博取两家之长,避开两家之短
拿表单来举个例子,我们的程序其实是分成两部分的,首先是组件的渲染,也可以理解为静态UI的展示,这一部分其实是比较容易通过DSL来表示的;在UI背后还有逻辑层,包括组件的联动,校验等逻辑,这一部分逻辑存在DSL困境
其实我们写的代码,比如js,html等就是通用的DSL,既然如此不如换个思路?一个子类型包含哪些组件,组件包含哪些参数,通过低代码架构来实现;组件背后的逻辑层,通过正交架构来实现
低组件架构,融合了两种架构,避免了正交架构中的组件发散问题,同时也避免了低代码架构中的DSL爆炸问题
不过这个架构,我只是做了纸面上的推演,并未落地,纸上得来终觉浅,希望能给同学们一起启发,如果有同学有尝试,欢迎交流
总结
综上,我们介绍了多子类型系统的4种架构,其实4中架构都能实现需求,但其思想却有很大不同,下面同不同层面做个对比
首先从子类型数量和系统复杂度方面来对比下,低组件架构其实是低代码架构的一个变种,所以此处不单独列出
再来对比下,开发人员面对不同架构时的心智模型
通过上面的介绍和对比,相信同学们对4种架构的定义和区别都有了自己的认识,其实架构没有好坏之分,只有适合不适合,下面从我的认知,给大家总结下不同架构适合的不同业务场景
架构 | 适应场景 |
---|---|
耦合架构 | 无,除非你想离职了,不过害人终害己,天道好轮回 |
正交架构 | 子类型小于30个的场景,子类型之间区别很大时 |
低代码架构 | 逻辑不重,子类型较多时,子类型迭代较快 |
低组件架构 | 同上,但逻辑较重时 |