大学上软件工程课的时候,老师一直在强调「抽象」的重要性。当时似懂非懂,觉得抽象就是提取公共代码,把重复的逻辑放进一个函数里。工作几年后再回头看,才发现这个理解太浅了。
抽象的本质是「选择性地忽略」
抽象的本质不是隐藏代码量,而是管理复杂度。一个好的抽象告诉你:你现在只需要关注这几件事,剩下的细节我来搞定。
比如说你调用 fs.readFile(),你不需要知道操作系统是怎么处理文件的、磁盘的扇区是怎么寻址的、文件系统是 ext4 还是 NTFS。API 层把这些全部封装了,你只需要传一个路径。这就是抽象的力量。
抽象就是给复杂系统开一扇小窗——你看到的只是你需要看到的那部分。
过度抽象的陷阱
但抽象也有副作用。层数太多的时候,问题反而更难排查了。
举个例子:你在一个方法里加了一行日志,结果走了三层继承、两层装饰器、一个 AOP 切面,然后日志被一个新加入的过滤器拦截了。你想加一行日志,结果发现在这个系统里,「加日志」这件事本身的复杂度已经堪比一个小型项目。
这种过度抽象在一些「面向设计模式编程」的项目里很常见。代码结构非常「优雅」,每一层都严格遵循了某种设计模式,但你要改一行逻辑,要跨越七八个文件。所谓的高内聚低耦合变成了低内聚零耦合——每个类确实只做了一件事,但你找不到那件事到底在哪里。
什么时候该抽象
我现在的原则是三次法则:一段逻辑出现两次,不急于抽象;第三次出现时再考虑提取。过早抽象和过晚抽象都是问题,但过早抽象的代价往往更大——你抽象出一个「通用方案」,结果下个需求一来就打破了你的假设。
好的抽象应当是自发的,而不是设计的。它从重复中自然生长出来,而不是由设计者在第一天就预设好。Kent Beck 说过:
Run the tests, make it pass, make it right, make it fast。把「make it right(即抽象)」放在「make it pass」之后是有道理的——先能跑,再抽象。
接口是契约,实现是细节
关于抽象还有一个重要的原则:接口应该比实现更稳定。抽象提供的接口是一个承诺——你调用这个方法,我保证返回某种结果。至于内部是查数据库还是调 API 还是直接算出来的,调用者不需要关心。
这种契约式的思维方式在架构设计中尤其重要。微服务之间的 API 定义就是一种抽象——服务 A 不关心服务 B 用什么语言写的、部署在哪里、升级了几次,它只关心 API 接口有没有变。接口变了,契约就破了,整个系统就要跟着调整。
抽象是编程中最强大的工具之一,也是最容易被误用的工具之一。理解它的本质,知道什么时候用、什么时候收手,可能是区分有经验的工程师和初学者的重要标志。