窃以为软件的最大追求是在合适的地方做正确的事

前段时间读了《软件的最大追求是什么》,击节叫好,深以为然,虽然该文章很多地方显得有点极端。

如今的软件系统越来越复杂,如果软件的结构不好会影响软件的可维护性,重构代码是一件极其痛苦的事情。

关于软件的复杂性问题,我做了一些思考:

1) Cyclomatic Complexity (圈复杂性 = number of decision points +1    其中number of decision points是指一个if else之类的条件判断语句 (引自《软件的最大追求是什么》)

if else 语句可以被行为模式/策略模式代替,不妨看下列的例子:

假设我们要根据条件判断来完成不同的行为,用if else 是这样写的:

main() {

if(case A){

//do with strategy A

}else(case B){

//do with strategy B

}else(case C){

//do with strategy C

}

}

用策略模式则如下:

class runner{

do();

}

class A extends runner{

do(){

//do with strategy A

}

}

class B extends runner{

do(){

//do with strategy B

}

}

class C extends runner {

do(){

//do with strategy C

}

}

main(){

runner.do();

}

用了策略模式后main()中的语句的确简单多了,再也看不到该死的if else了^_^酷~~~

可实际上是这样简单吗???仔细研究一下上面的代码就能看出问题出来,首先这两段代码中的A、B、C的实际意义是不一样的:第一段代码中ABC代表的是某一个逻辑条件的值而第二段中的ABC则是具体的类;第二段得到如此简化的前提是它得到的逻辑条件就是一个类;如果得到的仍然只是一个逻辑条件,那么为了达到代码简化的效果,必须由另一个类(或方法)完成这种逻辑条件到具体类的转换,会出现类似下列的代码

class RunnerFactory{

runner getInstante(case){

if (case A) return new A();

else if (case B) return new B();

else if (case C) return new C(); } }

从测试的角度来说,两者的测试分支都是3,复杂度相同,而第二种方法在很多的时候貌似还要测试借口的有效性。用策略模式还有一个缺点就是会使系统中的类的数量大大的增加,如上的例子,采用if else类的数量为1,而采用策略模式的类个数为5个或6个(主要取决于逻辑映射是否用单独的类)。

如果逻辑判断的条件有三个,每个逻辑条件有三种可能的话,用策略模式系统至少会增加10个新类;如果条件更多的话类的个数也会更多 这么看来GOF的策略模式还要它干嘛??

当然不是,策略模式在很多情况下是一种非常好的解决方案。

这还要从if else 语句造成程序复杂以至难以维护的真正原因说起。就我个人的感觉真正造成if else语句难以维护的原因是每一个逻辑分支中的处理语句过长。比如我现在工作中维护的代码,常常一个条件下面的业务处理语句有两三千行,每次我光确定某个逻辑分支的结束位置就要找半天,头晕-_-!。如果是多层条件的话情况就更糟了。一个分支就一千多行,几个分支上万行自然很难维护。 if else 语句本质上是程序的流程控制语句,而分支中N长的代码通常是业务处理语句。

行为模式/策略模式就是把流程判断和业务处理进行了一次解耦,将业务逻辑封装成一个个单独的类。换句话说,行为模式/策略模式并不是不需要if else 语句(事实上该判断的还是要判断),只不过的换了地方或者是别的代码帮你做了。另一方面,进行逻辑判断的语句被集中起来而不是分散在程序的各个角落,有利于逻辑本身的维护。策略模式/行为模式还有一个明显的好处就是如果新增加了一种状态,我们只需要新增加一个策略类(同上的ABC)就可以了,避免了在程序中改动那些大段大段让人厌烦的if else 语句。

所以对于你的程序来说到底是使用设计模式还是简单的使用if else 关键在于你的程序是否复杂,有没有必要将控制逻辑和业务逻辑进行解耦。当然如果你可以用别的方式实现解耦也是非常好的。

2) Response for Class(RFC) 当一个类和很多其他类存在依赖时,它就变得复杂甚至难以修改和维护,这样,RFC值越大,表示你的系统味道越坏。(引自《软件的最大追求是什么》)

复杂性是由类与类之间的依赖关系(dependency)造成的。

具体如下所示:

interface Runner;

class A implement runner{ do(){}; }

