技术开发 频道

敏捷开发的必要技巧(六)

【IT168 技术文章】    如果现在有一个类Parent,里面有一个属性的类型是Child,add方法里还有一个参数的类型是Girl,如下:

class Parent{ Child child; void add(Girl girl){ ... } }

    因为上面Parent里用到Child和Girl这两个类,我们说,Parent引用了类Child和类Girl。现在的问题是,如果Child这个类或Girl这个类编译不通过,那么Parent这个类也编译不了。也就是说,Parent依赖于Child和Girl。本章讲述的就是因为一些类的依赖造成的无法重用的问题。

示例

    这是一个处理zip的程序。用户可以在主窗口中输入要生成的目标zip的路径,比如c:\f.zip,然后输入想压缩到这个zip的源文件的路径,比如c:\f1.doc和c:\f2.doc 。然后,程序就会开始压缩f1.doc和f2.doc,生成f.zip文件。在压缩各个源文件的时候,主窗口下的状态栏都要显示相关的信息。比如,在压缩c:\f2.doc时,状态栏显示“正在压缩 c:\f2.zip”。这样的代码如下:

class ZipMainFrame extends Frame { StatusBar sb; void makeZip() { String zipFilePath; String srcFilePaths[]; //根据UI上给zipFilePath和srcFilePaths赋值 ... ZipEngine ze = new ZipEngine(); ze.makeZip(zipFilePath, srcFilePaths, this); } void setStatusBarText(String statusText) { sb.setText(statusText); } } class ZipEngine { void makeZip(String zipFilePath, String srcFilePaths[], ZipMainFrame f) { //在该路径上创建zip文件 ... for (int i = 0; i < srcFilePaths.length; i++) { //将srcFilePaths[i]的文件加到压缩包中 ... f.setStatusBarText("Zipping "+srcFilePaths[i]); } } }

    我们还有一个存货管理系统,里面有一些程序的数据文件经常需要压缩起来备份。这些源数据文件都有固定的路径,所以不需要用户单独去输入路径。现在我们想直接把上面这个ZipEngine类拿来重用。这个存货管理系统也有一个主窗口,同样在压缩待备份文件时,状态栏上面也要显示目前正在压缩的文件名称。
    现在,问题来了。我们希望可以在不修改代码的情况下直接重用ZipEngine类。但从上面的代码我们发现,在调用makeZip这个方法时,还需要一个传递ZipMainFrame类型的参数进来。可是很明显,我们现在的存货管理系统里并没有ZipMainFrame这样的类。也就是说,现在ZipEngine这个类在我们的存货管理系统中用不了。
    再往远一点想,其他的系统一般也不会有ZipMainFrame这个类。即使类名一样,里面所做的功能也不一样。那么其他系统也重用不了这个ZipEngine类。

“不合适的引用”让代码难以重用

    因为ZipEngine引用了ZipMainFrame这个类,当我们想重用ZipEngine时,就需要将ZipMainFrame也加进来;调用ZipEngine的makeZip方法时,也要构造一个ZipMainFrame对象传给它。而在新的环境中,我们不可能有一个同样的ZipMainFrame,也不可能特地为了调用这个方法,随便创建一个ZipMainFrame对象给它。
    一般来说,如果一个类A引用了一个类B,当我们想要重用类A时,我们还得将类B也加入系统。如果类B引用了类C,那么类B又将类C也一起拉了进来。而如果类B或类C在一个新的系统中没有意义,或者压根儿不应该存在的情况下,真正我们想要用的A这个类也用不了了。因此,“不合适的引用”让代码很难被重用。
    为了可以重用ZipEngine,首先,我们得让ZipEngine不再引用ZipMainFrame,或者说让ZipEngine不用依赖于ZipMainFrame。该怎么做呢?回答这个问题之前,我们先回答另一个问题:给你一段代码,你怎么判断这段代码是不是包含了“不合适的引用”?“不合适”这个词定义的标准是什么?

怎么判断“不合适的引用”

方法1:
    一个简单的方法就是,我们先看一下这段代码里面有没有一些互相循环的引用。比如,ZipMainFrame引用了ZipEngine类,而ZipEngine又引用了ZipMainFrame。我们管这样的类叫“互相依赖”。互相依赖也是一种代码异味。我们可以认定这样的代码是“不合适的引用”。这个方法很简单,不过,这种方法并不能包含全部情况,并不是所有有“不合适的引用”的代码都有这样的互相依赖。
方法2:
    另一个方法比较主观,在检查代码的时候,我们可以问自己:这些已经引用的类是真正需要引用的吗?对于ZipEngine,它真的需要ZipMainFrame这个类吗?ZipEngine只是改变ZipMainFrame状态栏上的信息。是不是只有引用了ZipMainFrame才能满足这样的需求?其他类行不行?有没有一个类可以取代ZipMainFrame呢?
    实际上,ZipEngine并不是一定要引用ZipMainFrame,它想引用的其实只是一个可以显示信息的状态栏而已。因此,我们将代码改为:

class ZipEngine { void makeZip(String zipFilePath, String srcFilePaths[], StatusBar statusBar) { //在该路径上创建zip文件 ... for (int i = 0; i < srcFilePaths.length; i++) { //将srcFilePaths[i]的文件加到压缩包中 ... statusbar.setText("Zipping "+srcFilePaths[i]); } } }

