第二章 务实的态度 (A Pragmatic Approach)
一、导读
2.1 本章学习目标
掌握 DRY(Don't Repeat Yourself)原则的本质及其在代码、架构、文档等多个层面的应用
理解正交性(Orthogonality)的概念,学会识别和消除代码中的相互依赖
学习可逆性(Reversibility)思维,理解为什么好的设计应该允许决策的回退
掌握牵引线(Tracer Bullet)与原型(Prototype)的区别及各自适用场景
理解领域语言(Domain Language)和外部 DSL 在提高表达力方面的价值
学会估算(Estimation)的技巧,避免常见的估算陷阱
理解灵活性与权衡(Flexibility vs. Trade-offs)的关系,避免过度设计
掌握如何通过重构(Refactoring)持续改进代码质量
2.2 为什么本章如此重要
如果说第一章建立了务实程序员的世界观,那么本章则提供了方法论。务实的态度不是抽象的哲学,而是体现在每一行代码、每一个设计决策中的具体实践。DRY、正交性、可逆性——这些原则构成了软件设计的底层逻辑,无论你使用什么语言、什么框架,它们都是适用的。
DRY 原则可能是软件工程中最广为人知的原则之一,但真正理解并贯彻它的人并不多。DRY 不仅仅是"不要复制粘贴代码"那么简单。它关乎的是"每一个知识点都应该在系统中有一个唯一的、明确的、权威的表示"。当你深入理解这一点时,你会发现违反 DRY 的地方无处不在:数据库 schema 与代码中的 model 定义重复、API 文档与实际行为不一致、部署配置在多个环境中硬编码。
正交性则是一个经常被忽视但极其重要的概念。正交的系统组件彼此独立——修改一个不会影响另一个。正交性带来的好处是巨大的:更高的可测试性、更低的变更风险、更好的可理解性。当你发现修改一个功能需要同时改动五个文件时,这就是正交性不足的信号。
2.3 核心问题引导
DRY 原则除了"不要重复代码"之外,还有哪些更深层的含义?
如何判断两个模块之间是否存在正交性问题?
为什么作者说"牵引线不是原型"?两者的根本区别是什么?
在什么情况下,违反 DRY 原则反而是正确的选择?
如何培养对"正交性"的直觉?
估算为什么总是错的?有什么方法可以提高估算的准确性?
如何在"灵活性"和"过度设计"之间找到平衡点?
重构的最佳时机是什么?如何避免重构变成无休止的重写?
二、核心概念详解
概念一:DRY 原则(Don't Repeat Yourself)
DRY 原则的核心表述是:Every piece of knowledge must have a single, unambiguous, authoritative representation within a system。翻译过来就是:系统中的每一处知识都必须有一个唯一的、明确的、权威的表示。
这个原则的应用范围远超代码层面。在代码层面,DRY 意味着不要复制粘贴逻辑——如果两段代码做同样的事情,应该提取为一个函数或方法。在数据层面,DRY 意味着不要在多个地方存储同一份数据——使用数据库的规范化来消除冗余。在设计层面,DRY 意味着不要在多个模块中重复同一个概念的定义——使用统一的抽象。
违反 DRY 的后果不仅仅是"多写了一些代码"。当同一个知识在多个地方重复出现时,修改就变成了一个危险的操作:你必须找到所有的重复,确保它们都被一致地更新。遗漏任何一个,都会导致系统行为的不一致。这就是为什么 DRY 是软件可维护性的基石。
但 DRY 也有其边界。在不同的 bounded context(限界上下文)中,同一个概念可能有不同的含义。在这种情况下,强行统一反而会导致混乱。关键是要区分"真正的重复"和"表面相似但本质不同的概念"。
概念二:正交性(Orthogonality)
正交性源自数学概念:两个向量正交意味着它们彼此独立,互不影响。在软件设计中,正交性意味着系统的各个组件应该彼此独立——修改一个组件不应该影响其他组件的行为。
正交性的对立面是"耦合"(Coupling)。高度耦合的系统中,一个看似简单的修改可能引发连锁反应。比如,修改一个数据结构的字段,导致十个文件需要同时修改。这种"牵一发而动全身"的现象,正是正交性缺失的表现。
实现正交性的关键是关注职责分离(Separation of Concerns)。每个模块、每个类、每个函数都应该有明确的、单一的职责。当两个功能的变化原因相同时,它们应该在一起;当变化原因不同时,它们应该分离。这就是所谓的 Common Closure Principle。
正交性还体现在层次结构上。UI 层不应该知道数据库层的实现细节;业务逻辑不应该依赖于特定的 Web 框架。通过定义清晰的接口和抽象层,你可以让每一层独立演化,互不干扰。测试正交性的一个好方法是:如果你可以独立地替换某个组件而不影响其他组件,那么你的设计就具有良好的正交性。
概念三:牵引线与原型(Tracer Bullets vs. Prototypes)
牵引线和原型是两种不同的探索策略,它们的目的和生命周期截然不同。
原型(Prototype)的目的是回答一个特定的问题:这个方案可行吗?这个技术能满足性能要求吗?这个 UI 交互方式用户能接受吗?原型是故意不完整的——你只关注需要验证的部分,其余部分可以用 mock 数据或硬编码来代替。原型的生命周期是短暂的,验证完问题后就应该丢弃。
牵引线(Tracer Bullet)则不同。它是一条贯穿整个系统的细线——从用户界面到后端逻辑到数据库——虽然功能极其简单,但它走通了整个路径。牵引线的代码是真实的、可运行的,它会被保留并逐步扩展为最终产品的一部分。
两者的区别在于:原型是实验,用完即弃;牵引线是骨架,逐步生长。选择哪种策略取决于你的目标:如果你需要验证技术可行性,用原型;如果你需要快速建立一个可工作的端到端骨架,用牵引线。
概念四:估算的艺术(Estimation)
软件估算之所以困难,是因为它本质上是在不确定性中做预测。但"难"不等于"不可能"。作者提供了一些实用的估算技巧。
首先,明确估算的精度要求。当有人问"这个功能要多久"时,先反问:你需要多精确?如果答案是"大概就行",你可以说"一到三周";如果需要精确到天数,你需要更多时间来分析。模糊的估算和精确的估算需要不同的方法论。
其次,使用"数学模型"来分解估算。将大任务分解为小任务,对每个小任务分别估算,然后求和。分解的过程本身就能帮助你发现隐藏的工作量和风险。
第三,建立反馈循环。记录你的估算和实际耗时,分析偏差的原因。随着时间推移,你会越来越了解自己和团队的"估算偏差系数",从而提高估算的准确性。
最后,永远不要用"天"作为唯一的估算单位。考虑沟通成本、会议时间、代码审查、测试、部署等因素。一个"三天"的开发任务,实际可能需要一周的日历时间才能完成。
概念五:灵活性的代价(The Cost of Flexibility)
灵活性是软件设计中的重要品质,但过度的灵活性会带来巨大的成本。每增加一个抽象层、每引入一个配置项、每设计一个插件机制,都会增加系统的复杂度。
关键问题是:这个灵活性真的会被用到吗?作者提出了一个实用的判断标准:如果你能举出具体的例子说明这种灵活性会被需要,那就值得投资;如果只是"以防万一",那就不要做。YAGNI(You Aren't Gonna Need It)原则在这里同样适用。
灵活性的另一个维度是"可逆性"。好的设计应该允许你回退之前的决策。如果一个决策一旦做出就无法更改(比如选择了某个数据库),那它就是一个高风险决策,需要更谨慎地评估。而如果一个决策可以轻松回退(比如选择了某个日志框架),那就可以更快地做出决定。
三、深入分析与讨论
讨论一:DRY 的层次与边界
DRY 原则看似简单,但在实践中有很多微妙的层次。最基础的层次是代码级别的 DRY:不要复制粘贴代码片段。这通过提取函数、方法来消除重复。
更高层次是架构级别的 DRY:不要在多个服务中重复定义同一个业务概念。这通过共享库、API 契约、schema 定义等方式来实现。再高一层是运维级别的 DRY:不要在多个环境中手动维护相同的配置。这通过 Infrastructure as Code、配置管理工具来实现。
但 DRY 也有边界。在微服务架构中,每个服务应该有自己的数据模型,即使它们在某些字段上看起来"重复"。这是因为每个服务代表一个独立的 bounded context,同一个词在不同上下文中可能有不同的含义。强行统一会导致服务间的紧耦合,违反正交性原则。
因此,务实的 DRY 需要判断:这个重复是"有害的重复"(同一知识的多份拷贝,修改时容易遗漏)还是"必要的重复"(不同上下文中的独立概念,恰好有相似的表面形式)?前者必须消除,后者应该保留。
讨论二:正交性与系统演化
正交性不仅关乎代码质量,更关乎系统的长期演化能力。一个正交的系统,其各个组件可以独立地升级、替换、扩展,而不会影响其他组件。这使得系统能够适应不断变化的需求和技术环境。
考虑一个典型的 Web 应用:如果 UI 层直接调用数据库查询,那么更换数据库就意味着同时修改 UI 代码——这是不正交的。通过引入 Service 层和 Repository 层,每一层只依赖下一层的接口而非实现,你就可以独立地更换数据库而不影响 UI——这就是正交性。
正交性还有一个经常被忽视的维度:团队的正交性。如果两个团队的工作高度耦合——需要频繁沟通、协调发布——那么团队的效率会受到严重影响。通过良好的架构设计,让不同团队负责不同的正交模块,可以显著提高组织的整体效率。这就是 Conway's Law 的积极应用。
讨论三:估算的心理学陷阱
软件估算之所以不准确,不仅仅是因为技术问题,更因为心理因素。锚定效应(Anchoring Effect)会让你的估算受到第一个数字的影响;乐观偏差(Optimism Bias)会让你低估完成任务所需的时间;承诺升级(Escalation of Commitment)会让你在已经超期的情况下继续坚持原来的估算。
对抗这些心理陷阱的方法是:使用基于数据的估算而非直觉。参考类似任务的历史数据,建立团队的估算基准。使用"计划扑克"(Planning Poker)等团队估算方法,通过多轮独立估算和讨论来减少个人偏差。
另一个有效的技巧是使用范围估算而非点估算。"这个任务需要5天"不如"这个任务需要3到7天"诚实和有用。范围估算承认了不确定性,给决策者提供了更完整的信息。
四、实践案例
案例一:DRY 原则在微服务中的权衡
某电商平台最初采用单体架构,商品价格在多个模块中被引用:订单模块、购物车模块、推荐模块、报表模块。所有模块共享同一个数据库和同一份 Price 模型。这符合 DRY 原则——价格信息只有一个权威来源。
随着业务增长,系统拆分为微服务。最初团队试图保持"DRY"——所有服务共享一个 price-service,其他服务通过 RPC 调用获取价格。但这导致了严重的性能问题:每次页面加载需要数十次 RPC 调用,延迟急剧上升。
最终团队采用了"数据复制"的策略:每个需要价格信息的服务维护自己的价格缓存副本,通过事件驱动的方式同步更新。虽然表面上违反了 DRY(价格在多个地方存在副本),但每个副本都有明确的同步机制和一致性保证。这是一个"必要的重复"——用可控的复杂性换取了显著的性能提升。
案例二:牵引线方法在遗留系统重构中的应用
某金融公司需要重构一个有十年历史的交易系统。团队决定采用牵引线方法:首先选择最简单的一个交易类型(单笔买入),从用户提交订单到最终结算,建立一条完整的端到端路径。
这条牵引线只有最基本的功能:接受订单、验证参数、记录数据库、返回确认。但它走通了整个技术栈——新的 API 框架、消息队列、数据库 schema、监控告警。团队用两周时间完成了这条牵引线,验证了新技术栈的可行性。
之后,团队逐步将其他交易类型添加到这条路径上,每添加一个类型就是一次增量交付。与"先搭好所有基础设施再开发功能"的传统方式相比,牵引线方法让团队在第一周就有了可演示的成果,极大地增强了利益相关者的信心。
五、常见误区与注意事项
误区一:把 DRY 等同于"不要复制粘贴"
DRY 的层次远不止代码级别。配置文件、数据库 schema、API 契约、部署流程——所有包含"知识"的地方都应该遵循 DRY。同时,也不要过度应用 DRY——如果两段代码只是"看起来相似"但服务于不同的目的,强行合并反而会降低可读性。
误区二:为了正交性引入过多抽象层
正交性是目标,但实现正交性的手段(抽象层、接口、中间件)本身也有成本。过多的抽象层会增加代码的理解难度和调试复杂度。务实的做法是:只在真正需要隔离变化的地方引入抽象,而不是为每一层都创建一个接口。
误区三:混淆原型和牵引线
原型是可以丢弃的实验代码,牵引线是要保留的生产代码骨架。如果你把原型代码直接投入生产,你就是在积累技术债务。如果你把牵引线当作原型随意丢弃,你就是在浪费有效的工作。
误区四:估算时忽略"不可见工作"
很多估算只考虑了"写代码"的时间,忽略了代码审查、会议沟通、环境搭建、测试调试、文档编写、部署发布等环节。这些"不可见工作"通常占总工时的30%-50%。
误区五:把灵活性当作设计目标而非手段
灵活性本身不是目的,它是应对变化的手段。在投入成本增加灵活性之前,先问:变化真的会发生吗?变化的概率和成本分别是多少?如果增加灵活性的成本高于应对变化的成本,那就不要做。
六、本章小结
本章介绍了务实程序员的核心方法论。DRY 原则要求系统中的每一处知识都有唯一的权威表示,它是可维护性的基石。正交性要求系统组件彼此独立,它是可演化性的保障。牵引线和原型是两种不同的探索策略,适用于不同的场景。
估算是一门需要持续练习的艺术,它需要数据支撑而非纯靠直觉。灵活性的投资需要谨慎评估——只为真正可能发生的变化买单。所有这些原则的共同主题是:在理想与现实之间找到平衡,做出明智的权衡。
务实的态度不是妥协,而是在充分理解各种原则和约束之后,做出最适合当前情境的选择。这需要经验、判断力和勇气——而这些都只能通过持续的实践来培养。
七、思考题与练习
在你当前的项目中找出三处违反 DRY 原则的地方,评估修复的成本和收益,选择最值得修复的一处进行重构。
分析你最近修改的一个 bug:这个 bug 的根因是否与正交性不足有关?如果是,如何改进设计来避免类似问题?
选择一个你正在考虑的技术方案,分别用原型和牵引线的方法进行探索,比较两种方法的优劣。
对你下一个开发任务进行估算:先给出点估算,再给出范围估算,记录实际耗时,分析偏差原因。
在代码库中搜索所有 "TODO" 和 "FIXME" 注释,分析哪些是"破窗"需要立即修复,哪些可以合理推迟。
设计一个小型系统(如图书管理系统),画出模块依赖图,评估其正交性。尝试重新设计以提高正交性。
回忆一次你因为"过度设计"而浪费时间的经历。如果重来一次,你会如何在灵活性和简洁性之间做出更好的权衡?
与团队成员进行一次"计划扑克"估算练习,比较个人估算和团队估算的差异,讨论差异的来源。