1. 项目概述与设计哲学
1.1 核心需求解析
这篇博客想聊的,不是某个具体的框架、中间件或者某段可以抄的代码,而是一套架构设计层面的方法论,我们内部代号叫“底层锁死”。简单来说,就是在马年这个节点上,我们重新整理了一套企业级可插拔架构的标准化方案,版本号刷到了V4.0专业终版。
很多团队在做系统架构时,容易走两个极端。要么是代码写得不够抽象,业务模块和底层实现死死绑在一起,改动一个存储层的字段要连坐十几个服务一起发版;要么是抽象过头,搞了一堆“可插拔”的接口,但每次新增一个模块依然要动到主流程代码,所谓的“可插拔”最后只停留在PPT上。这套V4.0方案要解决的,正是这两个极端中间的平衡问题。
标题里有个词叫“Freestyle”。这个词用在架构上,并不是说想怎么写就怎么写,而是指在“锁死”的边界之内,插件模块的开发者可以拥有最大限度的自由发挥空间。你可以自己决定这个模块内部怎么分层,自己选择是同步还是异步,自己决定引入哪个外部组件,只要你的实现遵循了我们定义好的契约,能通过系统的加载校验,那你就能作为独立单元插进整个大系统里。
所以准确理解的话,这套方案的真正价值在于,它是用一套非常严格、几乎不容商榷的底层约束,换取了一整片广阔的自由扩展区域。这有点像城市建设规划:地下管网、电网、主干路网必须统一标准、统一建设,一旦建成就很难改动;但地上的建筑、店铺、住宅区,则可以千姿百态,只要接入管网的方式符合规范就行。锁死的是管网标准和接口协议,解放的是业务形态。
特别声明一下,以下内容是我们在真实业务环境中反复迭代出来的经验沉淀。因为涉及的系统和内部命名都做了脱敏处理,所以我会用虚构的“某跨平台业务系统”和“某内部组件”来代称具体对象。讨论的重点是这套架构的设计思路、落地步骤以及踩坑记录。
1.2 版本演进的来龙去脉
不要一看到“V4.0”就以为我们是先烈了,更不要觉得版本数字越大越复杂。恰恰相反,从V1.0走到V4.0,我们做的事情是不断的减法——减掉不必要的依赖耦合,减掉模棱两可的接口设计,减掉多余的双向通信链。
V1.0时代我们做的其实就是一个普通的插件化框架。那时候大家觉得能动态加载Jar包就算成功,结果做出来以后发现插件之间没法互相调用,数据格式各干各的,每次升级主框架都会导致一堆插件兼容性问题。
V2.0开始引入统一的数据总线,解决了插件之间通信的问题,但代价是总线自身变得非常沉重。每次收发数据都要做校验、过滤、格式转换,性能损耗扛不住了,而且总线还在变相成为所有插件的隐式依赖,任何一个插件变更都有可能导致总线逻辑跟着微调。
V3.0我们把总线拆成了轻量级的事件流机制,只保留消息路由和轨迹追踪,把业务逻辑全部分发回插件自己处理。这一版上线之后稳定性大幅提升,但依然存在一个问题——插件接口的定义颗粒度不一致。有人把接口拆得特别细,一个业务方法都要拆成两个接口,有人则喜欢大而全的上帝接口,塞了几十个方法进去。接口定义风格的混乱,直接导致底层内核在发布版本计划的时候根本没法“锁死”任何东西。
所以V4.0的核心目标,不是加更多功能,而是把“接口契约的边界”给彻底定义清楚。我们花了大半年时间梳理已有接口清单,重新划分层级,将底层能锁死的部分全部锁死,将扩展点部分全部标准化,最终形成了这套比较成熟的模型。
1.3 适用场景与目标读者
如果你正在做或准备做以下类型的事,这篇博文会比较对路:
- 内部PaaS平台或基础技术底座建设,想让上层业务以插件形式快速接入;
- 微服务架构中的公共能力沉淀,比如权限、审计、消息推送这类横切能力,想达到“改一处全盘生效”的效果;
- 有多个业务线共用一个系统的场景,各业务线有公共诉求也有差异化诉求,需要一套插件隔离机制;
- 代码库长期多分支并行,每次合入主版本都掉一层皮,想通过“底层锁死”的方式减少迭代冲突。
无论你是架构师、技术负责人,还是资深的业务开发兼系统设计意愿比较强的人,这套思路都可以直接拿去做架构评审的参考框架。
2. 核心设计拆解:锁死与可插拔的辩证关系
2.1 锁死的是什么,可插拔的又是什么
聊具体技术之前,我先把边界划清楚。这套方案里有三个核心概念:底层内核、扩展契约、插件单元。
底层内核(我们内部叫Kernel)是整个系统的中枢,承载了三件事:生命周期管理、插件注册表、资源配额分配。它的代码量可以很少,但地位最高,不允许随便改动。一旦Kernel的API稳定下来并被外部插件依赖,那Kernel的代码版本就意味着不能乱动了。这里的“锁死”指的就是Kernel对外暴露的API签名、行为语义、错误码体系,包括接口的入参出参结构,全部进入一个“冻结”状态,后续只做兼容性修复,不做功能扩展。
可插拔的东西则是业务模块。比如一个数据脱敏模块、一个Webhook通知模块、一个自定义规则引擎模块,它们全部需要遵循底层的SPI(Service Provider Interface)标准来开发,各自独立打包,甚至可以独立版本,互不感知对方的存在。
这里很多人会犯一个错误——以为“可插拔”等于“随便插”。实际上,真正的可插拔建立在非常严格的标准化接口之上,你插的东西形状必须和插座完全匹配。我们的V4.0方案里,插件面向扩展契约开发,而不是面向Kernel开发。扩展契约是介于Kernel和Plugin之间的一层薄薄的接口层。
2.2 分层模型与依赖倒置
我把这种方法称为“依赖倒置的二次方”。
普通微服务里,服务A依赖服务B的SDK,这算一次依赖倒置,面向接口编程。但在插件系统里,光倒置一次还不够,需要倒置两次。
第一层倒置发生在Kernel和Contract之间。Kernel不关心Contract的具体实现,它只面向Contract接口编程,这样就可以在加载运行时传入任意符合Contract规范的实现。
第二层倒置发生在Contract和Plugin之间。插件开发者面对的是Contract接口,不面对Kernel的类库,这样插件并不能随意调用内核API,也不能通过非公开手段绕过内核,它的所有能做的事情都必须通过Contract接口暴露出来。
具体分层如下:
- Layer 0:Kernel核心层。生命周期管理、调度、安全校验、插件启动关闭。
- Layer 1:Extension Contract扩展契约层。一套经过严格评审的标准接口集合,比如
PluginActivator、ConfigResolver、EventHandler。 - Layer 2:Plugin实现层。你的业务代码所在的位置,是“Freestyle”发挥的空间。
- Layer 3:宿主应用层。无论是Spring Boot应用还是自研的并发容器,只负责嵌入Kernel并启动。
这种分层的精妙之处在于,Kernel在底层锁死之后,它的通用性反而变强了。你可以把Kernel想象成一个类似操作系统内核的微内核,它只负责进程调度和内存管理,并不关心用户是打开Word还是播放视频。业务形态的变化(相当于打开什么应用)全部通过插件来实现。
2.3 内核为什么不直接依赖业务模块
我经常在技术分享中打一个比方:如果你每天通勤的公交车路线把车上的每一位乘客的目的地都写进了调度系统,那只要有一个乘客改了行程,全车路线都得跟着改。我们传统的单体应用就是这个模式,业务模块和底层代码犬牙交错,任何一个业务逻辑的变化都可能导致底层的“重新编译和全量回归”。
V4.0的Kernel从一开始就不允许反向依赖任何业务模块。Kernel里没有“订单”“支付”“用户”这种概念,它只认识“插件A”“插件B”“插件C”,以及它们各自实现的接口类型。这样出现新业务模块时,Kernel不需要感知变化;老业务模块下线时,Kernel也不需要感知变化。所有变化都限制在插件世界的边界内部。
这样一来,锁死底层的代价被最小化了,因为它锁死的是通用机制,不是具体业务;而可插拔的能力被最大化了,因为业务模块的接入成本被降到了最低——大部分插件的接入只涉及继承一个基类加上实现两三个接口。
3. 实操环节:从接口设计到热插拔的全流程落地方案
3.1 插件接口的设计规范
接口设计是整套方案里最该磨的环节,没有之一。接口设计得好,后面所有流程都会很顺,接口设计得草率,后面每走一步都想推倒重来。
基于V4.0的实践,我总结了几个非常基础的接口设计原则:
第一个原则是接口要尽量少。很多团队在做插件化时,总想把所有能力都定义成接口,结果接口比实现类还多。我们实践下来的合理比例是,一个业务模块域最多2~3个核心接口,统一承载该模块对外暴露的全部功能。
第二个原则是接口要用名词命名,不要刻意加动作前缀。比如OrderQueryService而不是QueryOrderBusinessService,因为动词会变,而名词代表领域概念,稳定性高得多。
第三个原则是接口签名中尽量不要出现第三方框架的类型。比如别在接口参数里传另一个中间件专有的MessageContext,你无法保证所有插件开发者都熟悉那个框架,一旦代码里有这种类型,插件和中间件的耦合格外严重。接口的参数和返回值应该尽量是JDK原生的集合、POJO或自约束的Result对象。
我贴一段V4.0中比较标准的插件契约定义供参考,不过请注意这只是脱敏后的示意代码,真正落地时命名可以按自己体系来:
public interface ModuleActivator { void start(ModuleContext context); void stop(ModuleContext context); } public interface ModuleContext { <T> T getService(Class<T> serviceType); Registry getRegistry(); ConfigSource getConfigSource(); }这段代码看起来简单,但它非常克制。ModuleActivator只定义了生命周期影响,ModuleContext只提供了获取服务的能力,并没有把底层Kernel的几百个API直接暴露给插件。插件想发事件?可以,通过getRegistry()拿到事件中心;想读配置?可以,通过getConfigSource()拿到配置客户端。但因为上层的Kernel可以决定“怎么拿”,所以底层始终是可控的。
3.2 基于SPI机制的插件注册与发现
Java生态里最常用的SPI机制就是ServiceLoader,平时我们在框架里用得比较多,但在企业级插件系统里直接用原生的ServiceLoader容易碰见两个问题:一是插件实现类被重复加载,二是SPI文件散落在各个Jar包里,不好统一管理。
我们在V4.0中做的改进是:以META-INF/services为基准,但额外增加了一个配置文件的元数据描述。每个插件在开发时,除了需要放服务的候补列表,还需要额外提供一个plugin-descriptor.xml,描述插件的名称、版本、依赖的其他插件名、入口Activator类名、以及可扩展属性键值对。Kernel在启动时通过统一扫描器同时读取所有的SPI文件和描述文件,构建出一张完整的插件注册表。
每一张插件注册表都遵循标准的格式:
| 字段名 | 含义 | 是否必填 | 说明 |
|---|---|---|---|
| plugin.name | 插件唯一标识 | 是 | 不能与现有插件冲突 |
| plugin.version | 插件版本号 | 是 | 遵循语义化版本规范 |
| activator.class | 生命周期入口类 | 是 | 必须实现ModuleActivator |
| dependencies | 依赖的其他插件名 | 否 | 目前支持有向无环图结构 |
| order | 启动顺序 | 否 | 数字越小越先启动,默认按依赖顺序推导 |
这张注册表一旦生成,内核对所有的插件能力就有一个全量视图。后续不管是插件升级、下线还是热部署,都可以基于注册表做一个增量变更,而不是全量扫描文件系统和类路径,这个优化能让大项目冷启动时间缩短不少。
3.3 生命周期管理和热插拔实现
一个常见的误区是以为插件只能在系统启动时加载,启动之后就不能动了。其实热插拔的核心,就是一套安全、防泄漏的模块重生机制。
我们为每个插件设计的生命周期阶段是:INSTALLED(已安装) -> RESOLVED(依赖解析完成) -> STARTING(启动中) -> ACTIVE(运行中) -> STOPPING(停止中) -> UNINSTALLED(已卸载)。
- 升级插件时,系统先将该模块的其他所有依赖关系标记为“BLOCKED”,进入STOPPING状态;
- 通知所有正在调用它服务的线程走优雅降级逻辑,比如快速失败或者走缓存兜底;
- 等待在途请求处理完(我们设置了一个可配置的悬挂请求最长存活时间,默认30秒);
- 正式停止;
- 加载新的ClassLoader;
- 执行新版本Activator的start方法,将模块标记回ACTIVE。
这里面最麻烦的是并发请求的调度问题。我们引入了一套两阶段的流量切换方案:先切换控制面(停止路由发往旧模块的请求),再切换数据面(释放旧对象引用)。这套方案单独作为一个独立的调度组件,特意不依赖具体业务框架,可以嵌入到任何Spring Boot应用里。
3.4 双版本策略:编译期锁死与运行期可插拔的落地
“锁死”这个词在企业级技术体系里并不是一个绝对状态,它包含两层含义:一层是编译期的契约锁定,一层是运行期的约束锁定。
编译期锁死靠的是Maven/Gradle的依赖管理。我们把Kernel和Contract体系发布为一个独立的Artifact,比如com.company.kernel:kernel-api:4.0.0。所有插件项目在声明依赖的时候只允许引用这个Artifact,不允许引入Kernel内部实现包kernel-impl。我们在代码扫描规则中加了一条CRI规则:如果发现基础模块的Maven坐标出现在某个具体业务插件内部,或者被直接import了实现类,CI直接判定构建失败,这个靠纯人力review很难保证长期坚持,只有把规则固化到技术门禁里才会真正在团队内生效。
运行期锁死则依赖Java的ClassLoader隔离机制。每个模块加载到独立ClassLoader,插件之间不允许互相访问到对方的未被公开类。Kernel的类加载器只能加载Kernel和Contract相关类,插件类加载器的双亲委派机制被设置为只能加载插件自身的包路径,从而绕开系统路径。
这里有个比较容易踩的坑:如果你用了某个框架的Context来写插件逻辑,比如Spring的ApplicationContext,或者某一个ORM框架的SessionFactory,那你很难做到真正的锁死,因为插件之间会对同一份Bean定义膨胀竞争。我们的方案是每个插件只允许持有自己的独立ApplicationContext,不允许共享主应用的ApplicationContext,Spring的上下文冲突算是V4.0踩过比较深的坑之一,后面的常见问题部分会详细讲。
4. 工程化配套:怎么让锁死机制长期持续运转
4.1 代码质量门禁与自动化检查
架构方案不是画完图就算完了,如果执行层的开发人员不按规范来,“底层锁死”很快就变成一句口号。V4.0里我们做了一套自动化的架构守护体系,落地为五个方面的门禁。
- 依赖方向检查:使用ArchUnit类测试框架,自动断言
kernel-api不允许依赖任何plugin-*的包。 - 接口稳定型检查:凡是标了
@Stable的接口,不允许添加抽象方法。如果要加,必须把新方法放入新接口并继承旧接口。 - 循环依赖检查:在模块依赖图里执行DFS,发现环立即阻断发布。
- 磁盘Git提交钩子检查:在push阶段就做static analysis,避免等到CI阶段才报错,小步快跑。
- 版本号变动必须包含变更描述:如果某个插件升级了版本,必须同步更新changelog。
这里面最值得推荐的是ArchUnit。原来这套检查规则完全靠人肉在代码评审里讲,讲完一次团队记住了,过一个月又有新同学加入,规则就被忘记。现在ArchUnit直接把规则写成单测,跑一遍全量用例就知道规不规范。它有两个很实用的测试方法:classes().that().resideInAPackage(...).should().onlyAccessClassesThat().resideInAnyPackage(...),以及classes().that().haveAnnotation(...).should().haveOnlyFinalMethods()。
4.2 契约测试实践
接口层的稳定通常意味着不能随便改签名,但项目总是要演进的。为了在演进过程中不搞坏外部依赖,我们给核心接口加了独立的契约测试项目,里面跑的并不是常规的依赖测试用例,而是一组协议级别的测试用例。
比如,每个接口的返回对象都必须是Result包装类,这个包装类的字段结构必须满足预设的json schema,否则契约测试不通过。再比如,接口方法如果标注了@Nullable,那就不允许在契约测试中返回一个非空值实例,否则测试会将错误信息直接反馈给对应插件开发者。
契约测试的另一个作用是保护插件开发者的用户体验,我们自己做过统计:在没引入契约测试之前,插件作者在联调阶段经常因为底层接口行为不一致来来回回改代码;引入之后,一周内的开发工单量下降了接近四成。
4.3 文档与配置管理
架构落地的过程往往伴随着大量的文档编写。V4.0实践下来,我们的建议是采用两层文档体系:一层给模块继承方看,包括整体架构、模块依赖矩阵、核心接口的Javadoc;另一层给插件扩展方看,讲“如何以最小成本开发一个插件”。插件开发方只需要关注后者,界面不用面对特别庞大的体系,入口清晰很多。
配置管理方面,我们并没有做大而全的配置中心改造,而是直接在Contract层定了一个ConfigSource接口,插件可以通过这个接口读取自己的配置命名空间。配置被限制在插件自己的namespace内,不允许访问Kernel或其他插件的配置,这样既满足了插件的配置需求,又避免因为一个插件误删前缀而拖垮全局。
5. 常见问题与排查技巧实录
5.1 模块加载冲突
先问一个问题:你写了一个插件,里面引入了一个工具库的V2版,而核心系统里已经内置了同一工具库的V1版,会不会出问题?
如果你们用的是同一个ClassLoader,那么答案是:会出问题,因为类冲突会被触发。而且这种冲突的错误特别隐蔽,经常是运行几小时之后才在你完全没有预料到的地方抛NoSuchMethodError。
我们的解决方案很简单:经典的对第三方包做Shade重命名。插件在打包的时候,把它依赖的工具库的包名从org.apache.commons.lang3改成plugin1.shaded.org.apache.commons.lang3,然后放进插件自身的ClassLoader里。这样一个插件占用一个版本,其他模块用自己的版本,互不干扰。需要重点说明的是:重命名时要把对应文件下的resources配置也改干净,比如SPI文件里的全类名指向也必须同步替换。
5.2 循环依赖的检查与解法
插件A调用插件B的服务,插件B反过来又请求插件A的某个能力,这种关系如果在依赖配置里不控制好,最后会出现模块加载解析失败或者运行后频繁出现断链。
V4.0规定插件依赖必须是单射、无环的。依赖矩阵允许A依赖B,但B若依赖A则直接在构建阶段被安全门禁拦截。如果业务上确实存在双向调用的诉求怎么办?答案是公共能力下沉方案,把双方都要用到的公共接口提取到Contract层里,做成第三方小插件C,A和B都只依赖C,这样环就解开了。
5.3 热部署后旧实例被持有
这个算是比较隐蔽的坑。插件热升级之后,发现老版本的对象还在静态字段里被引用。排查时通过内存分析工具能看到老版本插件的Class对象一直存在,怎么回收都不掉。原因是有些框架的全局单例缓存了插件的ClassLoader导致的引用。
我们当时逐个清理了静态引用,把主要框架里常用的几个静态缓存都换成了WeakReference,并且封了一个内存泄漏监控的插件。这个插件周期性地通过堆快照分析自己的类加载器数量,超过阈值就告警,实测下来特别好用。
5.4 接口演进破坏的防御机制
假设底层的ModuleContext接口在2.0的时候定义了一个getConfig(String key)方法,到了3.0为了支持多环境配置改为getConfig(String key, String env),看起来只是加了一个参数,但这个改动会导致所有还在实现2.0接口的插件全部编译失败,属于典型的“破坏性变更”。
V4.0的做法是:给这部分后来不够合理的老接口专门建立一个不删不改的保留区,保留区接口只标记不推荐使用,同时定义一个新的扩展接口来覆盖新场景。新插件使用新接口,老插件可以继续运行,只是不能享受新能力。等到下一次大版本升级时(比如V5.0),再统一把老接口移除,这样能保证平时小版本的Kernel升级不会阻塞业务系统。
6. 团队协作模式与架构守护经验
6.1 架构委员会与技术评审流程
可插拔架构的应用通常需要团队具备高度的约定自觉。我们V4.0运作期间,专门设立了一个“架构评审小组”,小组每周花半天时间集中评审新插件和接口变更提案。
评审流程走三个环节:第一环节看“需求必要性”,确认这个能力是否值得做成插件;第二环节看“契约是否完整”,核对接口稳定性、资源开销预估和权限边界;第三环节看“兼容性验证”,确保它已经具备完整的自动化单元测试和契约测试。
这个评审本身不是为了卡进度,而是要逼着提案方把章节问题想在前面。不少团队直接让所有开发者随便建插件,最后主库没乱、插件库乱得不像样,还得回头补治理,反而浪费时间。
6.2 基线锁定与变更管理
“锁死”不等于“冻结一切改动”。我们为Kernel建立了一条稳定基线,每半年才会规划一次内核版本升级窗口。其他时间Kernel代码库只允许提交修复性改动,任何新增能力都被推到插件扩展机制或下一个窗口期再评审。
这个策略给了业务系统一个很清晰的心智模型:“只要我不升级Kernel,我的插件理论上永远不用改代码。”这对维护期业务系统的价值特别大,它可以完全不受内核迭代的影响,哪怕内核在提交线上有几十个新的Commits,老业务线还是稳如老狗地跑在旧基线上。
当进入新窗口期做内核升级时,团队内部需要执行一个完整的兼容性回归矩阵。每个插件最好都有契约测试来做自动化验证,没有自动化验证的插件会单独拉出清单,由对应负责人手工走查。
6.3 文化层面:从“自由发挥”到“自由但有边界”
最后想聊一个比较“软”的话题。Freestyle在嘻哈文化里代表的是即兴、无拘束,但在企业架构里,Freestyle如果过了头,往往会变成技术债的高发区。
我们在落地这套架构的过程中最大的收获,是把团队的工作模式从“自由发挥”调整为“自由但有边界”。插件可以在自己的小岛上随便盖楼,但岛和岛之间的航道、港口标准和电力接口必须是全岛统一的管理公约。很多开发同学一开始觉得“底层锁死”是一种限制,过了一段时间之后反而觉得舒服,因为边界清楚了,做决策的成本变低了,不用再去猜“这里改了会不会影响别人”。
所以如果你要引入这套模式,我的建议是:不光要发布技术规范文档,还要花时间把“为什么要有边界”这件事讲透彻,让大家从内心里认可,这套机制才能真正扎根落地,要不然留着规范文件,经常会在不显眼的地方发现“临时绕过架构做功能”的小技巧代码。
7. 实操总结与后续演进思路
7.1 马年版的落地位置
V4.0从正式定稿到现在,在我们自己的系统里经过了三个季度、两个业务大版本的验证。我们内部有两个系统完全基于这套架构重新改造完成:一个是跨平台的规则配置中心,另一个是通用性比较强的异步消息处理平台。两个系统的业务差异很大,但因为底层锁死,架构表现非常一致:核心代码几乎零改动,新增业务模块都是插件形式接入,一个模块的开发周期从原来的两周缩短到三天左右,联调成本下降尤其明显。
如果你想让这套方案在你的组织里落地,并不需要一步到位全部实现。可以拆分成三个阶段:
- 阶段一:先梳理现有的核心API,画出接口依赖图,确定哪些接口需要稳定保留;
- 阶段二:把确认保留的接口抽取成一组独立的Contract模块,并在依赖系统中固化规则;
- 阶段三:逐步迁移业务模块为插件形态,配上CI门禁和契约测试,形成完整的闭环。
这三个阶段每一个都可以单独发挥作用,不必等到全套齐了才收益。
7.2 后续演进方向
V4.0解决的是“静态锁死+动态扩展”的架构问题,但我们也看到了一部分待改进空间,正在往V5.0方向推进。
一个是动态契约协商能力。目前接口签名是编译期确定的,后续我们希望插件能向Kernel注册自定义扩展点,而不仅仅是Kernel单方面定义好接口等插件实现,这会进一步增强“Freestyle”的玩法。
另一个是弹性配额以与资源治理的精细化。目前插件能用的资源额度只能在启动时静态分配,未来希望做到基于实时负载动态调整配额。
还有就是云原生环境下的适配,特别是把Kernel放到容器里的场景,如何将插件的生命周期与Kubernetes的Pod生命周期做联动,做到插件级横向伸缩,这是我们在尝试的下一个前沿点。
7.3 一些真心话
折腾这套V4.0的整个过程,是我做技术架构这些年来比较过瘾的一段经历。尤其是看到十几个插件在同一个内核上各行其道,互不干扰又协同工作,那种“规则下的秩序感”会给人一种特别安定的愉悦。
如果你也打算在公司内部搞类似的架构,有几个比较实际的建议。第一,不要一上来就追求百分百抽象,先保留一小块“脏”区域作为实现验证。第二,给插件开发者写一份极简的快速上手文档,样例代码要能直接跑通,这比什么架构图都提升接入效率。第三,一定要有一个长期维护架构规范的人,专职或者半专职都行,因为这套体系最重要的不是一次设计,而是日积月累的守护。
这套方案不是万能药,它解决的是特定阶段的特定问题。但当你的系统真的膨胀到“改一处牵全身”的时候,回头来看这套“底层锁死 + 扩展开放”的路线,还是相当值得投入的。