ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

多项目共用代码治理解析:从抽取判断到版本管理

多项目共用代码治理解析:从抽取判断到版本管理 共用代码处理这件事听起来人人都懂真正做起来却经常变成一团乱麻。项目一多同一个工具函数、同一套组件、同一段配置逻辑在不同仓库里出现好几个版本改了一处忘了另一处最后谁都不敢轻易动。这篇文章想聊聊我在多项目、多团队场景里处理共用代码的一些实际经验包括什么时候该抽、用什么方式共享、版本和接口怎么管、出问题先查哪里。适合正在做多项目开发、准备抽公共模块或者已经被共享代码带来的变更搞得有些头疼的人看。我会从最基础的判断讲起不预设你已经用了 Monorepo 或私有依赖包。下面的内容更接近一套可以照着自己项目过一遍的检查思路不一定每条都适用但至少能帮你在“抽还是不抽”“用哪种方式”“怎么管版本”这几个问题上少一些反复。1. 共用代码为什么会从“方便”变成“负担”1.1 先问一句你现在的共用代码是怎么来的大部分共用代码不是一开始规划出来的而是从复制粘贴长出来的。项目 A 写了一个日期格式化函数项目 B 也遇到同样需求直接把函数复制过去。当时没有问题因为两个版本几乎一模一样。问题出在第三次复用之后。项目 C 复制的是项目 B 的版本而项目 B 已经改过几次参数项目 A 还停留在最初状态。这时候如果有人说“把所有项目里的公共函数统一一下”你要面对的不只是合并代码还有不同参数风格、不同错误处理、不同依赖版本。很多团队在这个阶段选择了“全部重写”结果重写后的模块又要兼容所有历史调用方。所以我一直觉得处理共用代码的第一步不是写代码而是先搞清楚现状哪些逻辑在多处存在各自的差异在哪哪些项目还在使用。现状不清楚抽公共模块就是往一个坑里填另一个坑。1.2 共用代码失控的几种典型信号判断一个项目是否已经因为共用代码失控看几个信号就够了。第一个信号是“同一段逻辑出现多个版本”。比如某个状态判断、某个金额计算、某个加密逻辑搜索一下能搜出三份以上实现而且细节不一致。这个信号最常见也最危险因为业务规则不一致会导致数据对不上。第二个信号是“改需求时不知道要改几个仓库”。明明是一个简单的字段变更你要先回忆哪些项目用过这个函数再一个个全局搜索。漏改一个线上就会报错。第三个信号是“公共代码的变更没人敢合”。只要一改动不知道会影响哪些调用方也没有自动化测试兜底所有人只能靠人肉验证。这个时候表面上是代码问题实际上是共享机制和验证机制没有建立起来。如果你发现项目已经出现以上任意一种信号那就可以认真考虑把共用代码单独管理了。但先别急不是所有代码都适合抽出来下一节说判断标准。2. 动手抽公共代码之前先判断三个问题2.1 稳定性判断这段代码被改动的频率有多高抽共用代码最怕抽到“天天变”的逻辑。如果一个函数本周还在算固定比例下周就变成按城市动态配置下个月又要接第三方灰度开关那把它塞进公共包只会让发布流程变得很痛苦。因为公共包的改动会影响所有依赖方每次改动都要考虑兼容、发布、消费方升级代价远高于直接在各项目里改。我一般会把代码按改动频率分三类长期稳定、迭代缓慢、频繁变动。长期稳定和迭代缓慢的逻辑比如基础校验、通用加密、日期格式化、通用字典转换适合抽到公共层。频繁变动的逻辑只要不是确实需要强一致建议留在业务项目里或者用配置中心、规则引擎等方式处理不要为了复用而硬抽。判断标准其实很朴素如果你不确定这段逻辑下个月会不会改就先别抽。把易变的代码放进公共模块是很多团队后来被迫做大规模重构的主要原因。2.2 通用性判断是业务规则还是通用工具第二个问题是通用性。很多人会把“项目 A 里的一段复杂逻辑”误当成“通用逻辑”。比如某个订单状态的流转判断看起来很适合复用实际上里面藏着项目 A 特有的优惠策略、审核流程、短信通知配置。抽成公共模块之后项目 B 根本用不上或者为了用它还要传一堆无意义的参数。正确的做法是先做纯函数化再做公共化。也就是说把输入输出尽量收紧让方法不依赖全局状态、不直接读数据库、不依赖具体业务表结构然后再判断它是否值得放到公共代码里。如果一段代码需要同时依赖好几个业务模块那它大概率不是公共代码而是一个聚合服务。这里面还有一个容易踩的坑公共模块一旦带了业务语义版本升级就会变成业务联调而不是单纯的技术升级。比如公共包里有一个“用户等级计算”函数听起来通用实际上每个项目的等级规则不同。要避免这种情况最好的办法是在命名和模块划分上就刻意区分通用工具和业务组件。2.3 复用人数和消费方数量决定共享方式第三个问题也是最容易被忽略的这段代码到底有几个项目在用未来可能几个项目用只在一个项目里用那就不要抽留在原项目里即可。两个项目在用可以用相对稳定的复制方式也可以考虑轻量共享。三个以上项目或团队长期维护才值得投入私有依赖包、子模块或独立代码库。消费方太少时建立一整套发布、版本、回归流程成本是收不回来的。比如只有两个项目用到一个工具函数却要为它单独维护仓库和 CI反而增加了工作量。反过来当消费方超过五个复制粘贴的方式一定会造成版本漂移回归验证也会失去基础。我建议在项目启动早期用一个简单的表格记录候选共用代码代码名、使用方、改动频率、是否涉及业务语义。列表不用很复杂但是能帮你在“要不要抽”这个决定上减少很多主观判断。3. 几种常见的共用代码组织方式怎么选3.1 复制粘贴与相对路径适合什么场景先说实话复制粘贴不是完全不可接受。在两个项目、代码量很小、改动频率极低的情况下复制一份是效率最高的方案。问题是很多人没有意识到它需要额外成本每次源端改了目标端也要同步改而且这种同步非常依赖记忆和检查。如果你决定用复制方式我建议做两件事。第一在复制过来的文件头部注释里写明来源和同步日期让后来的人知道这是一份共享副本。第二尽量在两边写同样的单元测试这样即使逻辑不同步至少能靠测试暴露行为差异。相对路径共享也比较常见比如在前端工程里直接引用上级目录。这种方式可以避免复制但没有版本概念。今天你本地改了明天 CI 拉取的时候可能还是旧版。如果只是几个人在自己机器上开发可以用一旦进入持续集成、多环境发布这种模式会很快失控。3.2 Git Submodule 与子树合并Git Submodule 曾经是很多人想到共享代码时的第一反应。它把一个独立的 Git 仓库挂到另一个仓库的子目录里看起来挺灵活。实际用下来痛点非常明显子模块的指针是父仓库里一个固定的提交记录每次子模块更新父仓库都要手动升级指针如果切换分支时忘记更新回滚操作也很容易造成混乱。对于不熟悉 Git 机制的成员Submodule 的学习成本不算低。子树合并的体验会好一些它相当于把另一个仓库的历史合并进当前仓库的对应目录更新时通过特定命令拉取。不过子树合并要求使用者理解 Git 的操作细节冲突处理也比较繁琐日常使用频率并不高。我的建议比较直接如果你的团队主要用 Git 做版本管理又没有专门的共享代码维护精力优先考虑私有依赖包或 Monorepo而不要一上来就选 Submodule。不是说 Submodule 不能用而是它在实际协作中的摩擦成本会比文档里看起来高很多。3.3 私有依赖包最常见但容易忽略细节现在大多数语言生态都支持私有依赖包前端是 npm后端可能是 Maven、pip、NuGet 等。私有依赖包的好处很明确有版本号、可发布、可回滚、消费方能按版本升级。它也是最接近“共用代码正规化”的一种方式。但私有依赖包不是只把代码发上去就完了。你还需要解决几件事私有仓库的权限和访问方式包的命名和作用域版本号策略发布前怎么验证消费方怎么升级升级后怎么回归。每一步都有细节如果只是在本地打包上传很容易在团队协作中漏掉关键环节。我见过很多团队在私有包上翻车原因都不是代码逻辑有问题而是版本没对应上。比如公共包改了个参数默认值发了个新版本项目 A 还在用旧版项目 B 升到了新版两边行为不同最后排查半天才发现是版本差异。所以私有依赖包方案的核心不是“发版”而是“版本管理”和“升级通知机制”。3.4 Monorepo 与独立代码库两种架构的取舍Monorepo单仓多包和独立代码库是两种更全局的方案。Monorepo 把所有项目放在同一个仓库里通过包管理工具划分模块。它的好处是改动公共代码时可以同时看到所有调用方回归也能在同一次变更里完成。缺点也很直接仓库体积变大、权限管理变粗、构建和发布流程需要专门设计。独立代码库则是每个项目一个仓库共用代码通过依赖包或版本引用。它的好处是边界清晰、权限可控缺点是跨仓库改动时版本升级和部署顺序会变得更复杂。选择哪种方案不能只看技术还要看团队协作方式。团队规模小、项目耦合高、同一个产品前后端一起开发Monorepo 往往更高效。团队规模大、项目独立性强、需要独立发布节奏独立代码库更稳妥。没有绝对正确的答案但有一条比较通用的原则不要为了用 Monorepo 而用也不要因为害怕复杂度而一直停留在复制粘贴阶段选择要能匹配你当下的团队协作和发布节奏。4. 私有依赖包落地的关键细节从目录设计到发布流程4.1 公共代码包的目录与命名规范如果决定用私有依赖包先不要急着写逻辑先把包的结构和命名定下来。一个好的命名结构能让消费方第一眼就明白这个包属于哪个业务域、负责什么问题。我习惯的划分方式是基础工具包、领域组件包、业务服务包。基础工具包放纯函数比如日期、字符串、校验、加解密不依赖业务数据领域组件包放和业务数据格式相关的组件或函数比如“订单计算”“用户权限判断”它们需要准确定义数据模型业务服务包则是面向具体业务的封装消费方通常比较少更新频率也更高。把这三类放同一个包里不是不行但升级时容易牵连过多消费方。命名上建议统一使用团队的前缀和职责说明例如team/utils、team/order-components或者自己选择的一套规则。只要能让成员一看包名就能判断用途就达到目的了。目录内部应该区分源码目录、构建产物目录、测试文件、类型声明文件、示例目录避免把打包结果和源码混在一起。4.2 版本号管理与变更记录版本号是私有依赖包最重要的一环没有之一。很多团队在早期会觉得做一个公共包后不用管版本其实公共包的版本就是它的契约。基础规则通常采用主版本号.次版本号.修订号主版本表示不兼容变更次版本表示向后兼容的功能新增修订号表示向后兼容的问题修复。这里要特别注意主版本号的变更一定要提前通知所有消费方并给出迁移说明。很多团队会犯一个错误——把不兼容变更伪装成次版本发布只改了函数内部逻辑没有更新调用方式。消费方升级后发现行为变了备份和排查都很困难。变更记录建议采用 Changelog 方式维护至少包括变更类型、影响范围、升级步骤、对应版本号。发布前把变更记录写清楚消费方升级时心里有底回归时也能更快定位问题。不要只在群里喊一句“公共包更新了你们升级一下”。4.3 发布与消费侧的联调流程公共包发版之后不是所有项目都必须马上升级。这里要区分“可升级”和“必须升级”。向后兼容的修复可以按项目节奏升级不兼容版本或安全修复则需要制定升级计划。一个比较稳妥的流程是在公共包仓库里先完成单测和代码检查再在样例项目里跑一遍集成验证让样例项目模拟消费方最常见的使用方式。确认没问题后再按版本发布。发布之后在消费方项目里先升级到新版本运行全量或关键单测如果能通过再进入人工验收。我在实际项目中更倾向的做法是给公共包加一个“最小调用示例”。发布包的时候顺便生成一个可运行的示例项目这个示例项目不一定是文档而是一个能真实跑起来的调用场景对于排查参数错误和版本兼容问题非常有效。5. 公共代码的接口设计边界比实现更重要5.1 参数设计默认值要谨慎配置不要层层透传公共代码的接口设计决定了消费方用起来是否舒服也决定了你以后好不好维护。第一原则是参数尽量少能在一个对象里聚合的不要拆成十几个位置参数。第二原则是默认值要谨慎不要为了让调用方省事就偷偷给参数设一个业务相关的默认值。举个例子一个公共发送验证码函数如果默认超时时间设成了 5 秒看起来没问题但某个业务方在低网速环境下一直接不到响应最后查了半天才发现是这个默认值。默认值不是不能设而是要设成技术合理值而不是业务巧合值。业务相关的值应该交给调用方显式传入这样出问题时一眼能看出来。配置也不要层层透传。我见过一个项目公共模块需要读很多配置调用方为了兼容把一长串配置从入口传到底层。这种做法会让接口极不稳定新增一个配置项所有调用方代码都要跟着改。更合理的做法是把配置收敛成一个明确的结构公共模块自己声明需要哪些配置调用方按约定传入。5.2 错误处理和日志输出公共代码里的错误处理往往比功能逻辑更容易被忽略。很多公共函数失败时直接抛异常调用方没有捕获结果一条业务请求失败之前根本没有有效日志。这种情况排查起来非常痛苦。比较好的做法是公共模块在入口处记录关键参数注意不要记录敏感信息在关键异常处提供明确错误信息和错误码调用方负责捕获和降级公共模块负责暴露失败原因。错误码要遵循一套统一的规范例如模块名_错误类型_错误位置这样消费方看到错误码就能判断属于公共包的哪个环节。还有一点日志级别不要随意输出。不要在每次调用时都打 info 日志否则流量一大日志量会把问题埋没。建议在函数入口、出口、异常分支使用不同级别日常运行只保留必要日志出现错误时可以快速定位链路。5.3 兼容性策略破坏性变更不要偷偷做公共代码一旦发布就相当于和消费方建立了一个隐式契约。破坏性变更不是不能做但一定要做到三点提前通知、提供迁移路径、预留过渡期。提前通知可以放在变更记录和群公告里明确说明废弃的函数、替代方案和升级截止时间。提供迁移路径意味着要么保留旧函数内部转发到新实现要么提供自动化的迁移工具帮消费方把调用方式改好。预留过渡期则是让消费方有充足时间升级不要上午发一个不兼容版本下午就要求所有项目全部升级完成。常见的不兼容变更有修改函数签名、修改返回结构、改变错误类型、修改默认值、删除一个曾经公开的方法。这些变更如果只是“顺手改掉”往往会让调用方在升级后悄悄出现问题。任何时候发现一个破坏性变更都要把它当作一次正式的版本升级来处理。6. 共用代码出问题时的排查顺序和长期维护建议6.1 包改了不生效先查什么公共代码最让人头疼的问题是“我明明改了为什么线上没生效”。出现这个问题时先不要怀疑编译或缓存按照顺序排查。第一步看消费方是否真的升级到了新版本。很多时候你以为已经发布了新版本但消费方的包管理文件里还写着旧版本号或者锁文件锁定了旧提交。第二步看是否多环境缓存。构建缓存、依赖缓存、CDN 缓存都可能导致旧代码继续生效需要确认对应环境的构建流程是否一致。第三步再看调用位置是否引用错了包名或路径。特别是存在新旧包名并存、目录迁移的情况很容易引用到老的路径。如果以上都没问题再看公共包内部的日志。有些问题不是“没生效”而是新版本里某个参数行为变了只是没有明显的报错。这时候对比新旧版本的变更记录通常能快速定位。6.2 多仓库场景下如何降低回归风险共用代码一旦被多个仓库依赖任何一次变更都意味着多份回归压力。要降低风险核心是让验证成本集中、可重复、可自动化。我建议在公共包仓库里建立一套覆盖常用场景的单元测试同时准备一个自动化脚本能够对依赖该公共包的所有项目跑一遍关键用例或冒烟测试。这个脚本不一定很复杂只要能拉取各项目、切换依赖版本、执行指定测试即可。如果工具链现在还做不到至少要用一个清单手动记录各项目升级后的验证结果避免漏项。回归风险还有一个来源是“公共包间接影响”。比如公共包依赖了某个第三方库的版本升级第三方库可能导致消费方行为变化。这类问题最好的办法是尽量锁住公共包的依赖版本升级第三方依赖时单独发版并明确标注影响范围。6.3 什么时候考虑把共用代码升级为内部服务最后聊一个进阶问题共用代码会不会最终变成内部服务要判断这个问题不能只看复用次数还要看数据和状态。如果共用代码主要处理无状态计算比如格式转换、校验、加解密私有依赖包就够了。但如果共用代码需要访问同一个数据库、需要统一的权限模型、需要保证跨系统一致性的业务规则那么它们更适合沉淀为内部服务。因为这样变更逻辑时不需要所有消费方升级依赖而是统一做服务端升级。把共用代码升级为内部服务的代价也很明显要设计接口、处理鉴权、考虑部署和容灾运维成本会比共享包高很多。所以我的建议是只有当你在多套系统里反复写同一套带状态的业务逻辑并且经常因为逻辑不一致导致线上问题才值得把这一层逻辑从代码包中拆出来做成服务。否则一个治理良好的共享库通常已经能解决大部分问题。如果顺着这条线继续往下做还可以考虑把共用代码相关的规范、评审、发布流程整理成内部文档。代码可以复用踩坑经验也一样值得复用。你不需要把所有公共代码都统一成一个体系但每一次抽取和发布都应该比上一次更加规范。
返回列表