服务热线:13616026886

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

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

敏捷开发的必要技巧:慎用继承

示例

  这是一个会议管理系统。用来管理各种各样的会议参与者信息。数据库里面有个表participants,里面的每条记录表示一个参会者。因为经常会发生用户误删掉某个参会者的信息。所以现在,用户删除时,并不会真的删除那参会者的信息,而只是将该记录的删除标记设为true。24小时以后,系统会自动将这条记录删除。但是在这24小时以内,如果用户改变主意了,系统还可以将这条记录还原,将删除标记设置为false。

  请认真的读下面的代码:

    
public class dbtable {                                                                         
       protected connection conn;                                                                  
       protected tablename;                                                                        
       public dbtable(string tablename) {                                                          
           this.tablename = tablename;                                                            
           this.conn = ...;                                                                        
       }                                                                                          
       public void clear() {                                                                      
           preparedstatement st = conn.preparestatement("delete from "+tablename);                
           try {                                                                                  
               st.executeupdate();                                                                
           }finally{                                                                              
               st.close();                                                                        
           }                                                                                      
       }                                                                                          
       public int getcount() {                                                                    
           preparedstatement st = conn.preparestatement("select count(*) from"+tablename); 
           try {                                                                                  
               resultset rs = st.executequery();                                                  
               rs.next();                                                                          
               return rs.getint(1);                                                                
           }finally{                                                                              
               st.close();                                                                        
           }                                                                                      
       }                                                                                          
    }                              
    
    public class participantsindb extends dbtable {                                                
       public participantsindb() {                                                                
           super("participants");                                                                  
       }                                                                                          
       public void addparticipant(participant part) {                                              
           ...                                                                                    
       }                                                                                          

       public void deleteparticipant(string participantid) {
           setdeleteflag(participantid, true);
       }                      
       public void restoreparticipant(string participantid) {
           setdeleteflag(participantid, false);
       }                      
       private void setdeleteflag(string participantid, boolean b) {
           ...                
       }                      
       public void reallydelete() {
           preparedstatement st = conn.preparestatement(
                             "delete from "+
                             tablename+
                             " where deleteflag=true");
           try {              
               st.executeupdate();
           }finally{          
               st.close();    
           }                  
       }                      
       public int countparticipants() {
           preparedstatement st = conn.preparestatement(
                             "select count(*) from "+
                             tablename+
                             " where deleteflag=false");
           try {              
               resultset rs = st.executequery();
               rs.next();    
               return rs.getint(1);
           }finally{          
               st.close();    
           }                  
       }                      
    }
                        

  注意到,countparticipants这个方法只计算那些deleteflags为false的记录。也就是,被删除的那些参会者不被计算在内。

  上面的代码看起来还不错,但却有一个很严重的问题。什么问题?先看看下面的代码:

    participantsindb partsindb = ...;
    participant kent = new participant(...);
    participant paul = new participant(...);
    partsindb.clear();        
    partsindb.addparticipant(kent);
    partsindb.addparticipant(paul);
    partsindb.deleteparticipant(kent.getid());
    system.out.println("there are "+partsindb.getcount()+ "participants");



  最后一行代码,会打印出"there are 1 participants"这样信息,对不?错!它打印的是"there are 2 participants"!因为最后一行调用的是dbtable里面的这个方法getcount,而不是participantsindb的countparticipants。getcount一点都不知道删除标记这回事,它只是简单的计算记录数量,并不知道要计算那些真正有效的参会者(就是删除标记为false的)。

继承了一些不合适(或者没用的)的功能

  participantsindb继承了来自dbtable的方法,比如clear和getcount。对于participantsindb来讲,clear这个方法的确是有用的:清空所有的参会者。但getcount就造成了一点点小意外了:通过participantsindb调用getcount这个方法时,是取得participants这个表里面所有的记录,不管删除标记是true还是false的。而实际上,没人想知道这个数据。即使有人想知道,这个方法也不应该叫做getcount,因为这名字很容易就会跟“计算所有的(有效)参会者数量”联系在一起。

  因此,participantsindb是不是真的应该继承这个方法getcount呢?或者我们应该怎么做比较恰当呢?

