Chinaunix首页 | 论坛 | 博客
  • 博客访问: 118247
  • 博文数量: 18
  • 博客积分: 2015
  • 博客等级: 大尉
  • 技术积分: 245
  • 用 户 组: 普通用户
  • 注册时间: 2008-05-07 12:38
文章分类

全部博文(18)

文章存档

2010年(3)

2009年(1)

2008年(14)

我的朋友

分类: Java

2008-05-07 13:24:09

摘要:

当我们想要让一个类继承自另一个类时,我们一定要再三的检查:子类会不会继承了一些它不需要的功能(属性或者方法)?如果是的话,我们就得认真再想想:它们之间有没有真正的继承关系?如果没有的话,就用代理。如果有的话,将这些不用的功能从基类转移到另外一个合适的地方去。
 

 
示例

  这是一个会议管理系统。用来管理各种各样的会议参与者信息。数据库里面有个表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) {                                                              
           ...                                                                                    
       }                                                                                          
       void setHeight(int newHeight) {                                                            
           ...                                                                                    
       }                                                                                          
    }    
    
    class Button extends RectangularComponent {                                                    
       ActionListener listeners[];                                                                
       ...                                                                                        
       void paint(Graphics graphics) {                                                            
           ...                                                                                    
       }                                                                                          
    }              
    
    class ClockComponent extends Component {                                                      
       ...                                                                                        
       void paint(Graphics graphics) {                                                            
           //根据时间绘制当前的钟表图形                                                      
       }                                                                                          
    }
                                                                                            

  这并不是唯一可行的方法。另一个可行的方法是,创建一个RectangularDimension,这个类持有这四个功能,然后让Button去代理这个类:

    abstract class Component {                                                                     
       boolean isVisible;                                                                          
       int posXInContainer;                                                                        
       int posYInContainer;                                                                        
       ...                                                                                        
       abstract void paint(Graphics graphics);                                                    
    }                
    
    class RectangularDimension {                                                                  
       int width;                                                                                  
       int height;                                                                                
       void setWidth(int newWidth) {                                                              
           ...                                                                                    
       }                                                                                          
       void setHeight(int newHeight) {                                                            
           ...                                                                                    
       }                                                                                          
    }      
    
    class Button extends Component {                                                              
       ActionListener listeners[];                                                                
       RectangularDimension dim;                                                                  
       ...                                                                                        
       void paint(Graphics graphics) {                                                            
           ...                                                                                    
       }                                                                                          
       void setWidth(int newWidth) {                                                              
           dim.setWidth(newWidth);                                                                
       }                                                                                          
       void setHeight(int newHeight) {                                                            
           dim.setHeight(newHeight);                                                              
       }                                                                                          
    }          
    
    class ClockComponent extends Component {                                                      
       ...                                                                                        
       void paint(Graphics graphics) {
           //根据时间绘制当前的钟表图形
       }    
    }  
    

总结    

  当我们想要让一个类继承自另一个类时,我们一定要再三的检查:子类会不会继承了一些它不需要的功能(属性或者方法)?如果是的话,我们就得认真再想想:它们之间有没有真正的继承关系?如果没有的话,就用代理。如果有的话,将这些不用的功能从基类转移到另外一个合适的地方去。


引述

  里斯科夫替换原则(LSP)表述:Subtype must be substitutable for their base types. 子类应该能够代替父类的功能。或者直接点说,我们应该做到,将所有使用父类的地方改成使用子类后,对结果一点影响都没有。或者更直白一点吧,请尽量不要用重载,重载是个很坏很坏的主意!更多的信息可以去:




Design By Contract是个跟LSP有关的东西。它表述说,我们应该测试我们所有的假设。更多信息:



阅读(577) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~