我干了这么多年开发,最烦躁的事情之一就是看代码时发现同样的HTTP调用逻辑散落在十几个文件里,改个超时时间得全局搜索替换;或者某个存储工具类被六个业务模块直接new出来用,后来要加个监控埋点,硬生生改了八处才改完。这些痛点其实都指向同一个问题:工具封装没做好。
“工具封装”这四个字听起来很基础,但真正能在项目里把封装做到“改一处、处处生效、同事用得顺手”的,其实不多。它不是简单地把函数塞进一个类里就完事,而是一套关于边界划分、接口设计、异常处理、可测试性的综合工程决策。这篇就把我在实际项目里沉淀下来的一套工具封装方法论拆开讲透,从设计思路到代码落地,再到踩坑记录,一次性说清楚。
1. 内容整体设计与思路拆解
1.1 为什么要封装:先说清楚“解决什么问题”
很多人对封装的第一反应是“减少重复代码”。这个说法没错,但只说对了一半。如果一个工具函数只在两个地方用到,复制一份其实也没多大问题,封装反而可能过度设计。工具封装真正的价值,体现在下面三个层面。
第一层是统一变更入口。比如项目里对接了阿里云的OSS存储,最初只在用户头像上传那里用了SDK。后来订单模块也要传电子发票,支付模块要传回执单,三处代码各自写了上传逻辑。某天OSS的Bucket策略变了,需要统一在请求里加个Header,如果你有三处分散的调用,就得改三处,漏一处就是线上事故。但如果一开始就封装了一个StorageService,那只需要改这个Service内部的方法,所有业务方的调用全部自动生效。
第二层是收敛复杂性。一个直接的SDK往往暴露了几十个参数,但业务方真正关心的可能只有“把文件传上去,拿到URL”。封装可以把那些需要业务感知但不该业务关心的东西藏起来——比如连接池大小、超时重试策略、认证Token刷新机制。业务方不需要理解这些,他们只需要一个简单、符合直觉的方法签名。
第三层是可测试性。没有封装的代码,测试时你得真的去连OSS、真的去发HTTP请求,网络一抖动测试就挂。封装之后,你可以在测试里注入一个内存实现的存储服务,或者Mock掉底层客户端,测试变得又快又稳。项目能走多远,很多时候取决于测试好不好写,而测试好不好写,取决于封装边界清不清晰。
1.2 封装粒度与边界:哪些东西该被收进工具层
封装最怕的就是“一刀切”——把所有东西都往工具类里塞,结果是这个工具类又臭又长,几百个方法,谁也不敢动。我理想的粒度判断标准很简单:如果一个函数被三个以上不同业务场景复用,且它们对该函数的调用方式完全一致,那这个函数就值得被抽出来封装;如果一个函数只在一个模块内部使用,那它应该留在模块内部,不放进公共工具层。
举个例子。项目里有一个“将用户ID转换成脱敏展示串”的功能,A模块在用户列表页用,B模块在订单详情页用,C模块在客服工作台用。三个地方调用方式一致,输入用户ID,输出带星号的脱敏串。这种就非常适合做成公共工具方法。
但“将订单金额转换成中文大写”这种功能,如果只有财务模块用,那就应该放在财务模块自己的工具包里,而不是塞进全局的common包。全局工具包一旦膨胀,所有人都不敢改——因为不知道谁在用、怎么用,改出问题的风险太大。
边界问题还涉及另一个维度:哪些底层能力属于“基础设施”,哪些属于“业务能力”。基础设施包括HTTP请求工具、日志封装、JSON序列化、加密解密、ID生成器、分布式锁客户端、缓存客户端等,这些跟具体业务无关,属于技术组件的封装。业务能力则包括“发短信验证码”“生成分享海报”“计算运费”这类带业务规则的能力,它们虽然也可能被多个模块复用,但本质上是业务逻辑的一部分,应该待在业务服务层,而不是混进技术工具层。这条路划清楚了,后面设计代码结构才不会乱。
1.3 方案选型的底层逻辑:为什么是“组合优于继承”
在封装工具层的时候,我经常看到技术方案选择上的分歧。早期项目有人喜欢用继承:写一个BaseService,把通用方法都放进去,业务Service都继承它。这个方案在项目初期看起来很快,但问题会在半年后集中爆发——父类不断膨胀,子类被迫接收它们根本不需要的方法;父类的一个改动可能影响所有子类;而且某些业务Service本来已经有父类了,无法再继承你的BaseService。
所以我现在做工具封装,原则是组合优于继承,能静态的尽量静态,能实例化的就实例化,尽量不搞继承树。用组合的方式,我的Controller不需要继承任何BaseController,只需要通过依赖注入拿到一个HttpHelper或者StorageService实例。这样每个类的职责清晰,不被迫接收自己不需要的东西,测试也简单,构造一个类只需要传入它真正依赖的工具即可。
还有一个容易忽略的点是重试与降级策略适合放在封装内部,而不是留在每个调用方手里。比如调用第三方短信接口,失败后要重试两次,退避时间依次是500毫秒和2秒。如果这个策略写在每个业务Service里,那十处调用就有十份重试代码,而且策略漂移了根本没人知道。把重试收进封装的SmsClient里,调用方只需关心“send(mobile, content)”能不能成功,失败后的补偿逻辑由工具层统一负责,这也算是控制反转思想在工具层的一种体现。
2. 核心细节解析与实操要点
2.1 工具类的外观设计:方法签名与命名规范
工具封装的第一步是设计外观,也就是方法签名。这一步非常重要,因为它决定了工具类的易用度。
我总结了一条经验:方法签名力求表达意图,参数尽量少,可选项用参数对象收拢。比如“根据文件路径上传文件到OSS并返回URL”这个方法,如果写成String upload(String filePath, String bucketName, String objectKey, int timeoutSeconds, boolean overwrite),五个参数,调用方看了就头大——我到底需不需要传bucketName?overwrite传false是什么行为?
更合理的方案是把必填参数和可选项分开:String upload(String filePath),让工具内部根据业务上下文推导出bucketName和objectKey的默认策略,业务方绝大多数情况下只需要传一个文件路径;如果确实有定制需求,再提供一个带UploadOptions参数的扩展方法。这样对90%的调用方保持了极简的入口,对剩下10%的特殊场景保留了灵活度。
命名上也要下功夫。方法名应该是“动词+宾语”的结构,直接表达行为。sendSms、downloadFile、generateShortUrl这种一看就懂。我见过一个工具类里有process、handle、doAction这种毫无信息量的名字,调用方必须看源码才知道它是干嘛的,这种命名本身就是设计缺陷。
2.2 参数校验与异常设计:让调用方拿到清晰反馈
工具方法内部一定要做参数校验,不要相信任何调用方。我最常用的是Guava的Preconditions或者Java原生Objects,校验失败抛出IllegalArgumentException,消息写清楚是哪个参数不允许什么值。
异常设计这里有两个流派:一种是“工具层不抛受检异常,全部包装成RuntimeException”;另一种是“定义业务错误码,通过返回结果传递错误”。在工具封装这块,我倾向折中——基础设施类工具(比如HTTP、缓存)直接抛RuntimeException,上层通过全局异常处理器统一转成友好的错误响应;而业务语义较强的工具(比如“发送验证码”)建议返回一个Result对象,包含错误码和错误信息,方便上层做更精细的业务判断。
举一个实际例子。缓存工具封装的get(key)方法,如果底层的Redis连接失败,抛一个CacheException出去就完事了。上层业务可以决定是走降级逻辑还是直接报错。但如果这个工具是“获取用户收货地址”,底层缓存挂了之后理论上还可以查数据库,那就不该把缓存异常直接抛出去,而是应该在工具内部完成降级,返回最终结果。这就是“工具层透明度”的设计取舍,没有绝对标准,但必须有意识去做选择。
2.3 配置化与默认值:封装要“开箱即用”但不“黑盒”
好的工具封装不应该是黑盒——调用方不应该完全不知道里面发生了什么,也不应该被逼着去看源码才能使用。我的做法是:提供合理的默认值,同时允许通过配置覆盖。
比如HTTP客户端封装,默认连接超时设为3秒,读取超时5秒,最大连接数50。这些默认值是经过压测和线上监控数据验证过的,适合一般业务场景。但也要暴露配置项,让极端场景(比如导出大文件需要更长读取超时)可以通过配置中心动态调整。
默认值要写在常量类里,而不是散落在方法里。方便统一管理和审查。配置项优先用Spring的@ConfigurationProperties绑定,这样一组相关配置可以自动映射到对象里,不污染代码。举个例子,我封装一个“文件服务”,会建一个FileServiceProperties类,字段包括storage-type、local-root-path、oss-bucket、oss-endpoint等,然后在配置中心直接填一组前缀以file-service开头的配置就能生效。
这里有个小技巧:配置项名称尽量带上前缀,避免和别的组件冲突。一个项目里可能有多个HTTP客户端(一个是调用内部服务的,一个是调用外部开放API的),如果配置前缀都叫http,那俩客户端就会互相踩踏。我习惯用inner-http和external-http来区分,细节上就能避免不少线上问题。
2.4 监控、日志与链路追踪:封装里最容易漏掉的一环
很多工具封装教程不会教你这些,但真实项目中,监控和日志才是工具封装最大的隐形价值。一个封装好的工具,天然就是一个完美的埋点位置——所有流量都经过这里,你只需要在这一处加监控,就能全局掌握这个基础设施的健康度。
以HTTP工具封装为例,我通常会在内部做这几件事。第一,记录请求耗时,按目标服务名聚合,接入Prometheus的Histogram指标。第二,记录成功和失败的计数器,失败时额外记录异常类型。第三,打印访问日志,包含请求方法、URL、状态码、耗时、TraceId,方便排查问题时串联链路。第四,如果配置了链路追踪(比如SkyWalking或OpenTelemetry),在工具内部创建Span,把目标服务名作为Tag传下去。
日志打印这里有个度的问题。我见过一个项目,每调一次HTTP就要打印请求体和响应体的完整内容,日志量直接爆炸,磁盘IO被打满,线上服务变慢。我的经验是:默认只打印必要信息(方法、URL、状态码、耗时、错误摘要),请求体响应体只在Debug级别打印,并且要加开关控制。对于包含敏感信息(手机号、身份证、密码)的请求,即使Debug级别也绝对不能打印原始内容,要么过滤掉,要么脱敏处理。这是一条安全红线,写进代码Review的CheckList里。
3. 实操过程与核心环节实现
3.1 构建统一HTTP调用封装:从0到1完整演示
HTTP调用是项目里最基础也最常见的工具需求。我带一个团队,第一步动刀的就是这里。原始代码里,有的地方直接用原生HttpURLConnection,有的地方用RestTemplate,还有的地方用OkHttp,乱得不行。我们统一封装一个HttpClientService,内部用OkHttp作为底层引擎,对外提供简单API。
先看核心方法签名:
public class HttpClientService { // 发送GET请求,返回响应体字符串 public String get(String url, Map<String, String> headers); // 发送POST请求,支持JSON body public String postJson(String url, Object body, Map<String, String> headers); // 发送GET请求,自动反序列化为指定类型 public <T> T getForObject(String url, Class<T> clazz, Map<String, String> headers); }内部实现要点:
第一,所有请求共用一个OkHttpClient实例,该实例内部维护连接池和线程池。千万不能每次调用都new OkHttpClient(),那会不断创建线程和连接,迟早把资源耗尽。
第二,所有的超时设置从配置中心读取,实现动态调整。所以OkHttpClient不能直接通过构造方式创建,而是从配置构建。
第三,调用外部接口时统一添加一个X-Request-Id请求头,值是从TraceContext里取出来的TraceId。下游服务看到这个Header后,就能把两条链路串起来。
第四,响应处理统一判断:HTTP状态码2xx视为成功,其它异常状态码抛出统一的HttpApiException,异常消息带上URL和状态码,方便快速定位。
第五,重试机制默认关闭(因为GET幂等可以重试,POST不保证幂等,重试可能造成重复下单),但提供了enableRetry()方法,供明确指出“此调用幂等”的场景使用。
这样封装完之后,团队的代码风格立刻统一了。新同学进来,不需要掌握OkHttp的API细节,只需要会调httpClientService.get(...)就够了。排查问题时,只要查这个工具类的日志,就能看到所有HTTP调用的状态和耗时。
3.2 缓存工具封装:多级缓存与数据一致性处理
缓存这块,我们封装了一个CacheService,内部整合了Caffeine本地缓存和Redis分布式缓存。
设计逻辑是这样的:读数据时,先查本地缓存Caffeine,如果命中直接返回;如果本地没有,查Redis;如果Redis也没有,才回调业务函数去数据库加载;加载成功后回填到Redis和本地。写数据时,先写数据库,再删除Redis,最后清空本地缓存。
public class CacheService { // 带穿透保护的多级缓存读取 public <T> T getWithLoad(String key, long localExpireSeconds, long remoteExpireSeconds, Supplier<T> loader) { // 1. 查本地缓存 // 2. 查Redis(序列化使用JSON) // 3. 都未命中时调用loader加载数据库 // 4. 回填两级缓存 // 5. 返回结果 } // 主动更新缓存 public void put(String key, Object value, long expireSeconds); // 删除缓存,用于写操作后的缓存失效 public void evict(String key); }封装缓存工具时,最关键的是防止缓存穿透。我们用的方案是:当loader返回结果为null时,设置一个空值占位符到缓存里,TTL设为30秒。这样同一时间内大量并发请求同一个不存在的Key时,先到的请求会去DB查一次,其余请求在30秒内直接命中空值占位符,DB压力骤减。
缓存工具里另一个常见问题是序列化格式选择。Redis里存的究竟是Java原生序列化的二进制,还是JSON字符串,对后续跨语言消费影响很大。我们统一用Jackson序列化成JSON字符串存在Redis里,这个决定在后来数据要同步给数据分析组时省了大功夫——他们直接用Python就能解析,不用理会什么Java序列化版本问题。
3.3 存储能力工具封装:文件上传与路径规范
文件上传做工具封装,主要是为了统一路径规范和回调公共处理。最初的代码里,上传图片的URL直接暴露了OSS的Bucket名和ObjectKey,用户猜一猜就能遍历别人的文件。封装后,我们在存储层统一做三件事:
第一,生成不可猜测的ObjectKey。规则是/{业务域}/{yyyyMMdd}/{uuid}.{ext},不包含任何用户ID这样的明文信息,保证文件URL通过未授权方式不可枚举。
第二,回调统一的文件访问日志和类型校验。上传时校验文件扩展名和MIME类型,黑名单里有exe、sh、jsp这类潜在危险文件,一律拒绝。
第三,URL生成方式集中管理。私有读写的文件不能直接返回公网URL,而是生成带签名、有时效的临时URL。封装后,业务方只需要调用storageService.getTemporaryUrl(fileKey, expireMinutes),底层是OSS的签名URL还是本地的临时Token,对上层彻底透明。
public class StorageService { // 上传本地文件到存储服务 public FileMeta uploadFile(String businessDomain, MultipartFile file); // 生成临时访问URL public String getTemporaryUrl(String fileKey, int expireMinutes); // 删除文件 public void deleteFile(String fileKey); }这个封装的收益很直接:后来公司要切换存储服务商(比如从OSS换到S3兼容的开源存储),只需要改StorageService这一个类的内部实现,所有业务方代码零改动。这比我之前见过的那种“在十个Controller里直接调OSS SDK”的写法,运维工作量少了一个数量级。
3.4 ID生成器与分布式锁:高并发场景的工具封装
高并发项目里,ID生成和分布式锁属于典型的“看起来简单、做起来全是坑”的工具。我们封装的IdGenerator支持三种策略:UUID(适合唯一标识但不适合做数据库主键)、雪花算法(适合需要趋势递增的分布式主键)、Redis自增(适合有业务含义的序号)。
雪花算法封装时要处理的一个大坑是时钟回拨。机器发生时钟漂移时,如果生成的ID比之前小,可能导致发号重复。我们的处理方式是:本地记录上一次生成ID的时间戳,当检测到当前时间小于上次时间时,先尝试睡眠等待时钟追平,如果等待超过5毫秒仍然回拨,则从配置中心获取一个临时序列号偏移,绕开冲突窗口。
分布式锁的封装也值得一提。直接使用Redis的SETNX做锁,表面上实现了互斥,但没考虑锁过期后其它线程可能进入临界区,或者锁没有续期就自动过期导致并发穿透。我们的DistributedLock封装了Redisson的看门狗机制,同时对外接口很简单:
public <T> T executeWithLock(String lockKey, long waitSeconds, Supplier<T> action) { // 1. 获取锁(阻塞等待waitSeconds) // 2. 拿到锁后执行业务action // 3. finally中释放锁 }这个接口设计有个好处:调用方根本不需要关心锁的获取和释放细节,只需要表达“我要在拿到锁后执行这段代码”。把模板方法收进工具里,业务侧代码干净很多。
4. 常见问题与排查技巧实录
4.1 “工具类不生效”的几种典型场景
实际开发中,很多同学会来问我:“我这个封装的工具类,改了之后好像没生效,怎么回事?”我总结下来,最常见的原因有三个。
第一个原因:多个实例并存。工具类被new出来而不是注入Spring容器的,导致配置中心更新后,旧的实例仍然持有旧配置。排查方法是看代码中是否有new XxxService()这样的写法,有就改成依赖注入。
第二个原因:方法重载参数歧义。工具类提供了sendSms(String mobile, String content)和sendSms(SmsRequest request)两个方法,调用时传了一个null进去,编译器懵了,选了错误的那个,然后NPE。解决方式是去掉可能产生歧义的重载,或者调用时显式强转。
第三个原因:包扫描路径不一致。工具类放在com.company.common包下,业务Application的@SpringBootApplication默认扫描的是com.company.web包,结果工具类根本没被扫描进容器,注入时直接报NoSuchBeanDefinitionException。排查这类问题,启动日志里看Bean定义是否注册,一眼就能定位。
4.2 封装升级引发线上故障的教训
有一次我们升级了HttpClientService的底层OkHttp版本,从3.x升到4.x,结果线上出现了一系列Connection reset错误。当时查了很久才定位到原因:OkHttp 4.x默认启用了HTTP/2多路复用,而某个下游老系统只支持HTTP/1.1,且对连接复用策略处理有问题。虽然OkHttp会自动降级到HTTP/1.1,但在特定网络环境下仍会触发异常。
这次事故给我的教训是:工具封装升级时,不能只看单元测试通过,必须跑一轮完整的回归测试,尤其是要覆盖老业务场景。从那以后,我们给所有核心工具封装都建了一个“兼容性回归清单”,每次升级前先用这个清单在老环境里跑一遍,再决定是否上生产。
类似的问题还有降级Redis客户端版本、升级Jackson版本导致序列化格式变化等,都踩过坑。升级序列化库时尤其要小心,JSON的字段顺序、特殊字符处理、BigDecimal精度的序列化方式都可能在不同版本间产生变化,表面上没报错,但数据语义变了。
4.3 工具封装中的性能坑:你以为的“公用”其实很昂贵
还有一个性能层面的坑。有人封装了一个“判断字符串是否为合法手机号”的静态工具方法,内部直接Pattern.compile(regex).matcher(phone).matches()。这个写法在单次调用上没毛病,但如果是每秒几十万次的校验场景,反复编译正则表达式就会造成严重的CPU浪费。正确做法是把Pattern缓存成static final常量,只在类加载时编译一次。
这类问题在封装层特别容易出现,因为工具方法一旦被复用,调用量往往远超预期。我把常见性能坑列成了一张排查表:
| 场景 | 错误写法 | 正确写法 |
|---|---|---|
| 正则校验 | 每次调用都Pattern.compile | 将Pattern编译为static final |
| ObjectMapper实例 | 每次调用new ObjectMapper() | 全局复用单例 |
| 格式化日期 | 用SimpleDateFormat共享实例 | 用DateTimeFormatter(线程安全) |
| 数据库连接 | 每次操作新开连接 | 通过连接池复用 |
| 调用底层SDK | 每次调用都创建客户端 | 复用全局客户端实例 |
这些坑的共性是:工具层是公共汇聚点,任何低效实现都会被放大倍数。所以工具方法在写完时,顺手想想这段代码如果被调用十万次会发生什么,很多性能问题就能在写代码阶段预防掉。
4.4 命名冲突与语义漂移:维护久了最容易出的问题
工具封装还有一个看不见的敌人,就是“语义漂移”。最初DateUtils.format方法的含义是“将Date转为yyyy-MM-dd HH:mm:ss字符串”,后来某位同学为了满足自己的需求,把方法改成了传什么格式就按什么格式输出,但方法名没变。结果原有调用方没注意这个行为变化,上线后发现所有日期显示格式都不一样了。
防止语义漂移,我的做法有三条。第一,工具方法的Javadoc上明确写明输入输出约定和示例,代码Review时重点关注工具类改动,要求改动者必须更新Javadoc。第二,为工具方法编写单元测试作为契约守护,改动方法必须同步改测试,测试就是活的文档。第三,新需求尽量新增方法而不是修改旧方法,旧方法保持对老调用方的兼容性,即使实现方式不同也不改变外部表现行为。
另外还有一个很细节但很常见的问题:切勿在静态工具类里保存可变的全局状态。静态Map、静态List如果被多个线程同时写入,轻则数据错乱,重则内存泄漏甚至死循环。工具类最好是无状态的,如果需要保存状态,那就设计成实例化类,交给Spring容器管理,不要图省事用static。
5. 工具封装的测试策略与工程落地
5.1 为工具层写“契约测试”而非“实现测试”
工具封装层的测试策略,我建议以契约测试为主,也就是不关心内部怎么实现,只关注“输入什么数据,应该得到什么输出”。
比如缓存工具,我可以集成一个本地Redis(或者用Testcontainers起一个Redis容器),测试put之后能正常get回来;再测试getWithLoad在缓存不命中的情况下会调用loader,并且返回后缓存被填充。这些测试不关心Redis客户端具体发什么指令,只关心工具层对外承诺的行为。
对于HTTP工具,测试时要特别注意Mock网络行为。Java生态里最常用的就是MockWebServer(OkHttp官方测试组件),它可以起一个本地Socket服务,然后断言工具发出去的请求行、请求头、请求体是否符合预期,同时返回你预设的响应。这样一来,测试HTTP封装时不再依赖真实外部服务,跑得快还稳定。
契约测试的好处在于:它能捕捉到“输出不符合约定”的问题,而不是“实现和之前不一样”的问题。这两个在工具封装里区别极大。假设某个方法内部从使用List改成了Set,行为上可能会变(比如顺序变了),但如果你的测试断言的是“包含哪些元素”而不是“元素顺序是什么”,那测试就还能通过。反之,如果方法输出格式从日期变成了时间戳,测试立刻变红。这正是我们要的效果——工具层对外契约稳定,内部实现可以自由演进。
5.2 封装代码的可观测性:不要把远程调用变成黑洞
工具封装得再好,如果内部发生问题无法被观测到,那对运维来说就是黑洞。我的标准实践是:为每一个核心工具封装建立最小指标集合。
HttpClientService:请求总数、成功率、P99耗时、按目标域拆分的失败数CacheService:命中率(区分本地/Redis两级)、回源DB次数、Redis平均延迟StorageService:上传文件数、上传字节数、上传失败率、临时URL生成数IdGenerator:生成速率、时钟回拨次数DistributedLock:获取锁失败次数、等待耗时分布、锁丢失告警次数
这些指标接入Prometheus之后,配几条关键告警:比如HTTP客户端P99超过阈值、缓存命中率持续低于某个百分比、分布式锁等待超时次数突增等。工具封装层有了这些指标,相当于给你的基础设施装上了仪表盘——哪天某个下游服务变慢了,先看工具层指标就能定位到影响范围,而不是挨个查业务代码。
5.3 封装在项目里的落地节奏:别想一口吃成胖子
最后聊一个工程落地层面的经验。很多团队一上来就喊“全项目统一封装所有工具”,然后排期三个月重构。这种做法的风险在于:改动面太大,回归成本极高,一旦出问题很难定位。更重要的是,团队对封装的边界理解还没有统一,强行推进会在代码评审时吵得不可开交。
我的建议是从“痛点最痛的模块”开始切。先找一个当前使用方式最混乱、bug率最高、维护成本最大的工具场景(通常是HTTP调用或者缓存使用),完成封装和切换,沉淀出模板和规范。这个回合并不追求覆盖全项目,但要明确封装风格、异常规范、配置方式、测试方法。等合并这些决议落地之后,再逐步推广到其它模块。
推广过程中要注意兼容已有调用方。我曾经在一个项目里把SmsSender从静态方法改成Spring Bean,然后全项目几十处调用点一次性改完,结果上线那天有几个调试点漏改了,电话被打爆。如果当时采用“先新增Bean,保留老静态方法作为转发”的平滑迁移策略,就不会出现那种局面。
实际经验告诉我,工具封装是“越到后期收益越大”的投资。前期大家都觉得重复写几行代码也没啥,但等系统规模上来之后,统一的工具封装能帮你省下大量排障时间,也让新同学更快上手。我个人现在写代码时,只要发现某个逻辑要在第三处地方复用,就会认真考虑抽象封装。不是为了显得专业,就是为了以后少熬夜排查问题。
另外再分享一个工作习惯:我在做工具封装时,会把每次“因为封装不到位而导致的线上问题”记进一个小小的“后悔清单”,后面写代码时翻出来看,提醒自己这里别再图省事了。这个习惯帮我避开过很多后来可能爆掉的坑。如果你也在为项目中重复代码多、改动一处牵一发动全身而头疼,希望这篇能给你一个相对完整的封装思路,动手之前先想清楚边界和契约,远比多喊几遍“要封装”有价值得多。