组件设计的方法——一个老程序员是怎么设计通用组件的 组件设计的方法——一个老程序员是怎么设计通用组件的不是请照着这个规范写代码。是一个在项目里做了十几年组件的人总结出来的六个设计决策——接口优先、学已有的、元数据驱动、安全内置、信创默认、消费者不感知。每个决策都有一个具体的实现案例。文章目录组件设计的方法——一个老程序员是怎么设计通用组件的一、接口优先——先定契约再写实现二、学已有的成熟方案——不要重复发明轮子三、元数据驱动——可变部分配成数据不变部分写成逻辑四、安全是基础设施——在设计阶段就嵌入不是在开发完成后补五、信创是默认支持——不要等甲方说了再改六、消费者不感知——SPI扩展点让消费者自定义但不改组件源码七、六个决策的优先级八、结语一、接口优先——先定契约再写实现不管你用什么框架、什么数据库、什么加解密算法实现了接口就能接入。消费者不用知道内部怎么实现只依赖接口。publicinterfaceEaConnectionProvider{ConnectiongetConnection();booleanhasTransaction();voidbeginTrans();voidcommit();voidrollback();}publicinterfaceEaConfig{StringgetEncryptAlgorithm();StringgetEncryptKey();StringgetSignAlgorithm();StringgetPrivateKey();StringgetPublicKey();EncryptStrategycustomEncryptStrategy();SignStrategycustomSignStrategy();}EaConnectionProvider把从哪拿数据库连接从组件内部剥离。JSP环境走BrowiseEaProviderSpring Boot环境走SpringBootEaProvider。组件的核心逻辑不动只换外围适配器。接口优先的核心不是写一个interface是把组件不敢替消费者做决定的事情全部暴露成接口。组件不知道你用什么框架拿连接、不知道你的密钥存在哪——它只知道我需要一个连接对象、“我需要一个密钥”至于这个对象从哪来它不关心。你把这些问题交给消费者自己决定组件就不需要承载任何环境的假设。二、学已有的成熟方案——不要重复发明轮子Activiti的自动建表是一个设计范本。它在首次初始化时检测act_ru_task等25张表是否存在不存在则根据数据库方言自动执行建表脚本。组件上了这个方案publicenumDatabaseType{ORACLE,MYSQL,GBASE8S,GBASE8A,DM,KINGBASE;publicstaticDatabaseTypedetect(Connectionconn)throwsSQLException{StringproductNameconn.getMetaData().getDatabaseProductName().toUpperCase();if(productName.contains(ORACLE))returnORACLE;if(productName.contains(MYSQL))returnMYSQL;if(productName.contains(GBASE))returnGBASE8A;if(productName.contains(DM))returnDM;if(productName.contains(KINGBASE))returnKINGBASE;thrownewRuntimeException(不支持的数据库: productName);}}启动时自动检测不存在就执行DDL。升级也走同一个机制——schema_version表记录当前版本检测到新版本自动跑增量升级脚本。学已有的方案不是抄袭是别人的东西已经在生产环境里跑了五六年没出过事——你的组件为什么要冒自己原创的风险。从可靠方案中提取方法论、去掉你不想要的部分、加上你业务需要的东西。这不叫模仿这叫工程判断力。三、元数据驱动——可变部分配成数据不变部分写成逻辑引擎核心里同一个数据库查询操作不同的需求变化全在配置表里的几个字段定义——分页、排序、条件拼接、数据格式都不一样。-- sqlDefinition: SQL语句定义不变的是查询引擎-- fieldDefinition: 字段定义可变的是哪个列、什么类型、是否必录、是否加密-- conditionDefinition: 条件定义可变的是什么条件、用什么操作符-- 消费者调用queryEngine.query(GET_USER_LIST,Map.of(sfzh,310...));-- 组件内部读sqlDefinition→fieldDefinition→conditionDefinition自动拼装SQL同样的查询引擎不同的SQL编号对应完全不同的业务场景——一个是按身份证查参保人一个是按部门查缴费记录一个是按时间范围查发放流水。这三条SQL的字段不同、条件不同、排序不同但同一个query()方法全部处理。这个思路和你之前的PB高级查询、XML生成、接口配置表一样——把可变的部分存成数据把不变的部分写成逻辑。当你发现自己在重复做同一种模式的事它不是简单的代码复用是设计层面的模式提取。四、安全是基础设施——在设计阶段就嵌入不是在开发完成后补组件的安全不是一个额外的模块。在设计阶段就嵌入了元数据-- fieldDefinition 字段表每个字段都有安全标记needEncrypt-- 是否加密Y/NneedSign-- 是否签名Y/NneedMask-- 是否脱敏Y/NmaskRule-- 脱敏规则如3,4表示保留前3位后4位执行流程里安全步骤是自动的——不是业务代码里写一行encrypt()调一句sign()ResultSet → needEncryptY → 解密 → needSignY → 验签 → needMaskY → 脱敏 → JSON每种操作独立判断——这一步要不要解密、下一步要不要验签、最后要不要脱敏每一层只检查一个标记位。新增一个加密字段只改元数据里的一条needEncrypt——代码不动。安全的成本不是在用了什么算法上是在你加一个安全措施要改多少代码上。如果加一个加密字段要改Controller→Service→DAO三层半个月后项目经理说这个字段不用加密了你又要回退三层——那你不是在加安全是在给自己挖坑。安全作为基础设施加一个字段只是加一行配置你才敢在产品交付后如实告诉审计每一个敏感字段都有保护措施。五、信创是默认支持——不要等甲方说了再改组件的DDL建表脚本直接覆盖了六大数据库meta.oracle.create.sql meta.mysql.create.sql meta.gbase8s.create.sql meta.gbase8a.create.sql meta.dm.create.sql meta.kingbase.create.sql启动时DatabaseType.detect()自动识别当前连的是什么数据库自动加载对应的DDL脚本。消费者不用知道这个组件在Oracle上能不能跑也不用自己写建表SQL。信创支持不是在项目做了九个月后甲方说下个月上达梦才开始改。不是事后补丁是设计的时候就把这个组件必须能跑在Oracle、MySQL、达梦、金仓上当成基本约束。这个约束一开始就定下来后面的每一步实现都自然会考虑多库兼容——不会有这个SQL是Oracle特有的写法、这个函数达梦不支持的问题。六、消费者不感知——SPI扩展点让消费者自定义但不改组件源码组件内置了SM4/SM2/SM3但不绑定它们。通过SPI暴露扩展点// 消费者自己实现加解密策略publicclassMyAesStrategyimplementsEncryptStrategy{publicStringencrypt(Stringplaintext){/* AES加密 */}publicStringdecrypt(Stringciphertext){/* AES解密 */}}// META-INF/services/ 注册一行// com.xxx.MyAesStrategyServiceLoader在启动时自动发现所有实现。消费者想用AES替代SM4——实现接口、注册SPI、配置文件指定算法名。组件的核心逻辑不动安全算法完全可替换。组件内部的异常体系覆盖了加密失败、验签失败、SQL超时、参数校验失败、元数据配置错误、事务异常六种错误类型。消费者try-catch之后拿到的不是通用的异常类是每个错误都有独立的code和message——知道是哪类错误、哪个参数出了问题、是配置错了还是数据错了。七、六个决策的优先级优先级决策一句话1接口优先组件不敢替你做决定的事全暴露成接口2学已有的从成熟方案中提取方法不在错误的地方自己原创3元数据驱动改配置不动代码当你第三次写同样的逻辑时停下来4安全内置在设计阶段就嵌入安全到元数据不是开发完成后补5信创默认设计就把多库兼容当成基本约束不做事后补丁6消费者不感知SPI扩展让你自定义但不改源码八、结语组件设计不是写一个通用的工具类。是做六个决策——接口怎么定、哪些东西学别人的、什么变什么不变、安全什么时候做、信创是不是默认、消费者能不能改。这六个决策不是在组件做完之后才写的总结——是在写第一行代码之前就已经定下来的约束。我的体会是只要你在写第一行代码之前把约束定清楚了后面所有的实现都是这六个决策的自然推导。反过来如果你先写着再说写到一半突然发现这个组件好像不支持达梦再从代码里找哪些地方要改——改到最后你会后悔为什么一开始没想清楚。