代码复用的三个谎言:组件库、工具函数和Copy-Paste都不是答案

博客分类: 

最近看到前端组有同学在讨论要不要把某个逻辑抽成公共工具函数。这个场景太熟悉了,几乎每个团队都在纠结类似的问题:到底什么该复用,什么不该复用?

我的观察是,大部分关于代码复用的讨论都建立在错误的前提上。不管是组件库、工具函数库,还是Copy-Paste,都不是真正的答案。真正的问题在于,我们对"复用"这个概念的理解就是错的。

 

组件库的幻觉

 

前端圈最大的迷思之一是:我们需要一个强大的组件库。

这个想法听起来很美好。把所有常用的UI组件抽象出来,封装成统一的库,然后各业务线直接调用。听起来既能保证一致性,又能提高效率。

但现实总是很打脸。

我见过太多这样的场景:某个业务需要一个带搜索功能的下拉选择器,组件库里有个Select组件。产品经理说要加个"最近搜索"功能,组件库维护者说这是业务定制需求,不该放在公共组件里。业务开发者只好在外面包一层。然后产品经理又说搜索结果要支持分组显示,再包一层。最后这个Select组件被包了三层,代码比从头写一个还要复杂。

问题出在哪?出在我们把"复用"等同于"抽象"。

抽象的本质是找共性,但业务的本质是个性。一个Button组件看起来够简单了吧?但金融业务的按钮可能需要风控埋点,电商业务的按钮可能需要ABTest分流,内容平台的按钮可能需要防抖和防重复提交。这些需求哪个是"共性"?

更糟糕的是,为了让组件库"足够通用",我们会加一堆配置项。最后组件的props有30个,文档写了5页,但80%的业务只用到3个配置。这还不是最坏的,最坏的是那些不在配置里的边界情况:某个业务需要的功能,刚好在配置的缝隙里。

从后端角度看这个问题会更清楚。我们不会指望一个"通用用户服务"能满足所有业务,为什么会指望一个"通用Button组件"能满足所有场景?后端早就学会了微服务和领域驱动设计,但前端还在追求"大而全"的组件库。

 

工具函数的泥潭

 

比组件库更糟糕的是工具函数库。

某天产品说要加个日期格式化功能,开发A写了个`formatDate`函数。过了几天,开发B也需要日期格式化,发现A写过一个,但刚好不满足自己的需求(A的函数不支持时区),于是写了个`formatDateWithTimezone`。再过几天,开发C需要更复杂的格式化,写了个`formatDateAdvanced`。

半年后,项目里有12个日期格式化函数,每个都有一点点不同。新人看懵了,问:我该用哪个?

这时候有人站出来说:我们需要一个统一的日期工具库!于是花了两周时间,写了一个支持所有场景的`DateUtil`类,有20个方法,500行代码,10页文档。

又过了半年,没人敢动这个类了。因为谁也不知道改一个方法会影响哪些地方。最后大家都在外面包装一层,或者干脆重新写一个。

问题在哪?在于我们把"工具函数"当成了"公共代码"。

工具函数应该是简单、纯粹、职责单一的。`formatDate(date, format)`就只做格式化,不管时区、不管国际化、不管本地化。如果需要时区支持,那是另一个函数:`formatDateInTimezone(date, format, timezone)`。

但我们总想让一个函数"做得多一点",结果就是函数越来越复杂,依赖越来越多,最后变成一个nobody wants to touch的黑盒。

从全栈角度看,后端的Service层也有这个问题。一个UserService最开始只负责用户CRUD,后来加了权限校验,又加了缓存逻辑,又加了消息推送,最后变成一个God Object。前端的工具函数库本质上是同样的反模式。

 

Copy-Paste的真相

 

既然组件库和工具函数都不靠谱,那Copy-Paste呢?

大部分技术人员对Copy-Paste深恶痛绝。DRY(Don't Repeat Yourself)被当成金科玉律。看到重复代码就浑身难受,觉得必须立刻抽象成公共函数。

但有些时候,Copy-Paste反而是最好的选择。

假设两个页面都需要一个用户信息卡片。卡片包含头像、姓名、等级、简介等信息。页面A的卡片点击跳转到用户主页,页面B的卡片点击展开详情弹窗。

你会怎么做?大部分人会抽一个`UserCard`组件,然后加个`onClick`回调props。看起来很合理。

但过了一周,页面A说要在卡片右上角加个关注按钮,页面B说要在卡片底部加个消息按钮。你怎么办?加两个props:`showFollowButton`和`showMessageButton`?

再过一周,页面A说关注按钮要根据关注状态显示不同文案,页面B说消息按钮点击要先检查是否实名认证。你又怎么办?

最后这个"通用组件"有15个props,内部全是if-else,两个页面的维护者都不敢改,生怕影响对方。还不如一开始就Copy两份,各改各的。

Copy-Paste的价值被严重低估了。它的本质是"允许差异化演进"。两份相似但独立的代码,可以朝着各自的方向演化,不需要相互妥协。

这也是为什么微服务比单体架构更受欢迎。即使有代码重复,但各个服务可以独立演进,价值远大于"消除重复代码"带来的好处。

 

那什么才是答案?

 

说了这么多问题,那到底该怎么办?

我觉得答案是:接受重复,拥抱差异,在适当的抽象层次上复用。

什么是"适当的抽象层次"?我的判断标准有三个:

第一,看变化频率。

如果两段代码的变化频率相近,且变化原因相同,可以考虑复用。比如API请求的错误处理逻辑,所有接口都需要,变化频率也相近,抽成一个interceptor很合理。

但如果两段代码看起来相似,但变化频率不同,就不要强行复用。比如登录页和注册页的表单,看起来很像,但实际上登录表单两年才改一次,注册表单因为业务策略调整可能一个月改三次。硬要抽成一个组件,就是给自己找麻烦。

第二,看依赖方向。

好的复用应该是单向依赖,业务层依赖基础层,而不是相互依赖。

组件库为什么容易出问题?因为很多组件本质上是"跨业务层"的抽象。一个Select组件既服务于表单场景,又服务于筛选场景,还服务于配置场景。不同场景对组件的需求不同,但组件必须同时满足所有场景,最后就变成了一个四不像。

真正应该复用的是更底层的东西。比如React Hooks、比如HTTP客户端、比如状态管理的核心逻辑。这些是单向依赖,业务永远不会反向影响它们。

第三,看迁移成本。

如果复用带来的收益,不足以抵消"不合适时迁移走"的成本,就不要复用。

这也是为什么我现在更倾向于Copy-Paste而不是npm包依赖。把代码复制到项目里,不合适随时可以改,成本很低。但如果依赖了npm包,发现不合适要迁移走,成本就高多了:要么fork一个私有版本,要么换成别的库然后改所有调用代码。

金融业务更是如此。我们的代码可能要跑5年、10年,技术栈会变,业务会变,团队会变。今天看起来"完美"的抽象,明天可能就是技术债。与其追求完美的复用,不如保持灵活性。

 

复用的本质

 

说到底,代码复用不是技术问题,是决策问题。

复用的本质是做trade-off:用"现在的一致性"换"未来的灵活性"。

当你抽一个公共组件,你获得了现在的一致性,但失去了未来各自演化的灵活性。当你写一个工具函数,你获得了代码量的减少,但失去了针对特定场景优化的可能性。

这个trade-off没有标准答案。不同的业务阶段、不同的团队规模、不同的技术栈,答案都不一样。

但大部分团队犯的错误是:过度追求复用,低估了灵活性的价值。

特别是在业务快速变化的阶段,灵活性远比一致性重要。与其花两周搞一个"完美"的组件库,不如让各业务线先跑起来,等业务稳定了再回过头看哪些真的需要统一。

从后端的演进也能看出这个趋势。早期大家追求"大而全"的服务总线、企业服务总线(ESB),现在都在拆微服务。不是因为重复不好,而是因为灵活性更重要。

 

最后

 

我不是反对复用,我只是反对为了复用而复用。

组件库有价值,但不是所有UI都该放进组件库。工具函数有价值,但不是所有相似代码都该抽成工具函数。Copy-Paste也有价值,有时候它就是最好的选择。

关键是要想清楚:这个复用是为了解决什么问题?是真的能带来长期价值,还是只是为了满足"代码不重复"的心理洁癖?

很多时候,接受一定程度的重复,反而能让系统更健康。就像生物界的冗余机制一样,一点"浪费"换来的是更强的适应性。

代码也一样。

Total votes: 30

添加新评论