敏捷思维(5)—简单设计
发表于:2008-02-02来源:作者:点击数:
标签:敏捷思维
XP非常强调简单的设计原则:能够用数组实现的功能决不用链表。在其它Agile方法中,简单的原则也被反复的强调。在这一章,我们就对简单性做一个全面的了解。 Context 架构应该设计到什么程度? Problem 软件的架构都是非常的复杂的,带有大量的文档和图表。
XP非常强调简单的设计原则:能够用数组实现的功能决不用链表。在其它Agile方法中,简单的原则也被反复的强调。在这一章,我们就对简单性做一个全面的了解。
Context
架构应该设计到什么程度?
Problem
软件的架构都是非常的复杂的,带有大量的文档和图表。开发人员花在理解架构本身上的时间甚至超出了实现架构的时间。在前面的文章中,我们提到了一些反对象牙塔式架构的一个原因,而其中的一个原因就是象牙塔式架构的设计者往往在设计时参杂进过多的自身经验,而不是严格的按照需求来进行设计。
在软件开发领域,最为常见的设计就是"Code and Fix"方式的设计,设计随着软件开发过程而增长。或者,我们可以认为这种方式根本就不能算是设计,它抱着一种船到桥头自然直的态度,可是在设计不断改动之后,代码变得臃肿且难以理解,到处充满着重复的代码。这样的情形下,架构的设计也就无从谈起,软件就像是在风雨中的破屋,濒临倒塌。
针对于这种情形,新的设计方式又出现了,Martin Fowler称这种方式为"Planned Design"。和建筑的设计类似,它强调在编码之前进行严格的设计。这也就是我们在团队设计中谈到的架构设计师的典型做法。设计师们通常不会去编程,理由是在土木工程中,你不可能看到一位设计师还要砌砖头。
"Planned Design"较之"Code and Fix"进步了许多,但是还是会存在很多问题。除了在团队设计中我们谈的问题之外,需求变更将会导致更大的麻烦。因此,我们理所当然的想到进行"弹性设计":弹性的设计能够满足需求的变更。而弹性的设计所付出的代价就是复杂的设计。
题外话:
这里我们谈论"Planned Design"引出的一些问题,并没有任何排斥这种方式的意思。"Planned Design"还是有很多可取之处的,但也有很多需要改进的地方。事实上,本文中我们讨论的架构设计方式,本质上也是属于"Planned Design"方式。和"Planned Design"相对应的方式是XP所主张的"Evolutionary Design"方式,但是这种方式还有待于实践的检验,并不能简单的说他就一定要比"Planned Design"先进或落后。但可以肯定的一点是:"Evolutionary Design"方式中有很多的思想和技巧是值得"Planned Design"借鉴的。
Solution
XP中有两个非常响亮的口号:"Do The Simplest Thing that Could Possibly Work"和"You Aren't Going to Need It"(通常称之为YAGNI)。他们的核心思想就是不要为了考虑将来,把目前并不需要的功能加到软件中来。
粗看之下,会有很多开发人员认为这是不切实际的口号。我能理解这种想法,其实,在我热衷于模式、可重用组件技术的时候,我对XP提倡的简单的口号嗤之以鼻。但在实际中,我的一些软件因为复杂设计导致开发成本上升的时候,我重新思考这个问题,发现简单的设计是有道理的。
降低开发的成本
不论是模式,可重用组件,或是框架技术,目的都是为了降低开发的成本。但是他们的方式是先进行大量的投入,然后再节省后续的开发成本。因此,架构设计方面的很多思路都是围绕着这种想法展开的,这可能也是导致开发人员普遍认为架构设计高不可攀的原因。
XP的方式恰恰相反,在处理第一个问题的时候,不必要也不可能就设计出具有弹性、近乎完美的架构来。这项工作应该是随着开发的演进,慢慢成熟起来的。我不敢说这种方式肯定正确,但是如果我们把生物的结构视同为架构,这种方式不是很类似于自然界中生物的进化方式吗?
在一开始就制作出完美的架构的设想并没有错,关键是很难做到这一点。总是会有很多的问题是你在做设计时没有考虑到的。这样,当一开始花费大量精力设计出的"完美无缺"的架构必然会遇到意想不到的问题,这时候,复杂的架构反而会影响到设计的改进,导致开发成本的上升。这就好比如果方向错了,交通工具再快,反而导致错误的快速扩大。Martin Fowler在他的论文中说,"Working on the wrong solution early is even more wasteful than working on the right solution early"(提前做一件错事要比提前做一件对的事更浪费时间),相信也是这个道理。
更有意思的是,通常我们更有可能做错。在我们进行架构设计的时候,我们不可能完全取得详细的需求。事实上,就算你已经取得了完整的需求,也有可能发生变化。这种情况下做出的架构设计是不可能不出错的。这样,浪费大量的时间在初始阶段设计不可能达到的"完美架构",倒不如把时间花在后续的改进上。
提升沟通的效率
我们在团队设计中已经谈过了团队设计的目标之一就是为了降低沟通的成本,以期让所有人都能够理解架构。但是如果架构如果过于复杂,将会重新导致沟通成本的上升,而且,这个成本并不会随着项目进行而降低,反而会因为上面我们提到的遇到新的问题导致沟通成本的持续上升。
简单的架构设计可以加快开发团队理解架构的速度。我们可以通过两种方式来理解简单的含义。首先,简单意味着问题的解不会非常的复杂,架构是解决需求的关键,无论需求再怎么复杂多变,总是可以找出简单稳定的部分,我们可以把这个简单稳定的部分做为基础,再根据需要进行改进扩展,以解决复杂的问题。在示例中,我们提到了measurement pattern,它就是按照这种想法来进行设计的。
其次,简单性还体现在表示的简单上。一份5页的文档就能够表达清楚的架构设计为什么要花费50页呢?同样的道理,能够用一副简单的图形就能够表示的架构设计也没有必要使用文档。毕竟,面对面的沟通才是最有效率的沟通,文档不论如何的复杂,都不能被完全理解,而且,复杂的文档,维护起来也需要花费大量的时间。只有在两种情况下,我们提倡使用复杂的文档:一是开发团队没有办法做到面对面沟通;二是开发成果要作为团队的知识积累起来,为下一次开发所用。
考虑未来
我们之所以考虑未来,主要的原因就是需求的不稳定。因此,我们如果考虑未来可能发生的需求变化,就会不知觉的在架构设计中增加复杂的成分。这违背的简单的精神。但是,如果你不考虑可能出现的情况,那些和目前设计格格不入的改变,将会导致大量的返工。
还记得YAGNI吗?原则上,我们仍然坚持不要在现有的系统中为将来可能的情况进行设计。但是,我们必须思考,必须要为将来可能出现的情况做一些准备。其实,软件中了不起的接口的思想,不就是源于此吗?因此,思考未来,但等到需要时再实现。
变更案例有助于我们思考未来,变更案例就是你在将来可能要(或可能不要)满足的,但现在不需要满足的需求。当我们在做架构设计的时候,变更案例也将会成为设计的考虑因素之一,但它不可能成为进行决策的唯一考虑因素。很多的时候,我们沉迷于设计通用系统给我们带来的挑战之中,其实,我们所做的工作对用户而言是毫无意义的。
架构的稳定
架构简单化和架构的稳定性有什么关系吗?我们说,架构越简单,其稳定性就越好。理由很简单,1个拥有4个方法和3个属性的类,和1个拥有20个方法和30属性的类相比,哪一个更稳定?当然是前者。而架构最终都是要映射到代码级别上的,因此架构的简单将会带来架构的稳定。尽可能的让你的类小一些,尽可能的让你的方法短一些,尽可能的让类之间的关系少一些。这并不是我的忠告,很多的设计类的文章都是这么说的。在这个话题上,我们可以进一步的阅读同类的文章(关于 refactoring 的思考)。
辨正的简单
因此,对我们来说,简单的意义就是不要把未来的、或不需要实现的功能加入到目前的软件中,相应的架构设计也不需要考虑这些额外的需求,只要刚好能够满足当前的需求就好了。这就是简单的定义。可是在现实之中,总是有这样或者那样的原因,使得设计趋向复杂。一般来说,如果一个设计对团队而言是有价值的,那么,付出一定的成本来研究、验证、发展、文档化这个设计是有意义的。反之,如果一个设计没有很大的价值或是发展它的成本超过了其能够提供的价值,那就不需要去考虑这个设计。
价值对不同的团队来说具有不同的含义。有时候可能是时间,有时候可能是用户价值,有时候可能是为了团队的设计积累和代码重用,有时候是为了获得经验,有时候是为了研究出可重用的框架(FrameWork)。这些也可以称为目的,因此,你在设计架构时,请注意先确定好你的目的,对实现目的有帮助的事情才考虑。
Scott W.Ambler在他的文章中提到一个他亲身经历的故事,在软件开发的架构设计过程中,花了很多的时间来设计数据库到业务逻辑的映射架构,虽然这是一件任何开发人员都乐意专研的事情(因为它很酷)。但他不得不承认,对用户来说,这种设计先进的架构是没有太大的意义的,因为用户并不关心具体的技术。当看到这个故事的时候,我的触动很大。一个开发人员总是热衷于新奇的技术,但是如果这个新奇技术的成本由用户来承担,是不是合理呢?虽然新技术的采用能够为用户带来效益,但是没有人计算过效益背后的成本。就我开发过的项目而言,这个成本往往是大于效益的。这个问题可能并没有确定的答案,只能是见仁见智了。
clearcase/" target="_blank" >cccccc border=1>
例1.Java的IO系统 从Java的IO系统设计中,我们可以感受到简单设计的困难。 IO系统设计的困难性向来是公认的。Java的IO设计的一个目的就是使IO的使用简单化。在Java的1.0中,Java的IO系统主要是把IO系统分为输入输出两个大部分,并分别定义了抽象类InputStream和OutputStream。从这两个的抽象类出发,实现了一系列不同功能的输入输出类,同时,Java的IO系统还在输入输出中实现了FilterInputStream和FilterOutputStream的抽象类以及相关的一系列实现,从而把不同的功能的输入输出函数连接在一起,实现复杂的功能。这个实现其实是Decorator模式(由于没有看过源码和相关的资料,这里仅仅是根据功能和使用技巧推测,如果大家有不同的意见,欢迎来信讨论)。 因此,我们可以把多个对象叠加在一起,提供复杂的功能: DataInpuStream in = new DataInputStream( new BufferedInputStream( new FileInputStream("test.txt");
上面的代码使用了两个FilterInputStream:DataInpuStream和BufferedInputStream,以实现读数据和缓冲的功能,同时使用了一个InputStream:FileInputStream,从文件中读取流数据。虽然使用起来不是很方便,但是应该还是非常清晰的设计。 令设计混乱的是既不属于InputStream,也不属于OutputStream的类,例如RandomAccessFile,这正表明,由于功能的复杂化,使得原先基于输入输出分类的设计变得混乱,根据我们的经验,我们说设计需要Refactoring了。因此,在Java1.1中,IO系统被重新设计,采用了Reader和Writer位基础的设计,并增加了新的特性。但是目前的设计似乎更加混乱了,因为我们需要同时使用1.0和1.1两种不同的IO设计。
| |
简单并不等于实现简单
说到这里,如果大家有一个误解,认为一个简单的架构也一定是容易设计的,那就错了。简单的架构并不等于实现起来也简单。简单的架构需要设计者花费大量的心血,也要求设计者对技术有很深的造诣。在我们正在进行的一个项目中,一开始设计的基础架构在实现中被修改了几次,但每修改一次,代码量都减少一分,代码的可读性也就增强一分。从心理的角度上来说,对自己的架构进行不断的修改,确实是需要一定的勇气的。因为不论是设计还是代码,都是开发人员的心血。但跨出这一步是值得的。
右侧的例子讨论了Java的IO设计,Java类库的设计应该来说是非常优秀的,但是仍然避免不了重新的修改。实际上,在软件开发领域,由于原先的设计失误而导致后来设计过于复杂的情况比比皆是(例如微软的OLE)。同样的,我们在设计软件的时候,也需要对设计进行不断的修改。能够实现复杂功能,同时自身又简单的设计并不是一件容易的事情。
简单设计需要什么样的设计师
简单的架构需要全面的设计师。什么才是全面的设计师,我的定义是既能够设计,又能够编码。我们在团队设计模式中就已经谈过象牙塔式架构和象牙塔式架构设计师。他们最容易犯的一个毛病就是设计和代码的脱离。从我们自己的经验来看,即使在设计阶段考虑的非常完美的架构,在编码阶段也会出现这样或那样的问题。从而导致架构实现变得复杂。最明显的特征就是在编码时出现了有大量方法的类,或是方法很长的类。这表明架构和代码脱钩了。在我们的开发过程中,不只一次出现这种现象,或者说,出现了坏味道(Bad Smell)。Refactoring的技巧也同样有助于识别坏味道。
一次的架构设计完成后,开发人员可以按照设计,快速的编程。可在一段时间之后,新的特色不断的加入,我们发现代码开始混乱,代码量增大,可读性下降,调试变得困难,代码不可控制的征兆开始出现。我们就知道,架构的设计需要调整了。这属于我们在后面所提到的Refactoring模式。而我们在这里要说的是,如果架构的设计师不参与编码,它是无法感受到坏味道的,因此也就不会主动的对设计进行改进。要解决这个问题,最好的办法是让设计师参与代码的编写,尤其是重要架构的现实部分需要设计师的参与。如果设计师没有办法参与编码,那就需要一种机制,能够把代码反馈给设计师,让他在适当的时候,重新考虑改进架构。一个可能的办法是Code Review。让设计师审核代码,以确保编码者真正了解了架构设计的意图。
例1.Java的IO系统
从Java的IO系统设计中,我们可以感受到简单设计的困难。
IO系统设计的困难性向来是公认的。Java的IO设计的一个目的就是使IO的使用简单化。在Java的1.0中,Java的IO系统主要是把IO系统分为输入输出两个大部分,并分别定义了抽象类InputStream和OutputStream。从这两个的抽象类出发,实现了一系列不同功能的输入输出类,同时,Java的IO系统还在输入输出中实现了FilterInputStream和FilterOutputStream的抽象类以及相关的一系列实现,从而把不同的功能的输入输出函数连接在一起,实现复杂的功能。这个实现其实是Decorator模式(由于没有看过源码和相关的资料,这里仅仅是根据功能和使用技巧推测,如果大家有不同的意见,欢迎来信讨论)。
因此,我们可以把多个对象叠加在一起,提供复杂的功能:
DataInpuStream in =
new DataInputStream(
new BufferedInputStream(
new FileInputStream("test.txt");
上面的代码使用了两个FilterInputStream:DataInpuStream和BufferedInputStream,以实现读数据和缓冲的功能,同时使用了一个InputStream:FileInputStream,从文件中读取流数据。虽然使用起来不是很方便,但是应该还是非常清晰的设计。
令设计混乱的是既不属于InputStream,也不属于OutputStream的类,例如RandomAccessFile,这正表明,由于功能的复杂化,使得原先基于输入输出分类的设计变得混乱,根据我们的经验,我们说设计需要Refactoring了。因此,在Java1.1中,IO系统被重新设计,采用了Reader和Writer位基础的设计,并增加了新的特性。但是目前的设计似乎更加混乱了,因为我们需要同时使用1.0和1.1两种不同的IO设计。
例2. measurement pattern
在分析模式一书中有一个measurement pattern(测量模式),原来它是为了要解决现实中各种各样纷繁复杂的可测量的属性。例如,一个医疗系统中,可能会有身高多高,体重多种,血压多少等上千种可测量的属性。如果分别表示它们,必然导致系统复杂性的上升。因此measurement pattern就从这些属性的可测量的共性出发,研究新的解决方法,提出了measurement pattern的想法:
如图所示,把可测量的属性(Measurement)做为Phenomenon Type的实例,此外,每一个的Person可以拥有多个的Measurement,同时,Measurement还对应处理的属性,例如图中的Quantity,就表示了Measurement的数量和单位。比如,一个人的体重是65公斤,那么,Phenomenon Type就是体重,Quantity的amount是65,units是公斤。
图 5.牋 measurement pattern 的类图
这其实是一个很简单的设计,但它清楚的表示了属性之间的关系,简化了数千种的属性带来的复杂性。此外,我们进一步思考,就会发现,这种架构只是针对目前出现属性众多的问题的基本解决方法,它还可以根据具体的需要进行扩展,例如,实现动态添加单位,或实现不同单位的转化等问题。
因此,我们这里展示的其实是一种思考的方法,假想一下,当你在面对一个复杂的医疗系统时,大量的属性和不同的处理方式,你是不是可以从这样复杂的需求中找出简单的部分来呢?在我们架构设计的第一篇中,我们谈到架构设计的本质在于抽象,这里例子就是最典型的一个例子,在我们传统的想法中,我们都会把身高、体重等概念做为属性或是类,但是为了满足这里的需求,我们对这些具体的概念做一个抽象,提出可测量类别的概念,并把它设计为类(Phenomenon Type),而把具体的概念做为实例。这种抽象的思想在软件设计中无处不在,例如元类的概念。
更深入的理解
下一章中我们将会讨论迭代设计,其中还会涉及到简单设计的相关知识。建议可以将两章的内容结合起来看。
(待续)
作者简介:
林星,辰讯软件工作室项目管理组资深项目经理,有多年项目实施经验。辰讯软件工作室致力于先进软件思想、软件技术的应用,主要的研究方向在于软件过程思想、Linux集群技术、OO技术和软件工厂模式。您可以通过电子邮件 iamlinx@21cn.com 和他联系。
原文转自:http://www.ltesting.net