它们之间是否真的有继承关系?

  当我们继承了一些我们不想要的东西,我们应该再三的想想:它们之间是不是真的有继承关系?participantsindb必须是一个dbtable吗?participantsindb希不希望别人知道它是一个dbtable?

  实际上,participantsindb描述的是系统中所有的参会者的集合,该系统可以是个单数据库的,也可以是多数据库的,也就是说,这个类可以代表一个数据库里的一个participants表,也可以代表两个数据库各自的两个participants表的总和。


  如果还不清楚的话,我们就这样举例吧,比如,现在我们已经有了2000个参会者,在两个数据库中存放,其中数据库a的participants表里面存放了1000个参会者,数据库b的participants这个表存放了1000个参会者。dbtable顶多只能描述一个数据库里面的一张表,也就是1000个参会者,而participants则可以完全的描述这2000年参会者的信息。前面可以当作数据库的数据表在系统中的代表,而后者表示的应该包含更多业务逻辑的一个域对象。(原谅这边我只能用域对象这样的词来断开这样的混淆。)

  因此,我们可以判断,participantsindb跟dbtable之间不应该有什么继承的关系。participantsindb不能继承dbtable这个类了。于是,现在participantsindb也没有getcount这个方法了。可是participantsindb还需要dbtable类里面的其他方法啊,那怎么办?所以现在我们让participantsindb里面引用了一个dbtable:                                  
                                                                          

    public class dbtable {                                                                         
       private connection conn;                                                                    
       private string tablename;                                                                  
       public dbtable(string tablename) {                                                          
           this.tablename = tablename;                                                            
           this.conn = ...;                                                                        
       }                                                                                          
       public void clear() {                                                                      
           preparedstatement st = conn.preparestatement("delete from "+tablename);                
           try {                                                                                  
               st.executeupdate();                                                                
           }finally{                                                                              
               st.close();                                                                        
           }                                                                                      
       }                                                                                          
       public int getcount() {                                                                    
           preparedstatement st = conn.preparestatement("select count(*) from "+tablename);
           try {                                                                                  
               resultset rs = st.executequery();                                                  
               rs.next();                                                                          
               return rs.getint(1);
          }finally{            
               st.close();      
          }                    
       }                      
       public string gettablename() {
          return tablename;    
       }                      
       public connection getconn() {
          return conn;        
       }                      
   }                          
  
   public class participantsindb {
       private dbtable table;  
       public participantsindb() {
          table = new dbtable("participants");
       }                      
       public void addparticipant(participant part) {
          ...                  
       }                      
       public void deleteparticipant(string participantid) {
          setdeleteflag(participantid, true);
       }                      
       public void restoreparticipant(string participantid) {
          setdeleteflag(participantid, false);
       }                      
       private void setdeleteflag(string participantid, boolean b) {
          ...                  
       }                      
       public void reallydelete() {
          preparedstatement st = table.getconn().preparestatement(
                              "delete from "+
                              table.gettablename()+
                              " where deleteflag=true");
          try {                
              st.executeupdate();
          }finally{            
              st.close();      
          }                    
       }                      
       public void clear() {  
          table.clear();      
       }                      
       public int countparticipants() {
          preparedstatement st = table.getconn().preparestatement(
                              "select count(*) from "+
                              table.gettablename()+
                              " where deleteflag=false");
          try {                
              resultset rs = st.executequery();
              rs.next();      
              return rs.getint(1);
          }finally{            
              st.close();      
          }                    
       }                      
   }
                          

    participantsindb不再继承dbtable。代替的,它里面有一个属性引用了一个dbtable对象,然后调用这个dbtable的clear, getconn, gettablename 等等方法。

