ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

软件设计哲学与代码整洁之道:复杂度优先还是整洁优先?

软件设计哲学与代码整洁之道:复杂度优先还是整洁优先? 最近看了一场很有意思的对谈《软件设计哲学》作者 John Ousterhout 和《代码整洁之道》作者 Uncle Bob 被放在同一场对话里标题里直接写着“正面交锋”。这两本书几乎是软件工程圈里被讨论最多的设计类书籍之一一本偏系统架构视角一本偏编码手艺视角。如果你也和我一样两本都读过又觉得“好像都对就是不知道怎么选”这篇文章可以帮你把问题拆清楚。下面不逐句复述视频内容而是从两本书公开发表的核心观点出发做一次对比和推演重点回答三个问题他们各自在解决什么、分歧点到底在哪、落到自己的项目里该怎么取舍。1. 两本书都在讲好代码出发点却完全不同1.1 Ousterhout 把“复杂度”放在第一线Ousterhout 的《软件设计哲学》核心思想很清晰软件设计最重要的目标是降低复杂度。他说的复杂度不是“代码很难读”这么简单而是系统在变更时表现出来的阻碍程度。一个模块改动会牵连很多文件一个接口有大量隐含规则一段逻辑要追很多层才能看懂这些都是复杂度。他从“模块应该更深”“信息隐藏”“通用接口”等角度给出设计策略目的是让大部分复杂度被限制在局部不向调用方扩散。这种视角更适合回答“系统是怎么长坏的”。很多项目不是从第一天就乱而是每个迭代都在正常需求上多开一个口子最后接口满天飞、依赖绕成网。Ousterhout 关心的就是这个积累过程。他会建议你不要急着把功能做出来而是先想清楚接口和依赖用战略式编程代替战术式补丁。换句话说他希望你每写一块代码都先问一句这个模块对系统整体的复杂度是加分还是减分。1.2 Uncle Bob 更关心代码卫生和专业主义《代码整洁之道》的系统性也很好但它更侧重编码纪律。Bob 很擅长用规则把“干净”变得可执行。比如函数应该短小、只做一件事命名要准确且有意义注释不要掩盖烂代码测试要前置格式要统一。这些规则直接作用于程序员每天写的每一行目标是让代码像一本读得下去的书。他把写整洁代码上升到职业素养。代码本身混乱不只是技术债也是团队沟通问题。如果每个函数都又长又绕后来者需要消耗大量精力去猜开发和排错成本都会上升。因此他的策略是从“微观卫生”开始靠持续重构和纪律压制混乱。这套思路对个人习惯养成和团队代码规范特别有效因为它能立刻给出一堆可执行条目让评审过程有据可依。1.3 为什么很容易被误读成谁对谁错两本书不是同一个抽象层级。Ousterhout 主要在讲模块、接口、依赖和复杂度Uncle Bob 主要在讲函数、命名、测试和风格。二者并不像标题里的“正面交锋”那样非此即彼很多读者读完之后觉得冲突是因为拿一本的标准去套另一本的概念。比如《代码整洁之道》提倡函数要小而 Ousterhout 可能会指出一个模块内部如果被拆成大量细碎的函数调用链很长调用方依然难以理解模块接口却没有变好。相反“深模块”允许内部有一大段复杂实现只要对外接口简单清晰整体复杂度反而更低。站在两套理论各自的角度都对。关键是你当前处理的问题发生在哪一层如果问题在函数内部Bob 的规则更顺手如果问题在模块边界Ousterhout 的思路更接近病根。2. 把两套哲学放在同一张表里差异就清晰了只看文字描述还不够直观我习惯把两位作者对同一类问题的答案放进表格里对比。这样能快速看出他们在乎什么、忽略什么。对比维度Ousterhout 的软件设计哲学Uncle Bob 的代码整洁之道核心视角复杂度是首要敌人整洁和专业是首要责任最小设计单元模块、接口、依赖函数、类、命名模块设计倾向深模块行为多接口少小函数、单一职责开发节奏战略式编程预留设计空间小步提交持续重构测试定位好设计让测试变简单TDD 是开发流程的一部分错误处理尽量减少异常扩散快速失败清晰处理边界典型担心系统复杂度不受控代码可读性越来越差2.1 核心视角复杂度降维 vs 纪律约束Ousterhout 的视角更像“降维”。他希望把人脑需要同时理解的细节压缩进一个黑盒让调用方只面对简单接口。Uncle Bob 的视角更像“约束”他希望靠规范和纪律把代码写成多数人一眼能看懂的常规形态。一个强调结构一个强调过程。这两种视角没有优劣但会直接影响你如何做评审。用 Ousterhout 的眼光看代码你会更关注“这个模块有没有把复杂性藏好”用 Uncle Bob 的眼光看代码你会更关注“这个函数拆得够不够小、命名够不够清楚”。2.2 模块设计深模块 vs 单一职责小函数深模块是《软件设计哲学》里最重要的概念之一。它要求一个模块对外暴露的接口尽量少但模块内完成的工作尽量多。调用方不需要知道内部细节改动成本就被限制在了模块内部。Uncle Bob 则强调函数和类要小要单一职责。如果只有一个职责就更容易理解、更容易测试。这两个方向在局部会产生摩擦。一个深模块如果内部逻辑复杂硬要拆成十几个小函数可能反而让调用链变长阅读时需要在文件之间跳来跳去。我的处理方式是把公开接口设计成深模块把模块内部的私有函数按 Bob 的规则拆小。公开层和内部层分开看矛盾就没有想象的那么大。2.3 开发节奏战略式投入 vs 持续重构Ousterhout 反对“只求当前能跑”的战术式编程。他认为很多项目死掉不是因为某个功能没实现而是因为所有设计都在为下一个补丁让路。他希望每次写代码都为未来一段时间的扩展留出空间。Uncle Bob 没有否认设计的重要性但他更相信小步重构的力量先让一个测试通过再通过重构改善结构每一步都可验证。这里其实不是谁对谁错而是时间尺度的差异。Ousterhout 要求你在动手前多用一段时间做设计Uncle Bob 则希望你在动手后的每一小步里保持结构健康。成熟团队通常两者都需要大方向用战略式设计实现细节用持续重构。2.4 测试与实践降低复杂度 vs 测试先行Bob 的 TDD 很多读者都背得出先写一个失败测试再写最简实现再重构。对 Ousterhout 来说测试是一种重要工具但如果设计得当大量测试其实不必存在。真正值得花成本的是那些经常出错、边界复杂的核心路径而不是每一个稳定功能都堆满测试用例。这个分歧对代码量影响很大。按 Bob 的思路很多团队会要求核心模块必须有高覆盖率的测试按 Ousterhout 的思路你会先问“这个模块为什么需要这么多测试”然后尝试通过简化接口减少测试复杂度。两者结合后我们通常会对风险高、变化多的路径多写测试对稳定且简单的模块少写装饰性测试。3. 两位作者最可能碰撞的五个交锋点以下交锋点不是视频里的逐字记录而是我从两个人公开的著作里推演出来的。如果你看完对谈后觉得有些地方针锋相对多半也绕不开这五个问题。3.1 “复杂度”到底由谁定义Bob 会说一个函数超过三十行读者扫一眼就看不下去了这就是复杂度。Ousterhout 会说一个函数虽然短但被 20 个地方以微妙方式调用改一个调用就影响一片这才是复杂度。这两种定义指向不同的解法。前者靠拆分后者靠封装。在真实项目里两者可能同时存在。最稳妥的做法是不要急着争论概念而是先定位“修改某个需求时到底要动多少文件、跳多少次上下文”。如果问题出在函数太绕就用 Bob 的办法清理如果问题出在模块边界太散就用 Ousterhout 的办法重画边界。3.2 深模块和小函数怎么和平共处深模块内部往往不能始终保持小函数。对外是简洁 API内部可能有一段复杂算法。按 Bob 的规则这段复杂算法应该拆成多个有名字的辅助函数按 Ousterhout 的思路辅助函数暴露出来只会增加接口数量。合理做法是让辅助函数保持在模块内部私有不暴露到公开接口。这样内部既能保持 Bob 式的小函数和清晰命名对外又只留一个深模块入口。两套规则只在公开边界上冲突模块内部可以放心结合。3.3 命名和注释到底算不算设计Bob 把命名和注释当作代码质量的一部分。他会说好的命名能减少注释好的注释能解释意图。Ousterhout 则更倾向于如果一个接口需要很长注释解释说明接口设计不够直白。他可能建议减少注释修改接口。实际编码时两种观点可以形成一条检查链先看命名是否准确再看不看注释也能理解最后看接口调用方是否需要知道很多内部细节。前两条偏向 Bob最后一条偏向 Ousterhout。3.4 测试是目标还是手段Bob 把测试当作开发过程的一部分没有测试的代码不值得信任。Ousterhout 会提醒你测试也是代码也要维护也增加复杂度。测试永远不应该只是为了覆盖率数字而是为了控制变更风险。对高风险路径多写对稳定边界少写。这个交锋点最容易在团队里引发争论。我的建议是先问你们项目最近频繁出问题的区域在哪里然后集中补测试。不要为了指标而给所有模块堆一样厚度的测试那样既增加维护成本又掩盖了真正需要关注的复杂度。3.5 重构应该在什么时候做Bob 的红-绿-重构把重构放进每次迭代做完一个小功能就顺手整理。Ousterhout 会警惕“因为没有设计好所以不断重构”的循环。他的建议是先停下重新设计模块边界再动手而不是继续在小范围里打补丁。项目里其实有两种重构一种是功能完成后的小调整属于 Bob 说的持续重构一种是结构坏了不得不重做属于 Ousterhout 说的重新设计。两者不能混为一谈。如果只是文件内部乱直接重构如果模块间依赖已经绕成网先画依赖图再决定怎么拆。4. 不同场景下怎么选更稳妥4.1 新手阶段先建立代码整洁的肌肉记忆新入行的读者我建议先按《代码整洁之道》的规则练手。理由很简单设计眼光需要经验支撑而整洁规则能立刻用代码规范反馈。命名、函数长度、分支深度、注释是否必要这些在评审和结对时很容易被指出。先把微观手感练出来后面学模块设计才不会悬空。很多刚工作的人容易犯一个错过早讨论“深模块”“战略式编程”这些概念却连一个函数的职责都拆不清。这就像还没学会基本刀功就开始研究菜单结构。先踏实写干净的小函数再慢慢往上层看。4.2 做架构和复杂系统站在 Ousterhout 这边更高效如果你已经能稳定交付出可运行的代码却在系统扩展时频繁踩坑这时候要切换视角。不要只追问“这个函数干不干净”要追问“这个模块的接口有没有把复杂度藏好”“变更会波及多少文件”。复杂度预算是架构评审中很实用的一把尺子。比如一个支付模块如果调用方必须知道退款状态机、重试策略、幂等键来源才能正确使用接口就不够深。这时候无论内部函数拆得多干净使用成本都高。Ousterhout 的意义就是逼你在设计阶段把这种问题暴露出来。4.3 老项目维护两条腿走路老项目往往二者都要。历史代码里既有名字乱、函数长的卫生问题也有模块边界混乱导致改一发而动全身的结构问题。直接套用任何一本书都会很痛苦。建议先画出模块依赖图找到热点模块按深模块思路重建边界再对内部函数做整洁化处理。优先处理哪个看变更频率。如果某个模块常年不改里面再乱也别急着动如果某个模块每周都有需求那它就是复杂度重灾区值得花一轮迭代做边界重构。重构后还要补测试否则下一次改动可能把全局带崩。4.4 团队协作统一语言比选某派更重要团队讨论代码时真正有效的不是“我信奉谁”而是我们对“接口”“模块”“复杂度”“整洁”这些词有统一理解。如果团队无法达成一致很容易出现互相看不惯架构师觉得码农只抠局部码农觉得架构师不落地。我见过比较有效的方式是约定两本检查单架构评审用复杂度清单核心逻辑用整洁清单。团队里可以有人偏 Ousterhout也有人偏 Uncle Bob但评审时必须用同一套问题问代码而不是互相抛作者名字。5. 一套可以落地的融合清单5.1 可读性作为最低线不管设计再高级如果代码连读都读不通没人会关心你的深模块。第一步永远是把可读性做到底线。命名的准确性、函数是否过长、重复代码是否抽出先处理掉。这一步花的时间不多但收益最直观。可读性不是“看起来漂亮”而是降低下一个维护者进入状态的成本。写代码时多问一句如果三个月后自己回来改这段逻辑能一眼看懂吗5.2 设计阶段做复杂度预算每次设计评审都可以回答三个数字模块对外暴露几个方法一次需求变更预计影响几个文件新逻辑的分支状态是否超过一个屏幕如果数字偏大就考虑是不是拆错接口了。复杂度预算不是死指标而是提醒工具。不用定得太死比如“接口最多三个方法”那样容易逼出反模式。重点是让每个人都习惯在动手前先量化一下改动范围。5.3 深模块负责“接口”整洁规则负责“内部”这是我自己比较常用的融合方式对外公开 API 尽量设计成深模块接口少、能力完整、信息隐藏模块内部实现则尽量用 Bob 的规则来组织函数短小、命名清楚、注释克制。这样既不会为了小函数把接口拆碎也不会为了隐藏细节把内部写成一坨。落到代码评审里就有两个明确的检查层次。评接口时只问“调用方需要知道多少”评内部时只问“函数是否容易理解”。两套标准同时存在争论会少很多。5.4 测试与重构纳入日常节奏而不是冲刺任务不要在版本发布前才补测试也不要在架构崩了才做重构。建议每次迭代都把“改动面最大的那个模块”列出来先设计后动手同时补测试。稳定但很少改的模块不要随便刷新避免引入回归。这个习惯特别适合业务团队。很多项目的测试覆盖率数字很好看但都是稳定模块上的装饰真正频繁出问题的核心路径反而没有测试。把测试资源投向热点模块比追求全局覆盖率更有用。设计层级主要参考检查问题模块接口Ousterhout调用方需要理解多少内部细节模块内部Uncle Bob函数是否短小、命名是否清楚、分支是否简单变更过程两者结合改一个需求要动几个文件测试是否覆盖风险路径5.5 不要被“复杂度”和“整洁”两个词绑架任何理论都是工具。如果团队整天为“这个接口不够深”“这个函数超过十行”吵架项目反而没法推进。我的建议是每轮只挑一到两个最需要改进的维度过一段时间再换。比如这一轮重点处理命名和函数长度下一轮再关注模块边界。两本书都值得读但没必要把它们变成僵化教条。真正健康的项目是设计者和实施者愿意根据具体情况调整标准而不是拿一本著作压人。6. 看完对谈之后我更建议你做的三件事6.1 选一个小模块试着重做接口不需要重构整个系统。拿项目里自己最常用、接口又很散的模块先记录当前暴露了几个 public 方法然后尝试压缩到一半让调用方更省心。这个过程会同时用到 Ousterhout 的深模块思想和 Bob 的函数整理手法。你会发现真正的困难不是改代码而是识别哪些逻辑可以放进内部哪些必须暴露给外部。这个判断做几次之后再回看两本书理解会完全不同。6.2 拿一段老代码做整洁度复盘选一个 500 行左右的文件按《代码整洁之道》的标准把问题列出来命名、重复、过深的分支、无效注释、过长函数。先别急着改标出优先级。你会发现很多问题是历史包袱不是简单扫一遍能解决的。这一步的收获不是马上变干净而是让你知道自己平时写代码到底在哪些环节积累问题。多数人复盘完之后会发现最大问题不是缺技巧而是缺少每次提交前的自查习惯。6.3 在仓库里记一笔“复杂度账单”这个做法简单作用却很大。每次修改后在 README 或 CHANGELOG 里记改的哪个模块、涉及几个文件、有没有补测试、下次谁最容易踩坑。持续记录几周你就能看到系统的复杂度分布在哪儿。复杂度账单不用写成长篇报告三五行即可。它的作用是让技术债从模糊感受变成可追踪记录。等到某模块频频出现在账单里就知道该动手重新设计边界了。6.4 别急着站队最后想强调这类书籍的价值不是让你成为某个作者的信徒而是给你增加一种视角。Ousterhout 和 Uncle Bob 的争论更像是同一枚硬币在不同光线下的影像。你要带着自己的项目问题去对照而不是带着观点去验证。我不觉得《软件设计哲学》和《代码整洁之道》必须分出胜负。更常见的状态是白天写业务代码时用 Bob 的规则保持局部干净晚上设计模块边界时用 Ousterhout 的复杂度视角判断方向。两套坐标都在手上比只选一边稳得多。下次有人问该读哪本先问自己现在最痛的是函数太脏还是接口太散。这个答案会告诉你从哪一边先出手。
返回列表