示例
这是一个会议管理系统。用来管理各种各样的会议参与者信息。数据库里面有个表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. 子类应该能够代替父类的功能。或者直接点说,我们应该做到,将所有使用父类的地方改成使用子类后,对结果一点影响都没有。或者更直白一点吧,请尽量不要用重载,重载是个很坏很坏的主意!更多的信息可以去:
http://www.objectmentor.com/resources/articles/lsp.pdf. http://c2.com/cgi/wiki?liskovsubstitutionprinciple.
design by contract是个跟lsp有关的东西。它表述说,我们应该测试我们所有的假设。更多信息:
http://c2.com/cgi/wiki?designbycontract.
系列的pdf下载: 敏捷开发的必要技巧:http://www.blogjava.net/files/wingel/%e6%95%8f%e6%8d%b7%e5%bc%80%e5%8f%91%e7%9a%84%e5%bf%85%e8%a6%81%e6%8a%80%e5%b7%a7%e5%ae%8c%e6%95%b4%e7%89%88.rar
|