敲响OO时代的丧钟——OO设计原则批判 OO设计模式编程单元测试软件测试
OO设计原则!
这是很多开发资源网站必备的一个栏目、专题、至少也要转载一篇放在自己的网站上的东西。所有的程序员,如果你不开发面向对象的程序也就罢了——反正你已经落伍很久了,如果你要想开发OO程序,而竟然没有把那些OO设计原则熟读背诵,搞得滚瓜烂熟。那么你就完了,一个公司面试你的时候,问你:“你对SRP的理解是怎么样的?”,而你居然不知道SRP是什么,那么这家公司你也就别想进去了。作为OO程序员的《旧约圣经》(设计模式自然是《新约圣经》)他怎么就会那么神圣呢?
介绍OO设计原则的文章很多,我在google上搜索了一下:“约有58,200项符合OO设计原则的查询结果”。真正能够介绍得透彻的,还真是没几个。正好我手边有一本Bob大叔的《UML for JAVA Programmers》那上面的介绍,在我看来,是最好的OO设计原则介绍之一了。另外一本不在手边的《敏捷软件开发 原则、模式与实践》也是Bob大叔的,还要详尽一些。如果要批判,自然要找这样的靶子来练!
一个类只能因为一个原因而改变。
“A class should have one, and only one, reason to change.”
这个原则何等的简单,又是何等的模糊呢?什么叫做一个原因呢?我们来看下面这个类:
java代码: |
class User{ private String name; private int age; public void setName(String name){ this.name=name; } public void setAge(int age){ this.age=age; } } |
请问,这个类是不是违反了SRP原则呢?设置用户的名字与设置用户的年龄,是一个原因,还是两个原因呢?Bob大叔在自己的书里举了一个例子,说明了违反SRP原则的情况,一个Employee类,包含了计算工资和扣税金额、在磁盘上读写自己、进行XML格式的相互转换、并且能够打印自己到各种报表。我说拜托啊大叔!一个类里的方法多到如此惊人的程度,自然是违反了SRP原则,但是我们要为它瘦身,该瘦到什么程度呢?按照大叔继续给出的自己的答案,它把计算工资和扣税金额的两个功能留给了Employee,其他的分离了出去。这个答案正确吗?员工的工资和税收是自己算的?还是有一个“财务部”对象来计算的呢?且不说那么扫兴的事情,就看看那个类图里分离出来的那几个类:
EmployeeXMLConverter、EmployeeDatabase、TaxReport、EmployeeReport、PayrollReport。这些类还需要有自己的内部数据吗?请注意,他们事实上都是通过接受Employee对象的内部数据而工作的,换句话说,这些所谓的类,根本就不是什么类,只不过是一个个用Class关键字包裹起来的函数库!当我们看到一个臃肿的Employee类,被拆成6个各不相同的类之后,内心自然升起了“房子打扫干净之后的喜悦”。但是,且慢!灰尘到哪里去了呢?当我们把一个类拆成6个类之后,那个原本的类自然已经遵守了SRP原则,然后新诞生的5个类,是不是也该遵守SRP原则呢?如果我们不能将一个原则应用于整个系统设计中的所有的对象,仅仅像小孩打扫卫生一样,把灰尘扫到隔壁房间,这剩下的事情,谁来处理呢?
好吧,我们不要这么严厉,毕竟这只是一个原则,追问太深似乎并不合适。我只想再搞清楚几个问题:按照SRP原则,C++中是不是一律不应该出现多重继承呢?按照SPR原则,Java中的一个类是不是一律不应该既继承一个类,又实现一个对象呢?一个简单的POJO,被动态增强之类的办法,添加出来的新的持久化能力,是不是也是违反SRP原则的呢?归根结蒂,我的问题是:按照SPR原则,我那些剩下的,但是又必须要找地方写的代码,究竟应该写在哪里呢?
软件实体(类、模块、方法等)应该允许扩展,不允许修改。
“Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.”
这个原则倒是非常的清楚明白,你不能改已经写好的代码,而应该扩展已有的代码!如何做到这一点呢?Bob大叔举了一个经典的例子:个人认为这个例子说明的是一个使用接口,隔离相互耦合的类的通常做法。而且这个做法不应叫做OCP,而应该叫做DIP。查了一下c2.com里的OCP的解释:
In other words, (in an ideal world...) you should never need to change existing code or classes: All new functionality can be added by adding new subclasses and overriding methods, or by reusing existing code through delegation.
但是在Bob大叔的OCP解释中,这个原则的具体实现被偷换了概念,从“鼓励多使用继承”,变成了“鼓励面向接口编程”。为什么?因为继承式OCP实践已经被证明会带来相当多的副作用,而面向接口编程又如何呢?我们在讨论DIP的时候再详细讨论吧。
有一个在JavaEye的讨论的连接可以参考:对于OCP原则的困惑
子类型必须能够替代他们的基本类型。
“Subtype must be substitutable for their base types.”
对于这个问题,我都不用多说什么,只引用Bob大叔在c2上的一句话,以作为我的支持。
“I believe that LSP is falsely believed by some few to be a principle of OO design, when really it isn't.”
A.上层模块应该不依赖于下层模块,它们都依赖于抽象。
B.抽象不应该依赖于细节,细节应该依赖于抽象。
“A. High level modules should not depend upon low level modules. Both should depend upon abstractions. ”
“B. Abstractions should not depend upon details. Details should depend upon abstractions.”
Bob大叔还补充说明了一下,这个原则的意思就又变了:更确切的说,不要依赖易变的具体类。也就是说,不容易变的具体类,还是可以依赖的。那么,当我们开始一次系统开放的时候,那些类是易变的具体类呢?那些类是不易变的具体类呢?怎么才算是易变、怎么才算是不易变呢?我们来看看代码吧:
java代码: |
class A{ public void doA(){ } } class B{ A a=new A(); a.doA(); } |
按照DIP原则,Class B依赖于一个具体实现类Class A,因此是可耻的!最起码你应该写成这样:
java代码: |
interface A{ public void doA(){ } } class AImpl implements A{ public void doA(){ } } class B{ A a=new AImpl(); a.doA(); } |
这样,AImpl和B都依赖于Interface A,很漂亮吧。还不够好,A a=new AImpl();还是很丑陋的,应该进一步隔离!A a=AFactory.createA();在AFactory里,再写那些见不得人的new AImpl();代码。然后呢?这还没完,更加Perfect的办法,是采用XML配置+动态IOC装配来得到一个A,这样,B就能够保证只知道这个世界上有一个Interface A,而不知道这个Interface A是从哪里来的了。这么做的理由是什么呢?有一个很吓人的理由:“如果A被B直接使用,那么对于A的任何改动,就会影响到B了。这就叫依赖,而这样的依赖会很危险!”
我们来看看这颇有说服力的话,漏洞何在?A的变化有两种情况,一种是只修改A中的方法的内部实现,无论是直接使用A还是使用Interface A的某一个实现,这时候B的代码都不用改。另一种是修改了方法的调用接口,如果是直接使用A的Class B,就需要修改相关的调用代码,而如果是使用接口的呢?就需要同时修改Class B和Interface A。这样看来,采用接口方式,反而需要修改更多的代码!这使用接口的好处何在?
客户端不应该依赖于自己不用的方法。
“The dependency of one class to another one should depend on the smallest possible interface.”
这个我就不说了!因为这个原则和SPR原则是矛盾的!就像合成复用原则(CRP)与LSP原则矛盾一样。
关于这个批判,我昨天晚上只写了一半,今天算是虎头蛇尾的写完了。最后录一段Bob大叔的话,作为结尾:
什么时候应该运用这些原则呢?首先要给您一个痛苦的告诫,让所有系统在任何时候都完全遵循所有原则是不明智的。
运用这些原则的最好方式是有的放矢,而不是主动出击。在您第一次发现代码中有结构性的问题。或者第一次意识到某个模块受到另一个模块的改变的影响时,才应该来看看这些原则中是否有一条或者多条可以用来解决问题。
......
找到得分点的最佳办法是大量写单元测试。如果能够先写测试,再写要测试的代码,效果会更好。
让我来翻译一下上面的告诫。原则不是你可以用来预防问题的,而是当你已经陷入麻烦的时候,你可以停下来悔恨一下。至于解决之道,依然不是很清楚,因此,你需要写大量的单元测试。而且,大量的单元测试并不是帮你检查你的设计漏洞,而是帮你更真切的感受自己的设计是否正确。至于他究竟是不是正确,这就看个人自己的感觉了。更为惊人的是,在测试驱动开发的建议中,如何驱动开发的准则,竟然是循环的来自于OO设计原则的。
这样的OO设计原则,就像老爸老妈给我们的人生教诲:“你要做好人啊”,别的什么都没说。而且我们还遇到了话都说不清的糊涂爹娘,怎么才算好人,不清楚,怎么才算坏人呢?被警察抓了,肯定就是坏人了。至于如何才能做得更好?自己体会吧。
(未完待续)