程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 敏捷開發的必要技巧:慎用繼承

敏捷開發的必要技巧:慎用繼承

編輯:關於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) {

...

}

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有關的東西。它表述說,我們應該測試我們所有的假設

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved