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

上一篇 / 下一篇  2007-12-07 14:56:49 / 个人分类:敏捷开发

    这是一个会议管理系统,用来管理各种各样的会议参与者信息。数据库里面有一张表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) {
  //根据时间绘制当前的钟表图形
 }
}

总结

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


TAG:

 

评分:0

我来说两句

显示全部

:loveliness: :handshake :victory: :funk: :time: :kiss: :call: :hug: :lol :'( :Q :L ;P :$ :P :o :@ :D :( :)

我的栏目

日历

« 2008-10-08  
   1234
567891011
12131415161718
19202122232425
262728293031 

数据统计

  • 访问量: 7587
  • 日志数: 72
  • 建立时间: 2007-11-28
  • 更新时间: 2008-10-07

RSS订阅

Open Toolbar