本节书摘来自异步社区《面向对象设计实践指南:Ruby语言描述》一书中的第1章,第1.3节设计行为,作者【美】Sandi Metz,更多章节内容可以访问云栖社区“异步社区”公众号查看。
1.3 设计行为
面向对象设计实践指南:Ruby语言描述
随着常见设计原则和模式的出现与传播,所有的OOD问题可能都已被解决。既然基础的规则都已知道,那么设计面向对象的软件还会有多难呢?
事实证明,它非常难。如果将软件理解为可定制的家具,那么原则和模式便像是木工的工具。了解软件在完成后会是什么样子,并不能让它自我构建成那个样子。应用程序之所以存在,是因为有程序员使用了这些工具。最终的结果可能是,它要么成为一个漂亮的橱柜,要么成为一张摇摇晃晃的椅子。具体是哪一种结果,则取决于程序员使用设计工具的经验。
1.3.1 设计失败
第一种设计失败的情形是缺乏设计知识。程序员最初对设计一无所知。不过,这还不构成威胁,因为在不知道设计首先该干什么的时候,也可以生产出能工作的应用程序。
除了部分语言以外,所有的OO语言比其他类型语言都更容易受到影响,这一情形千真万确。尤其是像Ruby这种“平易近人”的语言很容易“受伤”。Ruby非常友好,它几乎可以让任何人,通过创建脚本完成一些自动的重复性任务。而且,它还有一个像Ruby on Rails那样强悍的框架,将Web应用程序放置到每一位程序员能触及的地方。Ruby语言的语法非常文雅,任何具备将想法串成逻辑顺序能力的人,都可以编写出能工作的应用程序。对面向对象设计一无所知的程序员,使用Ruby也能轻松地获得成功。
不过,虽然成功但却未进行设计的应用程序,总是存在自我毁灭的风险。它们在编写时很轻松,但更改起来则会变得越来越难。程序员过去的经验并不能预测到未来。无痛开发的早期承诺都会逐渐地变得无法兑现。如果程序员在面对每个变更请求时开始说:“是的,我可以添加这个功能,但这会把所有事情都给破坏掉”,那么这时原本乐观的情形也会变得绝望。
稍有经验的程序员也可能遭遇过另一种设计失败的情形。这些程序员对OO设计技术都有所了解,但对于如何应用它们仍然不太清楚。尽管是出于好心使用这些设计技术,但他们很快便会掉进过度设计的陷阱。只有“半桶水”是很危险的。伴随着知识的增加和希望的回归,他们开始无情地进行设计。在热情膨胀之后,他们会不合时宜地乱用这些原则。在不该有的地方,你也能看到他们在使用模式。他们一开始想要建造出一座复杂而又漂亮的代码城堡,最后却痛苦地发现自己早已被一面面石墙团团围住。你能把这种程序员立即辨别出来,因为他们在面对变更请求时总是会报怨:“不行,我不能添加这项功能。它不是设计来干这事儿的。”
最后,当设计行为与编程行为分开时,所开发的那个面向对象软件也注定会失败。设计是一个逐步发现的过程,它依赖于往复不断的反馈。这种往复反馈应该是适时的和递增的。敏捷软件运动(Agile software movement)的这种迭代技术,也因此非常适合用于创建优秀设计的面向对象应用程序。敏捷开发(Agile development)所提倡的这种迭代特性,让设计可以有规律地调整和很自然地演变。当设计是由很遥远的事情所决定时,就没必要进行调整,早期的理解错误能让代码更巩固。当程序员们被强迫编写由孤陋寡闻的专家所设计的应用程序时,他们便可以说:“没错,我当然可以写这个软件,但是这并不是你真正想要的结果,你最终是会后悔的。”
1.3.2 设计时机
敏捷开发方法相信:在客户看到具体的软件之前,他们对所想要的软件是没什么概念的,所以向他们展示软件的时机是宜早不宜迟。如果这个假设前提成立的话,那么在逻辑上你就应该以微量递增的方式来构建软件,逐步将你的方法迭代成满足客户真正需要的应用程序。敏捷开发方法认为:想要生产出客户真正需要的应用程序,最划算的方法是与他们一起合作,逐步地构建软件。这样,每次交付都有了对下一步想法进行更改的机会。敏捷开发的经验表明:这种合作产生出来的软件与最初想象的结果总是存在差异。因此,最终的软件是无法通过其他方式进行预测的。
如果敏捷开发方法正确,那么另外两件事情也会是真的:第一件事情,即大规模预先设计(Big Up Front Design,BUFD)完全没有意义(因为它不可能正确);第二件事情,即没人能预测应用程序什么时候会完成(因为你事先无法知道它最终会干什么)。
有人不太喜欢敏捷开发方法,这也是意料之中的事。“我们不知道在做什么”以及“我们不知道什么时候能完成”这两个问题很难解决。对BUFD的要求始终存在,因为在某种程度上,它提供了一种控制的感觉;否则,便会让人感觉无法控制。尽管这种感觉可能让人很安心,但认为这种编写应用程序的行为会无法继续下去,那只是一时的错觉。
BUFD不可避免会导致客户和程序员之间出现敌对关系。因为在软件真正形成之前的任何大规模设计都不可能是正确的,按照特定保证编写出的应用程序根本无法满足客户的需求。在客户尝试着使用这个应用程序时,便会发现这一点。接着,他们要求进行更改。程序员们都会抗拒这些更改,因为他们是在按计划行事,而实际情况是他们很可能已经落后于计划。项目的参与者开始从努力想要让它变得成功,转变为努力想要避免因其失败而被指责。随着这一情况的出现,这个项目便会逐渐走向消亡。
对于这种“潜规则”大家都心知肚明。当项目错过了它的交货期限时,哪怕出现这种情况的原因是因为修改了规定,但还是错在程序员。不过,如果项目是按时交付的,尽管不满足实际的需要,那么就可以肯定是规定出了问题,所以这时便可以怪罪到客户头上。BUFD的设计文档在开始时常被用作应用程序开发的路线图,但它们会逐渐成为争论的焦点。这些文档不会产生出高质量的软件,相反它们提供的都是一些被严重消化过的话语。在最后,这些话语会被大家竞相引用,没人想成为那个手持烫手山芋挨批的人。
一遍又一遍地做同样的事情,并且期望能得到不同的结果。如果说这很疯狂,那么敏捷宣言则让我们大家都开始有所觉悟。敏捷开发方法之所以有效,是因为它承认:在应用程序最终形成之前,确定性是遥不可及的。敏捷开发方法认可这一事实,因此它提供了许多策略,并将它们用来克服软件开发的各种障碍,同时也无须对具体的目标和时间表了解太多。
不过,敏捷开发方法所说的“不进行大规模的预先设计”,并不是认为完全不做任何设计。在BUFD里使用的“设计”一词与OOD里使用的“设计”有着不同的含义。BUFD几乎全部都是在指定和记录所涉及那个应用程序被期望将来应具备的全部功能及其内部工作原理。如果有某位软件架构师参与进来,则可能预先将决定扩展至如何编排所有的代码上来。OOD所关心的则是更为狭窄的领域,它主要是关于编排已有代码以便让它们更容易更改。
敏捷开发过程为更改提供了保证,而所能做出更改的能力则取决于应用程序的设计。如果无法编写出设计良好的代码,那么在每次迭代的时候,你就必须要重写这个应用程序。
因此,敏捷开发方法并不排斥设计,相反它还需要设计。不仅需要设计,它还需要非常优秀的设计。它需要你付出努力。它的成功离不开简单、灵活和可塑性强的代码。
1.3.3 设计评判
在以前有一段时间,大家常根据程序员所产生的代码行数(也被叫做源代码行数或SLOC)来评判他们。很明显,这种度量会变成什么样子:有的老板认为编程就是一条流水线,在那里同样训练有素的工人都能构建出相同的部件。这样的老板很容易形成这样一种观念,即单个生产力可以通过简单的权重输出进行评判。对于那些迫切需要一种可靠方法来对程序员进行比较和对软件进行评估的管理者来说,尽管SLOC存在有很多明显的问题,但总比什么都没有好。它至少是一种可再生的测量方式。
这种度量方法显然不是程序员想出来的。尽管SLOC可能提供了一种可用来衡量个人努力和应用程序复杂性的衡量标准,但它对整体质量却“只字未提”。它处罚的是有效率的程序员,奖励的却是那些编写冗长代码的人。它经常被专家用来与底层应用程序的危害相对照。当你了解到坐在你身旁的新手程序员常被认为更具效率,因为他或她为实现某项功能编写了很多的代码,而你却能用更少行数的代码来实现它,这个时候你会对此做何反应呢?这种度量方法以损害质量的方式改变了奖励制度。
在当代世界里,SLOC是一个历史产物,它在很大程度上已被新的度量方法所替代。有许多Ruby程序包(在google里搜索一下“ruby metrics”,最靠前的那些就是它),它们可以帮你评估代码遵循OOD原则的情况。这些度量软件通过扫描源代码,并对预测的质量进行统计。针对你自己的代码运行某个度量套件,可能会出现启发、羞辱以及担忧这三种状况。看似精心设计的应用程序会出现大量违背OOD的情况。
糟糕的OOD度量值无疑也标志着设计很糟糕:得分很低的代码将难以更改。不幸的是,得分很高的代码也不能就此证明它易于更改。即是说,这些度量无法保证你下一次的更改一定是轻松和廉价的。其中的问题在于,有可能创建出对未来过度预测的漂亮设计。虽然这些设计可能会得到非常高的OOD度量值,但如果它们对未来预测错误,那么当真正的未来最终到来时,想要进行修正则会付出昂贵的代价。OOD度量无法辨别出那些“在方法上正确而在做法却是错误”的设计。
因此,对SLOC所存在问题的警示也要扩展到OOD度量上。对它们要半信半疑。度量非常有用,因为它们没有偏执,而是会产生出一些数据。根据这些数据,你便可以推断出与软件有关的某些东西。不过,它们不是衡量质量的直接指标,而是更深层测量的“代理人”。终极的软件度量应该是:在起关键作用的那段时间间隔里,每一项功能所花费的成本。但它很不好计算。成本、功能和时间,都难以单独定义、跟踪和测量。
即使你有可能将某项单独的功能隔离起来,并跟踪与它相关的所有成本,但至关重要的时间间隔也会对代码应该如何评判产生影响。有时,现在拥有该项功能的代价非常大,以致它会超越未来所有成本的增长。如果今天缺失了某项功能会迫使你破产,那么明天处理这些代码会花费再多的成本也没有关系,你必须要尽最大努力保证按期完成。做这样的设计妥协就像是向未来借用时间,也就是众所周知的要承担技术债务。这是一笔最终必须归还的贷款,极有可能还会带上利息。
即使你并非故意想要承担技术债务,设计也要占用时间和花费成本。因为你的目标是要编写每一项功能成本都保证在最低水平的软件,所以具体要做多少设计才合适,取决于这样两件事情:你的能力和时间表。如果这个月的设计占用了你一半的时间,并且在这一年之内都无法体现出它的好处,那么这样的设计就不值得去做。当设计行为阻碍软件按时交付时,你便会输掉。一个优秀设计的应用程序,如果只能交付一半,那么与完全不能交付所导致的后果是一样的。不过,如果设计在今天早上占用了你一半的时间,而在今天下午它就能让你得到回报,并且在这个应用程序的整个生命周期里都不断采用这种做法,那么在时间方面你便都能获得一种日积月累的好处。这种设计努力会一直带来好处。
设计的盈亏临界点依赖于程序员。那些没有经验的程序员会做很多预先设计,但可能永远无法获得这样的结果:早期的设计努力会得到回报。对于那些熟练的设计师,它们在今天早上还在编写精心设计的代码,而在今天下午便能节省成本。你的经历可能介于这两种极端情况之间,本书接下来的章节将教给大家一些技巧。它们可用来权衡设计,并为你带来好处。
本文仅用于学习和交流目的,不代表异步社区观点。非商业转载请注明作译者、出处,并保留本文的原始链接。