一个大型的系统中很多地方用到了runner接口,于是在很多地方出现了如下的相同代码:

{

Runner r = new A();

r.do();

}

如果因为某种原因runner接口的实现类改为B,则在所有用到runner接口的地方代码都要统统改为:

{

//Runner r = new A();

Runner r = new B(); r.do();

}

这些遍布系统各个角落的改动是繁琐且容易出错的。

于是出现了各种框架和模式,其中最著名的当然是IOC(Inversion of Control)反转控制或者称之为依赖型注射(Dependency Injection) 那些讨厌的代码变成了如下:

{

ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");

Runner r = (Runner)ctx.getBean("Runner");

r.do();

}

这样我们就不需要对各个角落的代码进行维护了,容器会自动的为我们选择合适的类。

我到觉得维护的工作还是我们的,为了让容器注射入合适的类,我们必须要维护一个叫spring.xml的配置文件,或者如果你不喜欢配置文件这个东东的话(比如偶)就得自己写一个注册类,将依赖关系一一注册进去。你还要为使用该接口的类定义一个以该接口为参数的构造函数或者该接口的setter方法,好让容器可以将实现类顺利的注射进来。

该做的还是要做,只不过换了一个地方做而已。但就是换了个地方,实现了对依赖关系的集中维护(又是集中),大大的改善了系统的结构,明确了不同职责单位之间的分工。呵呵,有时自己也极端的觉得设计的工作说到底就是解决代码结构的问题^_^

IOC一直是和轻量级框架联系在一起的。所谓的重量级框架EJB也有实现相同功能的解决方案:Service Locator Service Locator就是一个定位器,它维护了一个数据结构(比如一个表),通过这个定位器你可以准确的定位到你的接口想要的实现类,Service Locator同样使你免去了改变接口实现类后的维护恶梦:

{

Runner r = (Runner)ServiceLocator.lookup("Runner");

r.do();

}

无论是IOC还是Service Locator都帮助你维护了类之间的依赖关系。那么是否我们在编程中一定要用呢,这又是个权衡的问题,IOC带来了很多的好处,不过我个人认为它的代码是让人费解的(你根本不知道它做了什么),并且为了用它,一方面你要借助于容器,另一方面你要编写配置文件,要为依赖型注射提供合适的途径。

如果你的系统类之间的依赖型错综复杂,需求的变化常常导致实现类的变化,同时你又希望采用测试驱动的快速开发模式,IOC毫无疑问是一个完美的解决方案;如果你的系统不存在上述的问题,为什么不简简单单的在程序中写死呢,何苦去维护一堆配置文件(我所在的开发部门貌似都比较痛恨配置文件这个东东)。Service Locator也有很多缺点,被骂的最多的就是没法快速测试。

反转控制,即转换控制权。依赖关系控制权的转换是对代码结构的一次重构,重构的目标还是解耦,让不同的职责代码集中放到不同的地方,于是程序员可以更加专注的解决特定的问题,比如业务逻辑。

程序设计的发展就是对代码结构的不断调整,不断解耦,让特定的代码解决特定的问题而不是什么都混在一起。从面向过程到面向对象难道不是这样吗,封装的本质也是解耦。

在实际问题的解决当中,最重要的信条就是合适,要记住任何结构的改进都会付出代价,你的改进是否值得你为此付出的代价。比如当你做一个嵌入式程序的时候你首要考虑的自然是效率问题;而如果你做的是一个ERP产品,在系统设计的时候,光系统的可维护性问题就让你不得不绞尽脑汁考虑一下代码的结构。

一句话,只做最对的。

程序设计的最大追求就是在合适的地方做正确的事。

非常棒,我要抽时间好好看看,先mark一下。

我有以下观点要商酌一下:

>if else 语句可以被行为模式/策略模式代替
我是指模式/策略模式等替代,不只是这两个模式,实践中根据业务特点选取不同模式,其实使用if else是一个对业务逻辑没有进行对象化分析的结果,所以,这使我感到有必要引入分析模式,因为离开业务分析,只讲设计模式可能比较片面。

我举一个例子:
还是以大家熟悉的论坛帖子为例子,如ForumMessage是一个模型,但是实际中帖子分两种性质:主题贴(第一个根贴)和回帖(回以前帖子的帖子),这里有一个朴素的解决方案:
建立一个ForumMessage,然后在ForumMessage加入isTopic这样判断语句,注意,你这里一个简单属性的判断引入,可能导致你的程序其他地方到处存在if else 的判断。