    现在,ZipEngine只是引用了StatusBar,而不再是ZipMainFrame。可是,这样就好了吗?只是相对好一些。因为StatusBar比较通用(至少有StatusBar类的系统比ZipMainFrame多得多),这样,ZipEngine类的可重用性就大幅改观了。
    不过,这样的方法还是太主观。没有一个既定的标准可以判断ZipEngine到底需要什么样的东西。比如,我们可以说,ZipEngine其实想要的也不是一个状态栏,它只是想调用一个可以显示一些信息的接口而已,而不是一个状态栏这么大的对象。
方法3:
    第3种方法也很主观。在设计类的时候,我们先预测一个以后可能会重用该类的系统;然后判断,在那样的系统中,这个类能不能被重用?如果你自己都觉得以后的系统不能重用这个类,你可以断定这个类包含了“不合适的引用”。
    比如,我们在设计完ZipEngine这个类时,我们可以想一下,这个类能在别的系统重用吗?好像别的系统不会有ZipMainFrame这个类,至少一个没有GUI的系统会有这样的类。这样,它就不应该引用ZipMainFrame这个类。这一方法其实也很主观,不够实用,因为每个人预测的可能性都不一样。
方法4:
    第4个方法比较简单而且客观。当我们想在一个新系统中重用这个类却发现重用不了时,我们可以判断,这个类中包含了“不合适的引用”。比如,我们在存货管理系统中重用ZipEngine时,发现这个类重用不了。这时我们就认定,该类有“不合适的引用”。
    这是一种“懒惰而被动”的方法,因为只有我们真正想在具体项目中重用时才能判断出来。不过,这也是个很有效的方法。
    总结一下,要判断一个代码是否包含了“不合适的引用”,共有四个方法:
    1. 看代码:有没有互相依赖?
    2. 认真想想:它真正需要的是什么?
    3. 推测一下:它在以后的系统中可以重用吗?
    4. 到要重用的时候就知道:现在我要重用这个类,能不能重用?
    方法1跟4是最简单的方法,推荐初学者可以这样来判断。有了更多的设计经验后,再用方法2和3会好一些。

如何让ZipEngine不再引用ZipMainFrame

    现在来看看,如何让ZipEngine不再引用(或依赖于)ZipMainFrame。其实,在介绍方法2的时候,我们已经通过思考发现,ZipEngine这个类真正需要的是什么,也找出了解决办法。不过因为方法2相对来讲并不是可以简单用好的,所以我们在不知道方法2的结果的情况下,用方法4来解决。
    现在我们是在做一个文字模式的系统,没有状态栏,我们只能直接在没有图形的屏幕上显示这些信息。我们发现ZipEngine不能重用。怎么办?因为不能重用ZipEngine,我们只好先将它的代码复制粘贴出来,然后再修改成下面的代码:

class TextModeApp { void makeZip() { String zipFilePath; String srcFilePaths[]; ... ZipEngine ze = new ZipEngine(); ze.makeZip(zipFilePath, srcFilePaths); } } class ZipEngine { void makeZip(String zipFilePath, String srcFilePaths[]) { //在该路径上创建zip文件 ... for (int i = 0; i < srcFilePaths.length; i++) { //将srcFilePaths[i]的文件加到压缩包中 ... System.out.println("Zipping "+srcFilePaths[i]); } } }

