示例
這是一個會議管理系統。用來管理各種各樣的會議參與者信息。數據庫裡面有個表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有關的東西。它表述說,我們應該測試我們所有的假設