自写自看

软件最大的追求是什么? (转载)

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

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

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

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产品,在系统设计的时候,光系统的可维护性问题就让你不得不绞尽脑汁考虑一下代码的结构。



一句话,只做最对的。

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

posted on 2008-02-25 14:11 昨夜人生 阅读(208) 评论(0)  编辑  收藏


只有注册用户登录后才能发表评论。


网站导航:
博客园   IT新闻   Chat2DB   C++博客   博问