如果我们改用另外一种分析实现思路,以对象化概念看待,实际中有主题贴和回帖,就是两种对象,但是这两种对象大部分是一致的,因此,我将ForumMessage设为表达主题贴;然后创建一个继承ForumMessage的子类ForumMessageReply作为回帖,这样,我在程序地方,如Service中,我已经确定这个Model是回帖了,我就直接下溯为ForumMessageReply即可,这个有点类似向Collection放入对象和取出时的强制类型转换。通过这个手段我消灭了以后程序中if else的判断语句出现可能。

从这里体现了,如果分析方向错误,也会导致误用模式。

讨论设计模式举例,不能没有业务上下文场景的案例,否则无法决定是否该用模式,ppshen道友的文中if else 那个代码案例是不妥,因为你假设你的分析模型已经是if else,所以再什么设计模式也是徒然的,我必须了解业务上下文,决定合适的设计。其实我和zhuam在这个帖子:

http://www.jdon.com/jive/thread.jsp?forum=91&thread=24333&message=14484871#14484871

已经进行过讨论,它的PacketParser就是一个有业务上下文的案例,所以我们还是使用动态代理模式或Command模式来消灭那些可能存在的if else

关于软件的最大追求是什么,到底应该是什么,可能各人见智,ppshen道友是从方法论角度来谈,选择合适的模式,也就是选择合适业务上下文的模式。

我们选择合适的方式来完成软件设计,也是为尽可能追求松耦合,因为这是一个不可能完成的任务,所以只能作为目标,只能在现实中尽可能,在不伤害性能或易用性等情况下选择恰当设计。

"使用if else是一个对业务逻辑没有进行对象化分析的结果"

哈哈,这句话我喜欢^_^

受教了^_^

ppshen的思考让我收益菲浅,和你对话有乐趣,所谓碰出火花,希望多多发言,不管赞同的或不赞同的。

好,分析得非常客观。不知那些极端份子看了这个贴后会有什么反省呢?

>些极端份子看了这个贴后会有什么反省呢?
尽可能追求最大松耦合我不觉得极端,相比“合适才是最好的”这个标准,追求最大松耦合比较客观和可以量化。

“合适才是最好的”?
什么是合适的?是不是当事人自己认为合适就合适,如果你的水平很低,但是很自负,会认为合适,所以合适与否与说出此话的境界和水平有关系。

我们不能用这种含糊的,自我安慰的标准来衡量软件系统。

如果你说到:
最大化的松耦合;最快的开发简便和开发效率。两者统一在一起才是真正最好的。

我其实一直在批判“软件够用的思想”。

软件够用,就象你说:家里的饭够吃就可以,吃完怎么办?有没有想到下一步?这是国内软件人员的通病,也是软件质量无法提高的根源。

还有一种思想:80%软件不需要集群,这和软件够用就行思想是一致的,现在你这个软件也许不需要集群,但是现在面对互联网,访问量增加,你的企业迅速扩展,一个乡镇企业10年就能够变成大型企业,你的软件跟得上更新换代换血式的升级吗?

还有,世界上本来就是小软件数量多,大软件数量少,就向企业一样,小企业数量远大于大企业数量,大部分小企业确实没有必要使用集群,但是任何一个小企业都有成为大企业的可能,一旦你的企业就是这样发展,你不使用可伸缩的集群系统,你换血跟得上吗?

所以,我们的眼光要向前看,要是动态,设计最讲究动态。

80%应用无需集群是Spring作者在<without EJB>中提出的,有的人还推荐它,不说它的中文版虽然号称各个领域高手翻译的,翻译质量之差,惨不忍睹。就说这个作者本人说这话,我们就要批判性研究:Spring是一个以设计为特色的框架,设计最讲究动态的,讲究应付未来变化的,他的Spring为什么用Ioc/AOP,就是为了应付将来的需求变化,但是而作者却说出“80%应用无需集群”这种短视观点,逻辑前后严重不连贯。

可是还有很多国内高手抱着这句话“80%应用无需集群”的臭脚,他们难道不动动脑子想想一个人说的话?