设计已死?

发表于:2008-02-02来源:作者:点击数: 标签:设计
对很多粗略接触到 Extreme Programming 的人来说,XP 似乎 宣告了软件设计的死刑。不只很多的设计被嘲笑为 "Big Up Front Design"[译注1],连很多技术像UML、富有弹性的程序架构 (framework),甚至连模式 (pattern) 都不受重视,或是近似忽略了。事实上,XP内
对很多粗略接触到 Extreme Programming 的人来说,XP 似乎 宣告了软件设计的死刑。不只很多的设计被嘲笑为 "Big Up Front Design"[译注1],连很多技术像UML、富有弹性的程序架构 (framework),甚至连模式 (pattern) 都不受重视,或是近似忽略了。事实上,XP内含很多设计理念,但是它与现有的软件流程有着不同的运作方式。XP藉由多种实务技巧 (practice) 赋予演进式设计 (evolutionary design) 崭新的风貌,让演进变成一种实用的设计方法。它也让设计者 (designer[译注2]) 面临新的挑战与技巧,学习如何使设计精简,如何使用重构来保持一个设计的清楚易懂,以及如何逐步地套用模式。

  (这篇文章是我在 XP2000 研讨会发表的演说,它会公布在研讨会讲义中。)

  Planned and Evolutionary Design (经过规划的设计与演进式的设计)

  The Enabling Practices of XP (XP有效的实作技巧)

  The Value of Simplicity (简单的价值)

  What on Earth is Simplicity Anyway (究竟什么是简单)

  Does Refactoring Violate YAGNI? (重构违反了YAGNI吗?)

  Patterns and XP (模式与XP)

  Growing an Architecture (发展结构)

  UML and XP (UML与XP)

  On Metaphor (关于隐喻)

  Do you wanna be an Architect when you grow up? (你将来想成为一个软件结构师吗?)

  Things that are difficult to refactor in (很难重构的东西)

  So is Design Dead? (所以,设计死了吗?)

  Acknowledgements (致谢)

  Revision History (修订的记录)

  Extreme Programming (XP) 挑战很多软件开发常见的假设。其中最受争议的就是极力排斥 up-front design,而支持一种比较属于演进的方式。批评者说这是退回到了 "code and fix" 的开发方式,顶多只能算是一般急就章的程序设计罢了。支持者也常看到 XP 对于设计方法 (如 UML)、principle(设计准则)、patterns 等的排斥。别担心,仔细注意你的程序代码,你就会看到好的 design 浮现出来。

  我发现自己正陷于这个争论当中。我的工作着重在图形化设计语言 - UML 以及 patterns,事实上我也写过 UML 和 patterns 的书。我如此的拥抱 XP 是否表示我放弃了这些理论,或是将这些反渐进式 (counter-revolutionary) 的概念从脑中清除了?

  嗯... 我不想让你的心悬荡在这两种情境中。简单的说并不是;接下来的文章就让我来详细说明。

  Planned and Evolutionary Design

  我将在这篇文章中说明软件开发的两种设计方式是如何完成的。或许最常见的是演进式设计。它的本质是系统的设计随着软件开发的过程增长。设计 (design) 是撰写程序代码过程的一部份,随着程序代码的发展,设计也跟着调整。

  在常见的使用中,演进式设计实在是彻底的失败。设计的结果其实是一堆为了某些特殊条件而巧妙安排的决定所组成,每个条件都会让程序代码更难修改。从很多方面来看,你可能会批评这样根本就没有设计可言,无疑地这样的方式常会导致很差劲的设计。根据Kent的陈述,所谓的设计 (design) 是要能够让你可以长期很简单地修改软件。当设计 (design) 不如预期时,你应该能够做有效的更改。一段时间之后,设计变得越来越糟,你也体会到这个软件混乱的程度。这样的情形不仅使得软件本身难以修改,也容易产生难以追踪和彻底解决的 bug。随着计画的进行,bug 的数量呈指数地成长而必须花更多成本去解决,这就是 "code and fix" 的恶梦。

  Planned Design 的做法正好相反,并且含有取自其它工程的概念。如果你打算做一间狗屋,你只需要找齐木料以及在心中有一个大略的形象。但是如果你想要建一栋摩天大楼,照同样的做法,恐怕还不到一半的高度大楼就垮了。于是你先在一间像我太太在波士顿市区那样的办公室里完成工程图。她在设计图中确定所有的细节,一部份使用数学分析,但是大部分都是使用建筑规范。所谓的建筑规范就是根据成功的经验 (有些是数学分析) 制定出如何设计结构体的法则。当设计图完成,她们公司就可以将设计图交给另一个施工的公司按图施工。

  Planned Design 将同样的方式应用在软件开发。Designer 先定出重要的部份,程序代码不是由他们来撰写,因为软件并不是他们 "建造[译注3]" 的,他们只负责设计。所以 designer 可以利用像 UML 这样的技术,不需要太注重撰写程序代码的细节问题,而在一个比较属于抽象的层次上工作。一旦设计的部份完成了,他们就可以将它交给另一个团队 (或甚至是另一家公司) 去 "建造"。因为 designer 朝着大方向思考,所以他们能够避免因为策略方面不断的更改而导致软件的失序。Programmer 就可以依循设计好的方向 (如果有遵循设计) 写出好的系统。

  Planned design 方法从七○年代出现,而且很多人都用过它了。在很多方面它比 code and fix 渐进式设计要来的好,但是它也有一些缺点存在。第一个缺点是当你在撰写程序代码时,你不可能同时把所有必须处理的问题都想清楚。所以将无可避免的遇到一些让人对原先设计产生质疑的问题。可是如果 designer 在完成工作之后就转移到其它项目,那怎么办?Programmer 开始迁就设计来写程序,于是软件开始趋于混乱。就算找到 designer,花时间整理设计,变更设计图,然后修改程序代码。但是必须面临更短的时程以及更大的压力来修改问题,又是混乱的开端。

  此外,通常还有软件开发文化方面的问题。Designer 因为专精的技术和丰富的经验而成为一位 designer。然而,他们忙于从事设计而没有时间写程序代码。但是,开发软件的工具发展迅速,当你不再撰写程序代码时,你不只是错失了技术潮流所发生的改变,同时也失去了对于那些实际撰写程序代码的人的尊敬。

  建造者 (builder[译注3]) 和设计者之间这种微妙的关系在建筑界也看得到,只是在软件界更加凸显而已。之所以会如此强烈是因为一个关键性的差异。在建筑界,设计师和工程师的技术有清楚的分野;在软件界就比较分不清楚了[译注2]。任何在高度注重 design 的环境工作的 programmer 都必须具备良好的技术,他的能力足够对 designer 的设计提出质疑,尤其是当 designer 对于新的发展工具或平台越来越不熟悉的状况下。

  现在这些问题也许可以获得解决。也许我们可以处理人与人之间的互动问题。也许我们可以加强 designer 的技术来处理绝大部份的问题,并且订出一个依照准则去做就足够改变设计图的流程。但是仍然有另外一个问题:变更需求。变更需求是软件项目中最让我感到头痛的问题了。

  处理变更需求的方式之一是做有弹性的设计,于是当需求有所更改,你就可以轻易的变更设计。然而,这是需要先见之明去猜测将来你可能会做怎样的变更。一项预留处理易变性质的设计可能对于将来的需求变更有所帮助,但是对于意外的变化却没有帮助 (甚至有害)。所以你必须对于需求有足够的了解以隔离易变的部份。照我的观察,这是非常困难的。

  部份有关需求的问题应该归咎于对需求的了解不够清楚,所以有人专注于研究需求处理,希望得到适切的需求以避免后来对设计的修改。但是即使朝这个方向去做一样无法对症下药。很多无法预料的变更起因于瞬息万变的商场,你只有更加小心处理需求问题来应付无法避免的情况。

  这么说来,planned design 听起来像是不可能的任务。这种做法当然是一种很大的挑战。但是,跟演进式设计 (evolutionary design) 普遍以 code and fix 方式实作比较起来,我不觉得 planned design 会比较差。事实上,我也比较喜欢 planned design。因为我了解 planned design 的缺点,而且正在寻找更好的方法。

  The Enabling Practices of XP

  XP 因为许多原因而备受争议,其中之一就是它主张演进式设计 (evolutionary design) 而不是 planned design。我们也知道,演进式设计可能因为特定的设计或是软件开发趋于混乱而行不通。

  想了解这些争论的核心,就是软件研发异动曲线。曲线的变化说明,随着项目的进行,变更所需要的成本呈现指数的增加。这样的曲线常以一句话来表示:在分析阶段花一块钱所作的变更,发行之后要花数千元来补救。讽刺的是大部分的计画仍然没有分析过程而以非标准的方式进行,但是这种成本上的指数关系还是存在着。这种指数曲线意味着演进式设计可能行不通,它同时也说明着为什么 planned design 要小心翼翼地规划,因为任何的错误还是会面对同样的问题。

  XP 的基本假设是它可以将这种指数曲线拉平,这样演进式设计就行得通了。XP 使曲线更平缓并能运用这种优势。这是因为 XP 实作技巧之间的耦合效果:换句话说,不使用那些能够拉平软件开发曲线的实作技巧来工作,这条曲线也不会趋向平缓。这也是争论的来源,因为评论家不了解这其间的关系。通常这些批评是根据评论家自身的经验,他们并没有实行那些有效的实作技巧,当他们看到结果不如预期,对于 XP 的印象也就是这样了。

  这些有效的实作技巧有几个部份,主要是 Testing 和 Continuous Integration。如果没有 testing 提供保障,其它的 XP 实作技巧都不可行。Continuous Integration 可以保持团队成员信息同步,所以当你有改变的部份,不必担心与其它成员资料整合会有问题。同时运用这些实作技巧能够大大影响开发曲线。这让我再次想起在ThoughtWorks 导入 testing 和 continuous integration 之后,明显的改善了研发成果。改善的程度好到令人怀疑是不是像 XP 所主张的,必须要用到所有的实作技巧才能大幅改善效率。[译注4]

  Refactoring 具有类似的成效。那些曾经采用 XP 建议的原则来对程序代码进行refactoring 的人发现,这么做要比无章法或是特殊方式的 restructuring 明显的更有效率。那也曾经是 Kent 指导我适当的 refactor 得到的难忘经验,也因为这么一次巨大的转变促使我以这个主题写了一本书。

  Jim Highsmith 写了一篇很棒的文章 "summary of XP",他把 planned design 和 refactoring 放在天秤的两端。大部份传统的做法假设构想不变,所以 planned design 占优势。而当你的成本越来越不允许变更,你就越倾向于采用 refactoring。Planned design 并不是完全消失,只是将这两种做法互相搭配运用取得平衡。对我来说,在设计进行 refactoring 之前,总觉得这个设计不够健全。

  Continuous integration、testing 和 refactoring 这些有效的实作方法让 evolutionary design 看似很有道理。但是我们尚未找出其间的平衡点。我相信,不论外界对 XP 存有什么印象,XP 不仅仅是 testing、coding 和 refactoring。在 coding 之前还有 design 的必要。部份的 design 在 coding 之前准备,大部份的 design 则发生在实作每一项详列的功能之前。总之,在 up-front design 和 refactoring 之间可以找到新的平衡。

  The Value of Simplicity

  XP 大声疾呼的两个口号是 "Do The Simplest Thing that Could Possibly Work"(只做最简单可以正常运作的设计) 和 "You Aren't Going to Need It"(就是 YAGNI - 你将不会需要它)。两项都是XP实务中简单设计的表现形式。

  YAGNI 一词时常被讨论,它的意思是现在不要为了将来可能用到的功能加入任何程序代码。表面上听起来好象很简单,问题则出在像 framework、重用组件、和弹性化设计,这些东西本来就很复杂。你事先付出额外的成本去打造它们,希望稍后将这些花费都赚回来。这个事先弹性设计的想法被认为是软件设计有效率的关键部份。

  但XP的建议是,在处理第一个问题时不要因为可能需要某项功能,就建造出弹性的组件组及框架出来。让整体结构随着需要成长。假如我今天想要一个可以处理加法但是不用乘法的 Money 类别,我就只在 Money 类别中建造加法的功能。就算我确定下一个阶段也需要乘法的运算,而且我知道很简单,也花不了多少时间,我还是会留到下一阶段再去做它。

  其中一个理由是效益。如果我要花时间在明天才需要的功能,那就表示我没有将精神放在这个阶段应该完成的事情上。发表计画详列目前要完成的事项,现在做以后才需要的事情违背开发人员和顾客之间的协议。这种做法有让现阶段的目标无法达成的可能。而且这个阶段的 stroies[译注5] 是否具有风险,或是需不需要做额外的工作,都是由顾客来决定的 - 还是可能不包括乘法功能。

  这种经济效益上的限制是因为我们有可能出错。就算是我们已经确定这个功能应该如何运作,都有可能出错 - 尤其是这时候我们还没有取得详细需求。提前做一件错误的事情比提前做一件对的事情更浪费时间。而且XP专家们通常相信我们比较有可能会做错而不是做对(我心有戚戚)。

  第二个支持 simple design 的理由是复杂的设计违反光线行进的原理。复杂的设计比简单的设计还要令人难懂。所以随着渐增的系统复杂度,更加难以对系统做任何修改。如此,若系统必须加入更复杂的设计时,成本势必增加。

  现在很多人发现这样的建议是无意义的,其实他们那样想是对的。因为你所想象一般的研发并没有被 XP 有效的技巧所取代。然而,当规划式设计和渐进式设计之间的平衡点有了变化 (也只有当这样的变化发生时),YAGNI 就会变成好的技巧。

  所以结论是,除非到了往后的阶段有所需要,否则你不会浪费精神去增加新的功能。即使不会多花成本,你也不会这样做,因为就算现在加入这些功能并不增加成本,但是却会增加将来做修改时的成本。总之,你可以在套用 XP 时明智的遵守这样的方法,或是采取一种能降低成本的类似的方法。

  What on Earth is Simplicity Anyway

  因此,我们希望程序代码能够越简单越好,这听起来没什么好争论的,毕竟有谁想要复杂呢?但问题来了,究竟 "什么才叫简单呢?"

  在 XPE 一书中,Kent 对简单系统订了四个评量标准,依序是 (最重要排最前面):

  通过所有测试

  呈现所有的意图。

  避免重复。

  最少数量的类别或方法。

  通过所有测试是一项很普通的评量标准,避免重复也很明确,尽管有些研发人员需要别人的指点才能做到。比较麻烦的是 "呈现所有的意图"这一项,这到底指的是什么呢?

  这句话的本意就是简单明了的程序代码。XP 对程序代码的易读性有很高的标准。虽然在 XP 当中,"巧妙的程序代码 (clever code)" 这个字眼经常被滥用,不过意图清楚的程序代码,对其他人来说真的是一种巧妙。Josh Kerievsky 在 XP 2000 论文中举了一个很好的例子,检视在 XP 领域可能是大家最熟知的 JUnit 的程序代码。JUnit 使用 decorators 在 test cases 中加入非必要的功能,像是同步机制及批次设定等,将这些程序代码抽出成为 decorator,的确让一般的程序代码看起来清楚许多。

  但是你必须扪心自问,这样做之后的程序代码够简单吗?我觉得是,因为我了解 Decorator 这个 patterns。但是对于不了解的人来说还是相当复杂的。类似的情况,JUnit 使用 pluggable method,一种大部分的人刚开始接触时都不会觉得简单的技巧。所以,也许我们可以说 JUnit 对有经验的人来说是比较简单的,新手反而会觉得它很复杂。

  XP 的 "Once and Only Once" 以及 Pragmatic Programmer(书名) 的 DRY(Don't Repeat Yourself) 都专注在去除重复的程序代码。这些良好的建议都有很显著而且惊人的效果。只要依照这个方式,项目就可以一路顺利的运作。但是它也不能解决所有问题,简单化还是不容易达成。

  最近我参与一个可能是过度设计的项目,系统经过 refactor 之后去除部份弹性的设计。但是就像其中一位开发者所说的 "重构过度设计的系统要比重构没有设计的要来的容易多了" 做一个比你所需要简单一点的设计是最好的,但是,稍微复杂一点点也不是什么严重的事情。

  我听过最好的建议来自 Bob 大叔 (Robert Martin)。他的建议是不要太在意什么是最简单的设计。毕竟后来你可以,应该,也会再重构。愿意在最后重构,比知道如何做简单的设计重要得多。

  Does Refactoring Violate YAGNI?

  这个主题最近出现在 XP 讨论区上,当我们审视设计在 XP 扮演的角色时,我觉得很值得提出来讨论。

  基本上这个问题起因于重构需要耗费时间却没有增加新的功能。而 YAGNI 的观点是假设你为了眼前的需要做设计而不是未来,这样算是互相抵触吗?

  YAGNI 的观点是不要增加一些现阶段不需要的复杂功能,这也是简单设计这项技巧的部份精神。重构也是为了满足尽可能保持系统的简单性这个需要,所以当你觉得可以让系统变得更简单的时候,就进行重构。

  简单设计不但利用了 XP 的实务技巧,本身也是其中一项有用的实务技巧。唯有伴随着测试,持续整合,及重构的运用,才能有效地做出简单设计。同时,让研发异动曲线保持平缓的基础也就是保持设计的简单。任何不必要的复杂都会让系统变得难于调整,除非这个复杂性是你为了所预测的弹性而加入的。不过,人们的预测通常都不太准确,所以最好还是努力地保持简单性。

  不管怎样,人们不太可能第一次就能够获得最简单的东西,因此你需要重构来帮助你更接近这个目标。

  Patterns and XP

  JUnit 的例子让我不得不想到 patterns。XP 和 patterns 之间的关系很微妙,也常常被问起。Joshua Kerievsky 认为 patterns 在 XP 被过分轻视,而且他所提出的理由也相当令人信服,我不想再重提。不过值得一提的是,很多人都认为 patterns 似乎与 XP 是有冲突的。

  争论的本质在于 patterns 常被过度滥用。世上有太多传奇性的 programmer,第一次读到四人帮以 32 行程序代码阐述 16 种 patterns 这样的事情还记忆犹新[译注6]。我还记得有一晚与 Kent 喝着醇酒一起讨论一篇文章 "Not Design patterns: 23 cheap tricks (不要用设计模式-23 个简单的诀窍)"。我们认为那不过是以 if 条件式来取代 strategy 这个 pattern 罢了。这样的笑话有个重点,patterns 被滥用了。但并不表示 patterns 是不足取的,问题在于你要怎么运用它。

  其中一项论点是简单设计的力量自然会将项目导向 patterns。很多重构的例子明确地这么做,或者甚至不用重构,你只要遵从简单设计的规则就会发现 patterns,即使你还不知道 patterns 是什么。这样的说法也许是真的,不过它真的是最好的方式吗?当然如果你先对于 patterns 有个大略的了解,或者手边有一本书可以参考,会比自己发明新的 patterns 要好些。当我觉得一个 pattern 快浮现的时候,我必定会去翻翻 GOF 的书。对我来说,有效的设计告诉我们 pattern 值得付出代价去学习-那就是它特有的技术。同样地就像 Joshua 所建议的,我们需要更熟悉于如何逐步地运用 patterns。就这一点而言,XP 只是与一般使用 patterns 的方式不同而已,并没有抹煞它的价值。

  但是从讨论区一些文章看来,我觉得很多人明显地认为 XP 并不鼓励使用 patterns,尽管 XP 大部分的提倡者也都是之前 patterns 运动的领导者。因为他们看到了不同于 patterns 的观点吗?或是他们已经将 patterns 融入思考而不必再去理解它?我不知道其它人的答案是什么,但是对我来说,patterns 仍然是非常重要的。XP 也许是开发的一种流程,但 patterns 可是设计知识的骨干,不管是哪种流程这些知识都是很有用的。不同的流程使用 patterns 的方式也就不同,XP 强调等到需要时才使用 patterns 以及透过简单的实作逐步导入 patterns。所以 patterns 仍然是一种必须获得的关键知识。

  我对于采用 XP 的人使用 patterns 的建议:

  花点时间学习 patterns。

  留意使用 patterns 的时机 (但是别太早)。

  留意如何先以最简单的方式使用 patterns,然后再慢慢增加复杂度。

  如果用了一种 pattern 却觉得没有多大帮助-不用怕,再次把它去掉。

  我认为XP应该要更加强调学习 patterns。我不确定它要怎么和 XP 的实务技巧搭配,不过相信 Kent 会想出办法来的。

 

原文转自:http://www.ltesting.net