本系列中我所用的很多示例都是依赖将决策推迟到最后责任时刻 的 Lean Software 概念。但是当最后时刻到来时您又如何知道呢?您又如何知道在有更好的途径出现之前可以将设计决策推进到未来多远?什么项目在紧急设计中受益最大?最后一期 演化架构和紧急设计 将回答您这些问题,然后总体回顾本系列的要点。
开始之前,我先引用一下 Donald Rumsfeld 的诗。
世界上有已知的未知,也就是说我们知道我们还不知道的那些事情。但是,还有未知的未知,那些我们不知道我们不知道的事情。
—Donald Rumsfeld,美国前国防部长
Rumsfeld 对未知进行区分,有些我们知道存在但是我们找不到,但是他也承认还有一些存在是超出我们的知识和经验的,我们甚至不知道要去寻找:这就是 “未知的未知”。
Rumsfeld 讨论的是战争的不确定性,但也是在说软件。如果您曾编写过一些重要软件,您可能涉及过未知之未知 问题 — 软件设计中最大的一个问题。您认为您对解决方案实现过程中所遇到的问题都有一个很好的理解,但是不可避免还是会出现一些不可预料的问题。同一个开源框架交互不是您想的那么简单,问题可能比原来所预计的更细微,需要更加精细缜密。
不可预期的设计问题会不断出现,因为软件的容错能力很低 — 比物理系统低很多。例如,以您身边的建筑物为例,构建一个大楼是一个多人数月(甚至数年)的项目。现在考虑类似时间段上的一个多人软件项目。这些项目有类似的规模,但是物理建筑往往更加宽容结构缺陷。如果一个开关不能完全掩盖墙上安装电线的洞,整个大楼不会倒塌。但是软件中的一个小问题可能会导致系统崩溃。将其比作原子,位元的容错都是不可原谅的。当然,软件易于修复:我们可以在洞上抹墙粉(修复 bug)立即改造房子。
如果一个机翼脱落了,工程师首先会查看机翼连接的位置:在物理系统上,错误和功能的临近度通常很高。但是在软件中,执行一行可能导致不稳定的代码,通常会使数百行甚至几千行似乎不相关的代码出现问题。
软件预先设计(Design up front)比较困难,因为各个部分之间以无数种、甚至不可预期的方式相互作用。预测这些交互的实现目前不在我们的能力范围之内,紧急设计包含这些不可避免的令人惊讶的复杂性,试着减少变化的破坏。
紧急设计不是一个二元状态。您不能肯定地说您的设计是 100% 敏捷的或是 0 敏捷的;有 图 1 所示的这样一个波谱:
图 1. 图 1. 设计波谱
在 图 1 的最左边,有传统的 Big Design Up Front (BDUF) ,具体反映在很多常见开发技术中。BDUF 以其优秀的稻草人(straw-man)形式体现了象牙塔架构,创建设计工件,不作任何更改直接交给开发人员来进行实现。在 nonparody 格式中,该设计方法努力尝试在编码之前找出所感兴趣的一切。这是一个预测模型,设计软件的预测模型。
图 1 右边显示了您在中学时期所做的各种编码:您可以进行修改使之运行,然后继续改进。这在小范围内可以很好地运行(基本上,只要这个问题小到您用脑子想想就可以解决),但是不能超出太多。
紧急设计通常在两个极端情况下失效,但是和左边比起来更趋向于右边。紧急设计是一个响应式的、被动的软件设计方法。
面对这么多的失败和不被看好的项目,还有这么多组织机构继续使用 BDUF,真的很令人费解!我并不是说您不能成功地使用 BDUF。(事实上,我曾做过很多这类项目。)但是数十年间关于这个开发技术的记录很少。Fred Brooks 的重要著作 Mythical Man Month 探讨了以该模式构建软件存在的问题,于 1975 年发布(见 参考资料)。
团队想使用这种风格进行开发并不奇怪,因为这更符合设计在传统工程中的工作方式。如果您正在设计小部件或 iPods,您必须进行所有的预先设计,因为您不能重构原子。看看原始的 Intel Pentium 处理器。在它发布之后,在浮点数据单元中发现了一个 bug,需要每个操作系统创建者来编写特定代码解决这个问题。一旦操作完成,您就不能对硬件进行更改。软件截然相反:软件项目的多数生命周期是在原始版本发布之后发生,通过增强、bug 修复、以及其他 “维护” 活动。我们处理位元而不是原子,位元的可塑性是无限的。
敏捷设计并不是要在项目开始阶段忽略设计。您对问题的本质知道得越少,在此过程的早期您所能做的就越少。我常常会问,“您如何决定在一个项目开始阶段进行多少设计合适?” 不同项目有不同的准则,它们在种类和复杂程度上的变化比多数非技术人员所意识到的要多很多。大致上说,您需要平衡两件事:如果您的早期决策足够精准可以避免日后进行更改,那么要考虑预先设计。如果后期修改成本太大,就要考虑收益问题。因此,您需要对早期阶段以及开发技术(可能会是后期更改代价昂贵)有一个很好的了解 。敏捷设计反对这个观点,因为人们倾向于过高地估计他们在早期制定精确决策的能力,因而他们将遭受不断扩大的后果。
这里是一些项目示例,了解一下您应该为哪些项目做更多的预先设计:
- 有严格的稳定性需求,数年之内没有更改计划的项目。
- 高度隔离的环境(比如太空傲游,水底探险),出于安全性考虑的项目。
- 您所编写的项目与之前的软件完全相同、同一组人、没有范围变化。(您将对该项目有一个极度精确的评估。)
- 对环境高度约束的项目,比如,嵌入式系统,确保您考虑到了环境约束条件。(我仍然会尽量使行为功能性尽可能多地出现。)
您不需要进行太多预先设计的项目有:
- 有高度可变性、项目需求不断变化的项目(几个月或几周),像多数商业应用程序。
- 需要响应外部因素(比如,市场条件)的项目。
- 您还不能确定技术或业务细节的项目,注意这实际上包括每个项目,又回到 Rumsfeld 的 “未知之未知” 理论上了。
- 从订阅到部署都可以从中收益的项目,而不是那些人为地区分为 “已完成的” 项目。没有软件项目是彻底完成的,因此您总是需要购买一个订阅,您越早认识到这一点,就表现的越像。
选择正确的时间制定决策比较困难,但很重要。所有项目都是独一无二的,给出具体建议无用。但是,这有一些通用的指导方针:
- 注意当 “最简单的工作” 已经工作一段时间之后,需要解决的问题变得更重要更复杂,例如,假设您正在使用一个数据库作为基础后台、异步活动的一个简单消息传递队列。然而现在,在性能变差的同时将两个新需求添加到各种异步活动中了。现在是重新访问您的 “最简单事情” 决策的最佳时间,因为您的解决方案不能匹配该准则。
- 在查看问题的整体规模时,试着隔离将趋向紧急设计技术的数据包。例如,假设您正在处理一个需要地理编码支持的应用程序,您不可能在此之前就使用一个地理编码库。您应该执行一些 spikes(简单直接的研究与开发项目)来确保您可以理解评估目的。尽量避免在不完全理解的基础上制定架构决策(后期很难改变),重新访问应用程序其余部分和这个隔离部分之间的接口点,看看是否能在更好理解的基础上进行改进。
- 试着保留应用程序之间的交互作用点。Simple Object Access Protocol (SOAP) 这类协议的一个副作用是在刚性结构和强类型(strong types)上的持久性。灵活性的秘密是特异性少,这种认识推进了 Representational State Transfer (REST) 和相关技术的广泛应用。构建一个 API 时,试着接受最通用的合理性,这也将应用于集成。
我将对这 18 期中的主要主题进行总结,结束本系列。
回到 “演化架构和紧急设计:利用可重用代码,第 1 部分”,我引用 Jack Reeves 的这篇文章,他认为软件设计是由一个项目的全部源代码构成,而不仅仅是我们通常所认为的工件(白板图(white-board diagrams)、Unified Modeling Language,等等)。他将源代码和工程的规划进行了比较,工程师的规划指出了生产团队将设计转换成原子所需的一切。我们的规划是源代码,编辑器将其转换成位元。如果对代码进行设计,那么我们所用的计算机语言和架构就定义了我们可以 设计的原始资料。
功能强大的语言的影响优势抵消了对其先进功能的恐惧。尽管选择和标准化有 10 多年历史的语言很不错,因为您有足够充足的低成本开发人员,但是您必须接受这个事实,您不能赶上您的竞争对手,因为他们有更现代的语言和工具。很多机构过分强调标准化,甚至以创新和稳键为代价。我曾见过很多被迫使用标准框架的项目,这很可笑也不利于问题发现,这会妨碍任何类型的设计问题发现,因为会被意外噪音所干扰。
这并不意味着您就允许每个开发人员都选择使用自己的工具堆栈。这意味着您要密切关注标准化所提供的真正价值,然后考虑合理方案。例如,或许您想继续使用 Java 技术开发您的项目,您可以使用更高级的工具进行构建、测试和其他开发任务。软件项目中代码似乎无处不在,这一切都体现在设计中,不管是有意识还是无意识的。
许多方法都想要避免不确定性。如果您使用原子方式进行构建,不确定性很糟糕,因为一旦原子被强制形成一定形状再更改其配置代价极为昂贵。但是,如果您使用位元进行构建,更改十分容易且十分必要。在软件中避免更改很困难且没有必要。
敏捷方法想要找到一种方法使得修改更为容易,使用诸如单元测试、重构、持续集成和交互开发之类技术。紧急设计体现在设计的敏捷哲学中,当出现设计决策时,问自己:
- 我现在需要制定这个决策吗?
- 我可以安全地推迟这一决策吗?
- 我能做些什么使决策可逆?
如果您是在这样一个易于重构代码的环境中,临时制定一个非最佳决策并不可怕,因为您可以轻松地进行修复。如果您设置您的项目以适应变化,推迟决策不会发生故障,因为您可以实时优化。接受修改需要有能力客观对待决策以及更改那些使事情糟糕的决策。
紧急设计的另一方面是能够看见 您代码中的有用设计元素,然后作为惯用模式 进行保存,这是我在 “演化架构和紧急设计:利用可重用代码,第 1 部分” 中作为已有问题的有效解决方案定义的。这些已发现模式就像是您公司皇冠上的珠宝,因为它们是实现公司运行方式的设计元素。令人担忧的是,您所编写的少量代码囊括了惟一值:余下的大部分只是拖放到数据库、构建 HTML,等等。惯用模式比在 Joint Application Design (JAD) 或象牙塔设计中编造的模式要更有价值,因为它们已经达到了实用的关键条件之一:它们已经被用来解决了一个实际问题。
您可以通过各种方法收获惯用模式。您可以创建一个 API,这只是很简单并没有什么特别的 — 看起来和您所使用的其他 API 没什么两样。您也可以捕获一些使用元程序和属性的惯用模式(见 “演化架构和紧急设计:利用可重用代码,第 2 部分”)。在 “演化架构和紧急设计:使用 DSL”、“演化架构和紧急设计:连贯接口”、“演化架构和紧急设计:使用 Groovy 构建 DSL” 和 “演化架构和紧急设计:使用 JRuby 构建 DSL” 中,我也讨论了使用特定域语言捕获惯用模式。
总是想要识别有用设计元素,避免制定人工难以执行的决策。例如,当这些元素被添加到代码基中,在您没有使用它们时 向您的应用程序(比如,可扩展层或面向服务架构的服务)中添加架构元素使您的代码更为复杂。务必确保在正确的时间添加复杂性,因为在您使用它之前,它都是偶发复杂性。
本系列的一个目标是强迫自己以不同的方式审视设计并记录该过程。我希望您也可以享受我的旅程。我没有放弃我旅行,只是换了个话题。请继续关注我的下一个关于函数式思考的 developerWorks 系列。
学习
- The Productive Programmer(Neal Ford,O'Reilly Media,2008 年):Neal Ford 最新书籍,扩展了本系列中的很多主题。
-
Donald Rumsfeld 的诗:Rumsfeld 的部分诗作收集在 verse form 和 set to music 中。
- The Mythical Man Month,第 2 版(Fred Brooks,Addison-Wesley,1995 年):本书是软件开发方面的开创性著作,揭示了软件项目的一些不直观但真实的怪癖。
-
浏览 Java 技术书店,阅读关于这些和其他技术主题的图书。
-
developerWorks 中国网站 Java 技术专区:这里有数百篇关于 Java 编程各个方面的文章。
讨论
- 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和 wikis,并与其他 developerWorks 用户交流。

Neal Ford 是一家全球性 IT 咨询公司 ThoughtWorks 的软件架构师和 Meme Wrangler。他的工作还包括设计和开发应用程序、教材、杂志文章、课件和视频/DVD 演示,而且他是各种技术书籍的作者或编辑,包括最近的新书 The Productive Programmer 。他主要的工作重心是设计和构建大型企业应用程序。他还是全球开发人员会议上的国际知名演说家。请访问他的 Web 站点。