代理(delegation)    
  
  其实我们这边可以看一下participantsindb的clear方法,这个方法除了直接调用dbtable的clear方法以外,什么也没做。或者说,participantsindb只是做为一个中间介让外界调用dbtable的方法,我们管这样传递调用的中间介叫“代理(delegation)”。            

  现在,之前有bug的那部分代码就编译不过了:

    participantsindb partsindb = ...;                                                              
    participant kent = new participant(...);                                                      
    participant paul = new participant(...);                                                      
    partsindb.clear();                                                                            
    partsindb.addparticipant(kent);                                                                
    partsindb.addparticipant(paul);                                                                
    partsindb.deleteparticipant(kent.getid());                                                    
    //编译出错:因为在participantsindb里面已经没有getcount这个方法了!
    system.out.println("there are "+partsindb.getcount()+ "participants");        

                

  总结一下:首先,我们发现,participantsindb 和 dbtablein之间没有继承关系。然后我们就将“代理”来取代它们的继承。“代理”的优点就是,我们可以控制dbtable的哪些方法可以“公布(就是设为public)”(比如clear方法)。如果我们用了继承的话,我们就没得选择,dbtable里面的所有public方法都要对外公布!


抽取出父类中没必要的功能

  现在,我们来看一下另一个例子。假定一个component代表一个gui对象,比如按钮或者文本框之类的。请认真阅读下面的代码:

    abstract class component {                                                                     
       boolean isvisible;                                                                          
       int posxincontainer;                                                                        
       int posyincontainer;                                                                        
       int width;                                                                                  
       int height;                                                                                
       ...                                                                                        
       abstract void paint(graphics graphics);                                                    
       void setwidth(int newwidth) {                                                              
           ...                                                                                    
       }                                                                                          
       void setheight(int newheight) {                                                            
           ...                                                                                    
       }                                                                                          
    }                
    
    class button extends component {                                                              
       actionlistener listeners[];                                                                
       ...                                                                                        
       void paint(graphics graphics) {                                                            
           ...                                                                                    
       }                                                                                          
    }                                                                                              

    class container {
       component components[];
       void add(component component) {
           ...
       }    
    }
      

  假定你现在要写一个时钟clock组件。它是一个有时分针在转动的圆形的钟,每次更新时针跟分针的位置来显示当前的时间。因为这也是一个gui组件,所以我们同样让它继承自component类:

    class clockcomponent extends component {
       ...  
       void paint(graphics graphics) {
           //根据时间绘制当前的钟表图形
       }    
    }    
  

  现在我们有一个问题了:这个组件应该是个圆形的,但是它现在却继承了component的width跟height属性,也继承了setwidth 和 setheight这些方法。而这些东西对一个圆形的东西是没有意义的。

  当我们让一个类继承另一个类时,我们需要再三的想想:它们之间是否有继承关系?clockcomponent是一个component吗?它跟其他的compoent(比如button)是一样的吗?

  跟participantsindb的那个案例相反的是,我们不得不承认clockcomponent确实也是一个component,否则它就不能像其他的组件那样放在一个container中。因此,我们只能让它继承component类(而不是用“代理”)。

  它既要继承component,又不要width, height, setwidth 和 setheight这些,我们只好将这四样东西从component里面拿走。而事实上,它也应该拿走。因为已经证明了,并不是所有的组件都需要这四样东西(至少clockcomponent不需要)。

如果一个父类描述的东西不是所有的子类共有的,那这个父类的设计肯定不是一个好的设计。

我们有充分的理由将这些移走。

  只是,如果我们从component移走了这四样东西,那原来的那些类,比如button就没了这四样东西,而它确实又需要这些的(我们假定按钮是方形的)。

  一个可行的方案是,创建一个rectangularcomponent类,里面有width,height,setwidth和setheight这四样。然后让button继承自这个类:

    abstract class component {
       boolean isvisible;
       int posxincontainer;
       int posyincontainer;
       ...  
       abstract void paint(graphics graphics);
    }      

    abstract class rectangularcomponent extends component {
       int width;
       int height;                                                                                
       void setwidth(int newwidth) {                                                              
           ...                                                                                    
       }                     &nb

扫描关注微信公众号