服务热线:13616026886

技术文档 欢迎使用技术文档,我们为你提供从新手到专业开发者的所有资源,你也可以通过它日益精进

位置:首页 > 技术文档 > JAVA > 新手入门 > 基础入门 > 查看文档

敏捷开发的必要技巧:处理不合适的依赖

    摘要:

    要判断一个代码是不是包含了“不合适的依赖”,共有四个方法:1.看代码:有没有互相依赖?2.认真想想:它真正需要的是什么?3.推测一下:它在以后的系统中可以重用吗?4.到要重用的时候就知道了:现在我要重用这个类,能不能重用?

如果现在有一个类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:/f2.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  

扫描关注微信公众号