SPI

为什么是 SPI 做框架开发时,经常需要让外部团队在不改源码的情况下插入自定义逻辑。Java 的 Service Provider Interface(SPI)正是解决这一痛点的经典方案:框架只暴露接口,具体实现由使用者提供,双方通过约定的加载机制解耦。 下图展示了 SPI 的角色关系: 框架接口 --> 服务提供者实现 --> 应用在运行时按需调用 | | | API 定义 resources/META-INF/services ServiceLoader 按需加载 框架负责定义接口与约定,提供者将实现类打包好,ServiceLoader 在运行时装配,两端互不感知彼此细节。 SPI 与 API 的区别 API:框架写好实现,并通过方法开放给调用方使用。 SPI:框架只制定契约,由外部提供实现,框架在运行时回调实现。 因此 SPI 更适合“我要在某个节点上让别人接管”的场景,比如数据库驱动、日志实现、路由策略等。 Java 里的落地步骤 定义接口:在框架模块中写出需要开放的契约。 制定注册文件:在 resources/META-INF/services 目录下,新建以接口全限定名命名的文件,内容是实现类的全限定名。 加载实现:使用 ServiceLoader.load(Interface.class) 遍历可用实现。 按需调用:框架可以根据配置挑选实现,或全部加载做聚合处理。 代码示例: ServiceLoader<MyExtension> loader = ServiceLoader.load(MyExtension.class); for (MyExtension extension : loader) { extension.handle(request); } 这种方式天然支持多实现并存,也可以搭配权重、标签等策略做更细粒度的路由。 经验分享 约定胜于配置:接口命名尽量描述清楚输入输出,让实现方知道该承担的责任。 尽早校验实现:在框架启动阶段做初始化测试,确保缺失依赖时能及时报错。 限制加载范围:按需使用自定义的 ClassLoader,避免整个应用 ClassPath 都被扫描,启动速度更可控。 配合缓存:ServiceLoader 支持懒加载,常驻的服务实现可以二次封装成缓存,减少重复实例化。 适用与注意事项 SPI 并非银弹。它适用于以下场景: ...

2023年7月19日 · 1 分钟 · 82 字

表驱动法

表驱动法是什么 表驱动法(Table-Driven Design)本质上是“以数据替代条件判断”。当规则数量不断增加、条件分支越来越难维护时,将规则整理到表中,再根据输入做查找,可以明显降低代码复杂度。 在业务系统中常见的例子包括:状态迁移表、权限矩阵、优惠券策略映射等。接下来用三个小程序演示这个思路。 案例一:石头剪刀布 传统写法往往是一串 if/else 或 switch。使用表驱动法,只需维护一份胜利映射: static const uint32_t WINNING_MOVES[] = { 0, // 占位,下标从 1 开始 SCISSORS, // ROCK 胜过 SCISSORS PAPER, // SCISSORS 胜过 PAPER ROCK // PAPER 胜过 ROCK }; bool player_wins(uint32_t player, uint32_t opponent) { if (player == opponent) { return false; // 平局视为未胜 } return WINNING_MOVES[player] == opponent; } 一旦要扩展玩法(例如加入“蜥蜴、斯波克”),只需补充映射表即可。 案例二:分数评级 查表法让分数转换成等级更直观: char make_grade(uint32_t score) { if (score > 100) { return 'X'; } static const char GRADES[] = { 'F','F','F','F','F','F','D','C','B','A','A' }; return GRADES[score / 10]; } 数组下标对应分段区间,使得规则一目了然。 案例三:月份天数 结合闰年判断,也可以把每月天数放进表里: uint32_t days_in_month(uint32_t year, uint32_t month) { static const uint32_t DAYS_COMMON_YEAR[] = { 31,28,31,30,31,30,31,31,30,31,30,31 }; static const uint32_t DAYS_LEAP_YEAR[] = { 31,29,31,30,31,30,31,31,30,31,30,31 }; bool leap = (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); return leap ? DAYS_LEAP_YEAR[month - 1] : DAYS_COMMON_YEAR[month - 1]; } 通过两份数组快速选取结果,避免在每次判断中重复写闰年的逻辑。 使用心得 先画表,再写代码:把所有输入、输出、边界情况列出来,确认是否覆盖完整。 表结构易于维护:使用配置文件或数据库存储规则,支持非工程师的运营同事调整。 注意缓存与性能:大型表可以放在内存中,或搭配哈希索引来加速查询。 保持单一职责:逻辑层只负责查表,不要混入过多的业务判断。 适用场景 多条件组合导致的“条件爆炸”。 规则相对稳定,但需要频繁调整内容。 需要将规则交给业务人员维护。 总结 表驱动法不是新的概念,却是控制复杂度的可靠武器。将分支逻辑转化为数据结构,再通过查表解释业务规则,代码更短、更易测、更容易交接。 ...

2021年12月14日 · 1 分钟 · 153 字