    再看一下原来的代码是这样的:

class ZipEngine { void makeZip(String zipFilePath, String srcFilePaths[], ZipMainFrame f) { //在该路径上创建zip文件 ... for (int i = 0; i < srcFilePaths.length; i++) { //将srcFilePaths[i]的文件加到压缩包中 ... f.setStatusBarText("Zipping "+srcFilePaths[i]); } } }

    很明显,这里面有很多的重复代码(代码异味)。要消除这样的代码异味,我们先用伪码让这两段代码看起来一样。比如改成:

class ZipEngine { void makeZip(String zipFilePath, String srcFilePaths[]) { //在该路径上创建zip文件 ... for (int i = 0; i < srcFilePaths.length; i++) { //将srcFilePaths[i]的文件加到压缩包中 ... 显示信息... } } }

    因为“显示信息”具体有两种实现,所以现在创建一个接口,里面有一个方法用来显示信息。这个方法可以直接取名为“showMessage”。根据该接口完成的事请,我们也可以直接将接口名取为“MessageDisplay”或“MessageSink”之类:

interface MessageDisplay { void showMessage(String msg); }

    将ZipEngine改为:

class ZipEngine { void makeZip(String zipFilePath, String srcFilePaths[], MessageDisplay msgDisplay) { //在该路径上创建zip文件 ... for (int i = 0; i < srcFilePaths.length; i++) { //将srcFilePaths[i]的文件加到压缩包中 ... msgDisplay.showMessage("Zipping "+srcFilePaths[i]); } } }

    而MessageDisplay接口的两个实现类则是:

class ZipMainFrameMessageDisplay implements MessageDisplay { ZipMainFrame f; ZipMainFrameMessageDisplay(ZipMainFrame f) { this.f = f; } void showMessage(String msg) { f.setStatusBarText(msg); } } class SystemOutMessageDisplay implements MessageDisplay { void showMessage(String msg) { System.out.println(msg); } }

    现在,两个系统也相应地做了修改:

class ZipMainFrame extends Frame { StatusBar sb; void makeZip() { String zipFilePath; String srcFilePaths[]; //根据UI给zipFilePath和srcFilePaths赋值 ... ZipEngine ze = new ZipEngine(); ze.makeZip(zipFilePath, srcFilePaths, new ZipMainFrameMessageDisplay(this)); } void setStatusBarText(String statusText) { sb.setText(statusText); } } class TextModeApp { void makeZip() { String zipFilePath; String srcFilePaths[]; ... ZipEngine ze = new ZipEngine(); ze.makeZip(zipFilePath, srcFilePaths, new SystemOutMessageDisplay()); } }

改进后的代码

    下面是改进后的代码。为了让代码看起来清楚一些,我们用了Java的内类:

interface MessageDisplay { void showMessage(String msg); } class ZipEngine { void makeZip(String zipFilePath, String srcFilePaths[], MessageDisplay msgDisplay) { //在该路径上创建zip文件 ... for (int i = 0; i < srcFilePaths.length; i++) { //将srcFilePaths[i]的文件加到压缩包中 ... msgDisplay.showMessage("Zipping "+srcFilePaths[i]); } } } class ZipMainFrame extends Frame { StatusBar sb; void makeZip() { String zipFilePath; String srcFilePaths[]; //根据UI给zipFilePath和srcFilePaths赋值 ... ZipEngine ze = new ZipEngine(); ze.makeZip(zipFilePath, srcFilePaths, new MessageDisplay() { void showMessage(String msg) { setStatusBarText(msg); } } } void setStatusBarText(String statusText) { sb.setText(statusText); } } class TextModeApp { void makeZip() { String zipFilePath; String srcFilePaths[]; ... ZipEngine ze = new ZipEngine(); ze.makeZip(zipFilePath, srcFilePaths, new MessageDisplay() { void showMessage(String msg) { System.out.println(msg); } } } }

附:
    依赖反转原则(Dependency Inversion Principle):抽象不应该依赖于具体,高层的比较抽象的类不应该依赖于低层的比较具体的类。当这种问题出现的时候,我们应该抽取出更抽象的一个概念,然后让这两个类依赖于这个抽取出来的概念。
    更多的信息可以看:
    http://www.objectmentor.com/resources/articles/dip.pdf
    http://c2.com/cgi/wiki?DependencyInversionPrinciple

0
相关文章