复用要等到什么时候
最近看到一段关于复用的评论,正好说中了我在组件设计里纠结的事:还没遇到第二个使用场景,就开始替未来设计接口。
The main driver of your motivation is reuse. That is a problematic motivation when it comes to software design and implementation, largely because it requires clairvoyance on the part of the designer/developer (unless it’s already been done several times before, i.e. experience); relevant reuse is mostly discovered, not designed up front. … (后面省略)
我理解这段话的重点是:有用的复用通常是在实践中发现的。如果没有做过几次类似的实现,提前设计通用接口,很大程度上是在猜。
还没复用,先多了一堆配置
写组件时,很容易想把 A、B、C 三个项目未来的需求都兼顾进去。写函数时,也会想多留几个参数,免得以后改接口。
但一个“通用组件”可能最终只用在一个项目里;为了覆盖假想场景加上的配置,却已经成为维护成本。等到真正的新需求出现,它未必落在之前预留的扩展点上。追求灵活,反而让眼前的代码更难读。
技术和需求都会变。“以后都能用”很难成为一个可以验证的设计目标。
React、Angular、Vue:先看到差异
评论里举了一个跨框架组件的例子:
So it makes perfect sense to first design a component in React, then in Angular if need be. Once you also have to create one for Vue it may make sense to try to extract one common implementation, one that works equally well in all three cases (though often the result is just equally awkward in all three).
先在 React 中实现,需要 Angular 版本时再实现一遍。等到 Vue 版本也成为实际需求,才考虑提取共同实现。即便如此,抽出来的东西也可能在三个框架里都不好用。
“写三遍”不是一个硬性次数要求。我更在意的是顺序:先解决具体问题,再比较几次实现之间哪些逻辑相同、哪些约束不同。第一遍先跑通功能;第二遍开始看到框架的差异;到了第三遍,才有更多依据判断抽象边界。
如果只是为了证明一个组件能跨框架运行而重写三次,又回到了为了复用而复用。
数据树和视图树的边界
评论还提到对 React 的一个批评:
One important criticism to React is: “… that’s like React’s biggest simplification, its biggest flaw—they are basically equating your data tree with your view tree …”
把数据组织与视图组织放在一起很直观,但两者的边界未必总是一致。这是评论者对 React 的判断,我觉得值得带着具体组件去看,而不是直接把它当成框架结论。
同样的问题也被延伸到了 Web Components:
This can apply equally to WCs where what is ostensibly a DOM element takes on application responsibilities while having to somehow interact with the rest of the application in some bespoke manner.
一个看起来只是 DOM 元素的组件,可能承担了应用层职责,还需要用专门的方式和外部应用通信。此时即便接口是原生的,也不代表业务逻辑已经解耦。换个应用后仍需要大量适配,跨框架这件事本身就没有解决复用问题。
让业务定义需要的接口
评论最后提到了六边形架构(Hexagonal Architecture):
… a sound, maintainable solution should dictate the interfaces it depends on (narrowing API or pattern of usage API), rather than accepting the interfaces already exposed by the dependencies.
我的理解是,业务代码应该依赖自己需要的接口,而不是把第三方库暴露的 API 原样带进来。
例如文件上传,先定义业务需要的 FileUploadService,再写 ThirdPartyFileUploadService 适配具体库。以后换库时,调整适配实现,尽量把影响留在这个边界里。能否只换实现,仍取决于新旧库是否满足同一份业务契约。
这比提前设计一个无所不包的上传组件更具体:接口服务于已知需求,复用也有了可判断的范围。