浅谈优雅——消除读代码的阻力感
最近在 CR 一段代码时,和实现者产生了分歧。
场景是这样的:有一个查询接口,需要先获取当前用户的数据权限 DSL,再拼接查询条件,最后执行查询。实现者的方案是:在接口的请求对象里预留了一个 authCode 字段,但这个字段不允许被调用方 SET——它会在 AOP 里被自动填充,由切面调用权限接口获取结果后覆盖进去,然后在另一个服务类里完成 DSL 的拼接。
我认为这个实现不合理,应该把权限获取的逻辑显式写在业务方法里。实现者的理由是:AOP 更优雅,不应该把这种逻辑混进业务代码。
这个争议让我开始认真想一个问题:优雅到底是什么?
优雅是阻力感的消失
当你读一段优雅的代码,你不会感觉到"在努力理解"。信息就这样流进来了,没有摩擦。这种感觉背后,是作者替你承担了所有的认知负担——他把所有可能让你困惑的地方都提前处理掉了,把复杂性消化在了你看不见的地方。
所以优雅内含一个悖论:它看起来很简单,但产生它需要很深的理解。一个对问题理解不够深的人,写不出优雅的东西,因为他自己还没把复杂性消化掉,只能把它原样暴露出来。
因为优雅的实现需要对问题的深刻理解,所以有一个经常被忽略的问题:优雅是有边界的,优雅的设计只在被解决的问题内优雅。。
一个设计在解决某类问题时是优雅的,但超出这个问题的边界就不是了。以 AOP 这个经典方案为例:AOP 处理日志是优雅的,因为在"日志"这个问题域里,它的抽象是准确的。但把它用来注入业务数据,就出了边界,优雅就变成了混乱。很多过度设计的问题,本质上就是把一个在局部优雅的方案,强行推广到了它不适用的地方。
因为对问题深刻的理解,所以优雅往往是减法,而不是加法。
不优雅的实现通常是"我需要解决这个问题,所以我加了这个",一层一层叠上去。优雅的实现是"我把问题想清楚了,发现其实只需要这个"。前者是在问题上堆解法,后者是在重新定义问题。帕斯卡有一句话:"如果我有更多时间,我会写一封更短的信。"优雅需要时间,因为它需要你把自己的思路压缩、提炼、再压缩,直到只剩下不能再少的东西。
复杂度是守恒的
理解了"优雅是阻力感的消失",下一个问题自然就来了:阻力从哪里来?
答案是:被转移的复杂度。
一个问题本身有多复杂,它就有多复杂,这是客观的。你能做的只有两件事:消化它,或者转移它。
消化复杂度是真正的有用的解决方案,你把复杂性吸收进设计里,让它在结构层面被处理掉,对外暴露的是简单的接口。而转移复杂度是彻彻底底的坏味道,你让自己的代码看起来简单了,但复杂性跑到了调用方、跑到了文档里、跑到了下一个读代码的人的脑子里。
开发中有几个地方特别容易发生隐性的复杂度转移:
-
命名是最常见的转移复杂度的方法。一个名字起得含糊,读者就要靠上下文猜测意图,
data、info、result、temp,这些名字本质上是作者在说"我懒得想清楚这是什么,你自己悟吧"。 -
隐式的前置条件是另一种典型的转移复杂度。一个方法要求调用方在调用前先做某件事,但这个要求不在签名里、不在注释里,只在实现里,调用方只有踩坑了才知道。
-
过度的间接层也是转移复杂度的重灾区。每多一层抽象,读者就要多维持一层上下文,如果一个抽象层只是把代码搬了个地方,没有降低任何概念复杂度,它就是纯粹的理解成本。
所以,优雅的设计必须**「不转移复杂度,不增加理解成本」**。
这两句话其实是同一件事的两个视角。转移复杂度是从作者视角说的:我没有解决这个复杂性,我只是把它挪到了别处,让它从我的视野里消失。增加理解成本是从读者视角说的:我要理解这段代码,需要先掌握多少前置知识?需要跳转多少次?需要在脑子里维持多少个上下文?两者是镜像关系——作者每转移一次复杂度,读者就多付出一次理解成本。
这些优雅实现的约束在某种意义上是一种道德姿态:我不把我没解决的问题留给别人。
AOP 的优雅,以及它的边界
带着这个框架,再回头看 AOP,就能说清楚它的优雅到底是什么,以及为什么开头那个实现不对。
AOP 的优雅,核心在于它发现深刻理解了一件事:有一类逻辑,它的"在哪里执行"和"执行什么"是可以分离的。日志、监控、事务,这些逻辑本身不复杂,复杂的是它们需要出现在很多地方。如果没有 AOP,你只能把它们一遍遍地写进每个方法里,或者靠调用方记得手动调用。这两种方式都在转移复杂度:前者是代码重复,后者是把"记得做这件事"的责任推给了调用方。AOP 把"在哪里"从"做什么"里解耦出来,统一管理,这是真正消化了一类复杂性。
但 AOP 的优雅有一个严格的前提:切面逻辑必须对业务逻辑透明。
透明的意思是:业务逻辑不需要知道切面存在,切面存在与否不影响业务逻辑的正确性,只影响"附加效果"。
日志切面去掉,业务还是对的,只是少了日志。事务切面去掉,业务逻辑还能执行,只是失去了原子性保证。透明性保证了一件事:读业务代码的人不需要知道切面的存在,就能完整理解业务逻辑。这是 AOP 优雅的根源——它让两个关注点的读者可以各自独立地理解自己关心的部分,互不干扰。
回到开头的场景:权限注入切面去掉,查询结果就错了。这意味着业务逻辑对切面有隐式依赖,它假设切面一定存在、一定在正确的时机执行、一定把正确的值写进了正确的字段。这些假设全都是隐藏的,不在代码里,不在签名里,只在某个人的脑子里。这时候 AOP 不是在消化复杂度,而是在制造隐式耦合。读者的理解成本不是降低了,而是更高了——他不仅要理解业务逻辑,还要知道有一个切面在暗中影响它,还要理解切面的执行时机和副作用。
判断一个逻辑该不该放进 AOP,有一个很干净的标准:这个切面,是在为业务逻辑服务,还是在参与业务逻辑? 服务——透明,可以用 AOP。参与——不透明,不应该用 AOP。日志是在服务业务逻辑,它记录业务发生了什么,但不影响业务的结果。权限数据注入是在参与业务逻辑,它直接决定了查询结果是什么。两者的本质差异,就是透明性。
优雅的根源是对问题的深刻理解
到这里,可以回答一个更根本的问题:为什么有人会把权限注入放进 AOP?
不是因为他懒,也不是因为他不认真。是因为他没有想清楚"权限获取"在这个流程里的角色——他把它当成了横切关注点,但它其实是核心数据流的一部分。理解出了偏差,解法自然就偏了。
这引出了一个判断不优雅的实用方法:不优雅有两种根源,处理方式完全不同。一种是理解不够深——设计者没有真正搞清楚问题的本质,解法是在问题的表面打补丁,这种情况下改代码没用,需要先退回去重新理解问题。另一种是理解到位了,但表达没跟上——问题想清楚了,但代码没有把这个清晰度传递出来,命名含糊、结构散乱、依赖隐式,这种情况下重构可以解决。
诊断方式也不一样:如果你能清楚地说出"这个设计在解决什么问题、为什么这样解",但代码没有体现出来,那是表达问题。如果你说不清楚,那是理解问题。
这也解释了为什么优雅很难被"学会"——你学不会优雅本身,你只能学会更深地理解问题。优雅是理解深度的副产品。
一个实用的自检方法
写完一段代码之后,假装自己是第一次读它的人,问:我需要知道哪些额外的东西,才能理解这段代码在做什么?
每一个"需要额外知道的东西",都是一个潜在的阻力点。有些是合理的——调用方确实应该了解领域知识。有些是可以消除的——通过更好的命名、更清晰的结构、更显式的依赖。把可以消除的阻力点消除掉,剩下的就是这个问题本身固有的复杂度,那是无法消除的,也不应该试图消除,因为那是真实世界的一部分。
这个框架不只适用于代码。一篇说不清楚的技术文档,一个让人困惑的 API 设计,一个需要反复解释的架构方案——背后都是同一件事:要么作者没想清楚,要么想清楚了但没表达出来。读者遇到阻力的地方,就是作者没有消化掉复杂度的地方。
优雅是把复杂性消化在内部、把清晰性暴露在外部的能力。它不是风格,不是品味,是一种对问题理解深度的体现。