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"借鉴的。
SolutionXP中有两个非常响亮的口号:"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属性的类相比,哪一个更稳定?当然是前者。而架构最终都是要映射到代码级别上的,因此架构的简单将会带来架构的稳定。尽可能的让你的类小一些,尽可能的让你的方法短一些,尽可能的让类之间的关系少一些。这并不是我的忠告,很多的设计类的文章都是这么说的。在这个话题上,我们可以进一步的阅读同类的文章( )。 辨正的简单
因此,对我们来说,简单的意义就是不要把未来的、或不需要实现的功能加入到目前的软件中,相应的架构设计也不需要考虑这些额外的需求,只要刚好能够满足当前的需求就好了。这就是简单的定义。可是在现实之中,总是有这样或者那样的原因,使得设计趋向复杂。一般来说,如果一个设计对团队而言是有价值的,那么,付出一定的成本来研究、验证、发展、文档化这个设计是有意义的。反之,如果一个设计没有很大的价值或是发展它的成本超过了其能够提供的价值,那就不需要去考虑这个设计。
价值对不同的团队来说具有不同的含义。有时候可能是时间,有时候可能是用户价值,有时候可能是为了团队的设计积累和代码重用,有时候是为了获得经验,有时候是为了研究出可重用的框架(FrameWork)。这些也可以称为目的,因此,你在设计架构时,请注意先确定好你的目的,对实现目的有帮助的事情才考虑。
Scott W.Ambler在他的文章中提到一个他亲身经历的故事,在软件开发的架构设计过程中,花了很多的时间来设计数据库到业务逻辑的映射架构,虽然这是一件任何开发人员都乐意专研的事情(因为它很酷)。但他不得不承认,对用户来说,这种设计先进的架构是没有太大的意义的,因为用户并不关心具体的技术。当看到这个故事的时候,我的触动很大。一个开发人员总是热衷于新奇的技术,但是如果这个新奇技术的成本由用户来承担,是不是合理呢?虽然新技术的采用能够为用户带来效益,但是没有人计算过效益背后的成本。就我开发过的项目而言,这个成本往往是大于效益的。这个问题可能并没有确定的答案,只能是见仁见智了。
例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设计。