2005年2月24日星期四

《ASD》包的设计原则-兼谈极限编程

我在之前谈ASD的时候,都有意回避了开发流程的问题。然而实际上敏捷开发就是极限编程的另一个名字。
对于一个了解传统软件工程的人来说,甚至对于一个有传统项目开发经验的人来说,极限编程的观念都有点难于接受。正因为如此,我闭口不谈极限编程,只谈论设计模式。无论如何设计模式是好东西。
到了“包”的部分,极限编程的话题再也绕不开了。
极限编程中最惊世骇俗的部分大概是,反对建模,直接实现功能,然后在软件具备一定规模之后,才由下向上的设计“包”。
如果过去有人向我提出这样的观念,一定会被我B4的,但是我现在到觉得,极限编程有一定道理。至少她在规模较小的项目中是有道理的。
该如何衡量项目大小呢?这又是一个很难的问题。但是可以确定的是,大部分的项目都是小项目。

极限编程认为,在实现功能之前,先建建立全局的模型是没有意义的,这样的模型只会限制软件的开发,然后模型和实现双方互相妥协,结果模型被改得面目全非,还是离理想结果相距甚远。

反对极限编程的人认为,没有总体模型的指导,实现会缺乏大局观,变得混乱重复,难于维护。

极限编程提高维护性的主要作法就是依赖“包”。“包”并不是传统的模块的概念,而只是一个软件结构的映射,用于管理依赖和重用。
而由于测试驱动开发,软件结构是非常容易变动的,所以可以在混乱刚出现的时候,调整“包”,来恢复整个软件结构的有序。
但是如果没有总体的模型,开发人员在实现功能的时候,如何才能知道有没有可以重用的包呢?在调整软件的时候,又如何知道该调整到哪个包里去呢?
这就是说,需要有人(很多人,最好是全部开发人员)了解整个系统的情况,知道包的结构。
这也是极限编程强调结对开发(pair development)的原因之一。结对开发的要义不仅是两个人一块干活,而且还是要经常变更自己的开发伴侣,开发系统中的不同模块。这样让知识在团队中广泛传播。团队中的每个人,都对系统中的各个部分都有所认识,然后软件的灵活调整才能得以实现。

我基本上同意上诉观点。还有两个推论:
第一个推论,项目的大小取决于开发人员的能力。由上面的讨论可以看出,要实施极限编程,需要每个开发人员都了解系统的结构。但是,事实上,个人的能力有限,在有限的时间中,能够理解的系统结构也是有限的。超出了这个限制之后,极限编程大概就会暴露出越来越多的问题。所以,极限编程确实在较小的项目中比较有效,这个大小的限制即是开发人员能够理解的系统的大小。团队中开发人员平均水平越强,极限编程能够支持的系统也就越大。
推论的推论,如果是作一个个人作品,毫无疑问的应该使用极限编程。
顺便一提的是,与极限编程对应的另一种方法是软件工厂,即是由少量的精英的制定模型,大量的软件蓝领实现极细小的模块。虽然我由衷的不喜欢这个点子,但是也许这种方法更适合中国的国庆。
第二个推论,极限编程的各个部分有着密切的关系,分开来应用其中的一条两条,并非极限编程。

好了,下面来谈论包的设计原则。前面已经提到,包的主要目的是管理软件模块的依赖关系和重用。包的设计原则就是为了使这样的管理尽可能的最优化。
下面来看看这些原则。这些原则的前提是,这是一个应用极限编程的项目。即是说,项目的规模有限,并且已经有了一定规模的类,现在要用包来管理这些类。

内聚性原则:包括REP,CRP,SRP。这里主要解决的是包的粒度问题,多少类该被放进同一个包中。然而,包其实并不等于软件模块,所以包的大小在通常情况下,是远小于软件模块的,就是说,同一个模块可能被放在多个包中。
REP是重用发布等价原则,重用的粒度就是发布的粒度。即是说,一个包中的软件要么都是可重用的,要么都是不可重用的。我的理解是,实现feature时要想想,那些部分是这个feature独有的,那些部分是其它feature也可以用的。这两个部分要分入不同的包中。
CRP,共同重用原则,一个包中的所有类应该是共同重用的。如果重用了包中的一个类,那么就要重用包中的所有类。我的理解是,即使一个feature中有很多部分都是可以重用的。但如果这些部分可以单独重用,那么也要分入不同的包。
CCP,共同封闭原则,包中的所有类对于同一类性质的变化应该是共同封闭的。一个变化若对一个包产生影响,则将对该包中的所有类产生影响,而对于其它的包不造成任何影响。我的理解是,应该把对于同一个变化敏感的类尽量放入同一个包中。
用不太准确的说法来说,前两个原则的动力来自包的client,趋向于拆分包;后一个原则的动力来自包的server,趋向于聚合包。包的粒度就是在这两种力量之间的动态平衡。需要注意的是,所谓包的client和server可能和你的想像有所不同。

耦合性原则,包括无环依赖原则,稳定依赖原则,稳定抽象原则。这些原则主要用于解决包之间的依赖关系。
无环依赖原则,在包的依赖关系图中,不允许存在环。所谓包的依赖是指,如果A包中的文件依赖B包中的文件时,就是A包依赖B包。(之前在DIP原则(类的设计原则)中,已经提到了实现应该依赖于接口。)这是一个应该严格遵守的原则,只有遵守了这个原则,才能谈后两个耦合性原则。依赖关系中如果出现了环,就要解决掉环。解决环的方法有两个,第一个方法的提示是DIP原则。第二个方法是,把两个包都依赖的部分提取出来作成第三个包,让两个包都依赖于这个包。这个方法的提示是,包在开发过程中是动态变化的。
稳定依赖原则,朝着稳定的方向进行依赖。所谓稳定性,即是包的依赖关系。若是包A依赖一大堆其它的包,那么任何一个包的变化,都会引起包A的变化,包A即是不稳定的。相反,若一大堆的包依赖包B,那么包B若要变化就会引起很多其它包的变化,这么说包B很难改变,包B即是稳定的。《ASD》中描述了一个计算不稳定性的方法,称为不稳定性度量I,主要取决于依赖于包的类的数量和包中依赖于包外的类的数量,具体算法很简单,略。I是一个比值,范围是[0,1]。0时最稳定,1时最不稳定。这个I值对于类的划分有点意义,要把不变的类放入I值较小的包,把要变的类放入I值较大的包。但是I的主要意义是表现在稳定抽象原则中。
稳定抽象原则,包的抽象程度应该和其稳定程度一致。理论上,抽象类即是接口,趋向于不变,具体类即是实现,趋向于变化。《ASD》再次给出一个描述包的抽象性度量A,即是用包中抽象类的总数除以保重类的总数得到的比值。这个抽象性A的取值范围是从0到1,数值越大,抽象性越大。因此,在理想的系统中,I越大,A就应该越小;A越大,I就应该越小。若是把A和I作成二维的坐标体系,那么(0,1)到(1,0)的连线就是一根理想连线,若是一个包远离这个连线,那么这个包的设计就是有问题的。
这个让我想到,也许应该设计一个软件来自动管理包的A值和I值,自动给出统计数据。那么对于系统的设计应该会有很大的好处。也许,过一段时间,等我手上的项目不太多的时候,我也许可以写一个这样的东西出来。

一个问题,在C++中,如何表现“包”呢?不明白,要想办法查查看。

Subscribe with Bloglines



没有评论: