來自:http://www.aygfsteel.com/huanzhugege/archive/2007/04/10/109647.html
經(jīng)常可以從開發(fā)人員口中聽到“面向?qū)ο?#8221;這個(gè)詞:
場景1、
A:我今天開始用面向?qū)ο蟮姆椒ㄔO(shè)計(jì)程序了!
B:你怎么做的?
A:我把保存文件、加載文件封裝成了一個(gè)類,以后只要調(diào)用這個(gè)類就可以實(shí)現(xiàn)文件操作了。
場景2、
A:我開始學(xué)習(xí)Java了,面向?qū)ο蟮恼Z言,你不要再學(xué)VB了,好土呀!
B:VB怎么了?
A:VB是面向過程的,已經(jīng)過時(shí)了,Java中都是類,很時(shí)髦!
B:VB中也有類呀!
A:(無語)
場景3、
A:面向?qū)ο笏枷刖褪呛醚剑艺娴碾x不開Java了!
B:你又用什么高超技術(shù)了?
A:我今天從一個(gè)操縱數(shù)據(jù)庫的類繼承了一個(gè)子類,然后重寫了它的保存到數(shù)據(jù)庫的方法,然后把數(shù)據(jù)通過Socket發(fā)送到了遠(yuǎn)程客戶端了,而調(diào)用者根本不知道,哈哈!
場景4、
A:我推薦你用的Java不錯(cuò)吧?
B:真是不錯(cuò),面向?qū)ο缶褪呛茫琂DK里邊也有好多好多的類可以用,不用像在VB里邊那樣要去查API文檔了。
A:但是我聽說現(xiàn)在又出了個(gè)面向方面編程,咱們看來又落伍了呀,看來做編程真的不是長久之計(jì)。
寫幾個(gè)類就是面向?qū)ο罅藛幔坷^承父類就是為了重用父類的代碼嗎?覆蓋父類的方法就可以瞞天過海了嗎?VB中也有類,它是面向?qū)ο髥幔?
1.1
類與對象
“類”和“對象”是面向?qū)ο缶幊讨凶罨镜母拍睿瑥恼Z言的角度來講,“類”是用戶自定義的具有一定行為的數(shù)據(jù)類型,“對象”則是“類”這種數(shù)據(jù)類型的變量。通俗的講,“類”是具有相同或相似行為的事物的抽象,“對象”是“類”的實(shí)例,是是一組具有相關(guān)性的代碼和數(shù)據(jù)的組合體,是有一定責(zé)任的實(shí)體。
類本身還可以進(jìn)一步抽象為類型,類型是一種更高層次上的抽象,它只用來描述接口,比如抽象類和接口就是一種類型。當(dāng)一個(gè)類型的接口包含另外一個(gè)類型的接口時(shí),我們就可以說它是此類型的子類型。類型是用來標(biāo)識特定接口的,如果一個(gè)對象接受某個(gè)接口定義的所有行為,那么我們就可以說該對象具有該類型。一個(gè)對象同時(shí)擁有多種類型。
面向?qū)ο缶幊痰奶匦?
面向?qū)ο缶幊逃腥齻€(gè)特性:封裝,繼承,多態(tài)。這三個(gè)特性從低級到高級描述了面向?qū)ο蟮奶卣鳌R环N語言只有同時(shí)具備這三種特性才能被稱為面向?qū)ο蟮恼Z言。VB中也有類,它的類也支持封裝和簡單的繼承,但是它不支持所有的繼承語義和多態(tài),因此VB只能被稱為基于對象的語言。
封裝是所有抽象數(shù)據(jù)類型(ADT)的特性,很多剛剛接觸面向?qū)ο蟮娜苏J(rèn)為封裝就是就是面向?qū)ο蟆⒊绦虬凑找欢ǖ倪壿嫹殖啥鄠€(gè)互相協(xié)作的部分,并將對外界有用的穩(wěn)定的部分暴露出來,而將會(huì)發(fā)生的改變隱藏起來,外界只能通過暴露的部分向這個(gè)對象發(fā)送操作請求從而享受對象提供的服務(wù),而不必管對象內(nèi)部是如何運(yùn)行的,這就是封裝。理解封裝是理解面向?qū)ο蟮牡谝粋€(gè)步驟,40%的程序員對面向?qū)ο蟮睦斫鈨H停留在封裝這個(gè)層次。
繼承也稱為派生,繼承關(guān)系中,被繼承的稱為基類,從基類繼承而得的被稱為派生類或者子類。繼承是保持對象差異性的同時(shí)共享對象相似性的復(fù)用。能夠被繼承的類總是含有并只含有它所抽象的那一類事務(wù)的共同特點(diǎn)。繼承提供了實(shí)現(xiàn)復(fù)用,只要從一個(gè)類繼承,我們就擁有了這個(gè)類的所有行為。理解繼承是理解面向?qū)ο蟮牡诙€(gè)步驟,50%的程序員對面向?qū)ο蟮睦斫鈨H停留在繼承這個(gè)層次。語義上的“繼承”表示“是一種(is-a)”的關(guān)系。很多人體會(huì)到了繼承在代碼重用方面的優(yōu)點(diǎn),而忽視了繼承的語義特征。于是很多濫用繼承的情況就發(fā)生了,關(guān)于這一點(diǎn)我們將會(huì)在后邊介紹。
多態(tài)是“允許用戶將父對象設(shè)置成為一個(gè)或更多的它的子對象相等的技術(shù),賦值后,基類對象就可以根據(jù)當(dāng)前賦值給它的派生類對象的特性以不同的方式運(yùn)作”(Charlie Calvert)。多態(tài)擴(kuò)大了對象的適應(yīng)性,改變了對象單一繼承的關(guān)系。多態(tài)是行為的抽象,它使得同名方法可以有不同的響應(yīng)方式,我們可以通過名字調(diào)用某一方法而無需知道哪種實(shí)現(xiàn)將被執(zhí)行,甚至無需知道執(zhí)行這個(gè)實(shí)現(xiàn)的對象類型。多態(tài)是面向?qū)ο缶幊痰暮诵母拍睿挥欣斫饬硕鄳B(tài),才能明白什么是真正的面向?qū)ο螅拍苷嬲l(fā)揮面向?qū)ο蟮淖畲竽芰Α2贿^可惜的是,只有極少數(shù)程序員能真正理解多態(tài)。
對象之間有兩種最基本的關(guān)系:繼承關(guān)系,組合關(guān)系。
繼承關(guān)系
繼承關(guān)系可以分為兩種:一種是類對接口的繼承,被稱為接口繼承;另一種是類對類的繼承,被稱為實(shí)現(xiàn)繼承。繼承關(guān)系是一種“泛化/特化”關(guān)系,基類代表一般,而派生類代表特殊。
組合關(guān)系。
組合是由已有的對象組合而成新對象的行為,組合只是重復(fù)運(yùn)用既有程序的功能,而非重用其形式。組合與繼承的不同點(diǎn)在于它表示了整體和部分的關(guān)系。比如電腦是由CPU、內(nèi)存、顯示器、硬盤等組成的,這些部件使得電腦有了計(jì)算、存儲(chǔ)、顯示圖形的能力,但是不能說電腦是由CPU繼承而來的。
1.2
對象之間有兩種最基本的關(guān)系:繼承關(guān)系,組合關(guān)系。通過這兩種關(guān)系的不斷迭代組合最終組成了可用的程序。但是需要注意的就是要合理使用這兩種關(guān)系。
派生類是基類的一個(gè)特殊種類,而不是基類的一個(gè)角色。語義上的“繼承”表示“is-a”(是一種)的關(guān)系,派生類“is-a”基類,這是使用繼承關(guān)系的最基本前提。如果類A是類B的基類,那么類B應(yīng)該可以在任何A出現(xiàn)的地方取代A,這就是“Liskov代換法則(LSP)”。如果類B不能在類A出現(xiàn)的地方取代類A的話,就不要把類B設(shè)計(jì)為類A的派生類。
舉例來說,“蘋果”是“水果”的派生類,所以“水果是植物的果實(shí)”這句話中的“水果”可以用“蘋果”來代替:“蘋果是植物的果實(shí)”;而“蘋果”不是“香蕉”的派生類,因?yàn)?#8220;香蕉是一種種子退化的了的植物果實(shí)”不能被“蘋果”替換為“蘋果是一種種子退化的了的植物果實(shí)”。
舉這個(gè)例子好像有點(diǎn)多余,不過現(xiàn)實(shí)的開發(fā)中卻經(jīng)常發(fā)生“蘋果”從“香蕉”繼承的事情。
某企業(yè)中有一套信息系統(tǒng),其中有一個(gè)“客戶(Customer)”基礎(chǔ)資料,里邊記錄了客戶的名稱、地址、email等信息。后來系統(tǒng)要進(jìn)行升級,增加一個(gè)“供應(yīng)商(Supplier)”基礎(chǔ)資料,開發(fā)人員發(fā)現(xiàn)“供應(yīng)商”中有“客戶”中的所有屬性,只是多了一個(gè)“銀行帳號”屬性,所以就把“供應(yīng)商”設(shè)置成“客戶”客戶的子類。
圖 2.1
到了年終,老板要求給所有的客戶通過Email發(fā)送新年祝福,由于“供應(yīng)商”是一種(is-a)“客戶”,所以系統(tǒng)就給“供應(yīng)商”和“客戶”都發(fā)送了新年祝福。第二天很多供應(yīng)商都感動(dòng)流涕的給老板打電話“謝謝老板呀,我們供應(yīng)商每次都是求著貴公司買我們的東西,到了年終你們還忘不了我們,真是太感謝了!”。老板很茫然,找來開發(fā)人員,開發(fā)人員這才意識到問題,于是在發(fā)送Email的程序里做了判斷“如果是供應(yīng)商則不發(fā)送,否則發(fā)送”,一切ok了。到了年初,老板要求給所有很長時(shí)間沒有購買他們產(chǎn)品的“客戶”,打電話進(jìn)行問候和意見征集。由于“供應(yīng)商”是一種(is-a)“客戶”,所以第二天電話里不斷出現(xiàn)這樣的回答:“你們搞錯(cuò)了吧,我們是你們的供應(yīng)商呀!”。老板大發(fā)雷霆,開發(fā)人員這才意識到問題的嚴(yán)重性,所以在系統(tǒng)的所有涉及到客戶的地方都加了判斷“如果是供應(yīng)商則……”,一共修改了60多處,當(dāng)然由于疏忽遺漏了兩處,所以后來又出了一次類似的事故。
我們可以看到錯(cuò)誤使用繼承的害處了。其實(shí)更好的解決方案應(yīng)該是,從“客戶”和“供應(yīng)商”中抽取一個(gè)共同的基類“外部公司”出來:
圖 2.2
這樣就將“客戶”和“供應(yīng)商”之間的繼承關(guān)系去除了。
派生類不應(yīng)大量覆蓋基類的行為。派生類具有擴(kuò)展基類的責(zé)任,而不是具有覆蓋(override)基類的責(zé)任。如果派生類需要大量的覆蓋或者替換掉基類的行為,那么就不應(yīng)該在兩個(gè)類之間建立繼承關(guān)系。
讓我們再來看一個(gè)案例:
一個(gè)開發(fā)人員要設(shè)計(jì)一個(gè)入庫單、一張出庫單和一張盤點(diǎn)單,并且這三張單都有登帳的功能,通過閱讀客戶需求,開發(fā)人員發(fā)現(xiàn)三張單的登帳邏輯都相同:遍歷單據(jù)中的所有物品記錄,然后逐筆登到臺帳上去。所以他就設(shè)計(jì)出了如下的程序:
圖 2.3
把登帳邏輯都寫到了“庫存業(yè)務(wù)單據(jù)”這個(gè)抽象類中,三張單據(jù)從這個(gè)類繼承即可。過了三個(gè)月,用戶提出了新的需求:盤點(diǎn)單在盤點(diǎn)過程中,如果發(fā)現(xiàn)某個(gè)貨物的盤虧量大于50則停止登帳,并向操作人員報(bào)警。所以開發(fā)人員在盤點(diǎn)單中重寫了“庫存業(yè)務(wù)單據(jù)”的“登帳”方法,實(shí)現(xiàn)了客戶要求的邏輯。又過了半個(gè)月,客戶要求出庫登帳的時(shí)候不僅要進(jìn)行原先的登帳,還要以便登帳一邊計(jì)算出庫成本。所以開發(fā)人員在出庫單中重寫了“庫存業(yè)務(wù)單據(jù)”的“登帳”方法,實(shí)現(xiàn)了客戶要求的邏輯。到了現(xiàn)在“庫存業(yè)務(wù)單據(jù)”的“登帳”方法的邏輯只是對“入庫單”有用了,因?yàn)槠渌麅蓮垎螕?jù)都“另立門戶”了。
這時(shí)候就是該我們重新梳理系統(tǒng)設(shè)計(jì)的時(shí)候了,我們把“庫存業(yè)務(wù)單據(jù)”的“登帳”方法設(shè)置成抽象方法,具體的實(shí)現(xiàn)代碼由具體子類自己決定:
圖 2.4
注意此處的“庫存業(yè)務(wù)單據(jù)”中的“登帳”方法是斜體,在UML中表示此方法是一個(gè)抽象方法。這個(gè)不難理解,每張單據(jù)都肯定有登帳行為,但是每張單據(jù)的登帳行為都有差異,因此在抽象類中定義類的“登帳”方法為抽象方法以延遲到子類中去實(shí)現(xiàn)。
繼承具有如下優(yōu)點(diǎn):實(shí)現(xiàn)新的類非常容易,因?yàn)榛惖拇蟛糠止δ芏伎梢酝ㄟ^繼承關(guān)系自動(dòng)賦予派生類;修改或者擴(kuò)展繼承來的實(shí)現(xiàn)非常容易;只要修改父類,派生的類的行為就同時(shí)被修改了。
初學(xué)面向?qū)ο缶幊痰娜藭?huì)認(rèn)為繼承真是一個(gè)好東西,是實(shí)現(xiàn)復(fù)用的最好手段。但是隨著應(yīng)用的深入就會(huì)發(fā)現(xiàn)繼承有很多缺點(diǎn):繼承破壞封裝性。基類的很多內(nèi)部細(xì)節(jié)都是對派生類可見的,因此這種復(fù)用是“白箱復(fù)用”;如果基類的實(shí)現(xiàn)發(fā)生改變,那么派生類的實(shí)現(xiàn)也將隨之改變。這樣就導(dǎo)致了子類行為的不可預(yù)知性;從基類繼承來的實(shí)現(xiàn)是無法在運(yùn)行期動(dòng)態(tài)改變的,因此降低了應(yīng)用的靈活性。
繼承關(guān)系有很多缺點(diǎn),如果合理使用組合則可以有效的避免這些缺點(diǎn),使用組合關(guān)系將系統(tǒng)對變化的適應(yīng)力從靜態(tài)提升到動(dòng)態(tài),而且由于組合將已有對象組合到了新對象中,因此新對象可以調(diào)用已有對象的功能。由于組合關(guān)系中各個(gè)各個(gè)對象的內(nèi)部實(shí)現(xiàn)是隱藏的,我們只能通過接口調(diào)用,因此我們完全可以在運(yùn)行期用實(shí)現(xiàn)了同樣接口的另外一個(gè)對象來代替原對象,從而靈活實(shí)現(xiàn)運(yùn)行期的行為控制。而且使用合成關(guān)系有助于保持每個(gè)類的職責(zé)的單一性,這樣類的層次體系以及類的規(guī)模都不太可能增長為不可控制的龐然大物。因此我們優(yōu)先使用組合而不是繼承。
當(dāng)然這并不是說繼承是不好的,我們可用的類總是不夠豐富,而使用繼承復(fù)用來創(chuàng)建一些實(shí)用的類將會(huì)不組合來的更快,因此在系統(tǒng)中合理的搭配使用繼承和組合將會(huì)使你的系統(tǒng)強(qiáng)大而又牢固。
1.3
接口的概念
接口是一種類型,它定義了能被其他類實(shí)現(xiàn)的方法,接口不能被實(shí)例化,也不能自己實(shí)現(xiàn)其中的方法,只能被支持該接口的其他類來提供實(shí)現(xiàn)。接口只是一個(gè)標(biāo)識,標(biāo)識了對象能做什么,至于怎么做則不在其控制之內(nèi),它更像一個(gè)契約。
任何一個(gè)類都可以實(shí)現(xiàn)一個(gè)接口,這樣這個(gè)類的實(shí)例就可以在任何需要這個(gè)接口的地方起作用,這樣系統(tǒng)的靈活性就大大增強(qiáng)了。
接口編程的實(shí)例
SQL語句在各個(gè)不同的數(shù)據(jù)庫之間移植最大的麻煩就是各個(gè)數(shù)據(jù)庫支持的語法不盡相同,比如取出表的前10行數(shù)據(jù)在不同數(shù)據(jù)庫中就有不同的實(shí)現(xiàn)。
MSSQLServer:Select top 10 * from T_Table
MySQL:select * from T_Table limit 0,10
Oracle:select * from T_Table where ROWNUM <=10
我們先來看一下最樸素的做法是怎樣的:
首先定義一個(gè)SQL語句翻譯器類:
public class Test1SQLTranslator
{
private int dbType;
public Test1SQLTranslator(int dbType)
{
super();
this.dbType = dbType;
}
public String translateSelectTop(String tableName, int count)
{
switch (dbType) {
case 0:
return "select top " + count + " * from " + tableName;
case 1:
return "select * from " + tableName + " limit 0," + count;
case 2:
return "select * from " + tableName + " where ROWNUM<=" + count;
default:
return null;
}
}
}
然后如下調(diào)用
public static void main(String[] args)
{
String tableName = "T_Table";
int count = 10;
int dbType = 0;
Test1SQLTranslator translator = new Test1SQLTranslator(dbType);
String sql = translator.translateSelectTop(tableName,count);
System.out.println(sql);
}
如果要增加對新的數(shù)據(jù)庫的支持,比如DB2,那么就必須修改Test1SQLTranslator類,增加一個(gè)對DB2的case語句,這種增加只能是在編輯源碼的時(shí)候進(jìn)行添加,無法在運(yùn)行時(shí)動(dòng)態(tài)添加。再來看一下如果用基于接口的編程方式是如何實(shí)現(xiàn)的。
首先,定義接口ISQLTranslator,這個(gè)接口定義了所有SQL翻譯器的方法,目前只有一個(gè)翻譯Select top的方法:
public interface ISQLTranslator
{
public String translateSelectTop(String tableName, int count);
}
接著我們?yōu)楦鱾€(gè)數(shù)據(jù)庫寫不同的翻譯器類,這些翻譯器類都實(shí)現(xiàn)了ISQLTranslator接口:
public class MSSQLServerTranslator implements ISQLTranslator
{
public String translateSelectTop(String tableName, int count)
{
return "select top " + count + " * from " + tableName;
}
}
public class MySQLTranslator implements ISQLTranslator
{
public String translateSelectTop(String tableName, int count)
{
return "select * from " + tableName +" limit 0,"+count;
}
}
public class OracleSQLTranslator implements ISQLTranslator
{
public String translateSelectTop(String tableName, int count)
{
return "select * from " + tableName+" where ROWNUM<="+count;
}
}
如下調(diào)用:
public static void main(String[] args)
{
String tableName = "T_Table";
int count = 10;
ISQLTranslator translator = new MSSQLServerTranslator();
String sql = translator.translateSelectTop(tableName, count);
System.out.println(sql);
}
運(yùn)行以后,打印出了:
select top 10 from T_Table
可以看到,不同的數(shù)據(jù)庫翻譯實(shí)現(xiàn)由不同的類來承擔(dān),這樣最大的好處就是可擴(kuò)展性極強(qiáng),比如也許某一天出現(xiàn)了了支持中文語法的數(shù)據(jù)庫,我要為它做翻譯器只需再增加一個(gè)類:
public class SinoServerTranslator implements ISQLTranslator
{
public String translateSelectTop(String tableName, int count)
{
return "讀取表"+tableName+"的前"+count+"行";
}
}
修改調(diào)用代碼:
public static void main(String[] args)
{
String tableName = "T_Table";
int count = 10;
ISQLTranslator translator = new SinoServerTranslator();
String sql = translator.translateSelectTop(tableName, count);
System.out.println(sql);
}
運(yùn)行后控制臺打印出:
讀取表T_Table的前10行
這里的translator 可以隨意實(shí)例化,只要實(shí)例化的類實(shí)現(xiàn)了ISQLTranslator 就可以了,這個(gè)類也可以通過配置文件讀取,甚至是其他類傳遞過來的,這都無所謂,只要是實(shí)現(xiàn)了ISQLTranslator 接口它就能正常工作。
如果要給SQL語句加上驗(yàn)證功能,也就是翻譯的時(shí)候首先驗(yàn)證一下翻譯的結(jié)果是否能在數(shù)據(jù)庫中執(zhí)行,我們就可以采用偷天換日的方式來進(jìn)行。
首先創(chuàng)建一個(gè)VerifyTranslator類:
public class VerifyTranslator implements ISQLTranslator
{
private ISQLTranslator translator;
private Connection connection;
public VerifyTranslator(ISQLTranslator translator, Connection connection)
{
super();
this.translator = translator;
this.connection = connection;
}
public String translateSelectTop(String tableName, int count)
{
String sql = translator.translateSelectTop(tableName, count);
PreparedStatement ps = null;
try
{
ps = connection.prepareStatement(sql);
ps.execute();
} catch (SQLException e)
{
DbUtils.close(ps);
return "wrong sql";
}
return sql;
}
}
這個(gè)類接受一個(gè)實(shí)現(xiàn)了ISQLTranslator 接口的變量和數(shù)據(jù)庫連接做為構(gòu)造參數(shù),最重要的是這個(gè)類本身也實(shí)現(xiàn)了ISQLTranslator 接口,這樣它就完全能“偽裝”成SQL翻譯器來行使翻譯的責(zé)任了,不過它沒有真正執(zhí)行翻譯,它把翻譯的任務(wù)轉(zhuǎn)發(fā)給了通過構(gòu)造函數(shù)傳遞來的那個(gè)翻譯器變量:
String sql = translator.translateSelectTop(tableName, count);
它自己的真正任務(wù)則是進(jìn)行SQL語句的驗(yàn)證:
ps = connection.prepareStatement(sql);
再次修改調(diào)用代碼:
public static void main(String[] args)
{
String tableName = "T_Table";
int count = 10;
ISQLTranslator translator = new VerifyTranslator(
new SinoServerTranslator(), getConnection());
String sql = translator.translateSelectTop(tableName, count);
System.out.println(sql);
}
運(yùn)行后控制臺打印出:
wrong sql
下面這段代碼看上去是不是很眼熟呢?
ISQLTranslator translator = new VerifyTranslator(new SinoServerTranslator(), getConnection());
這段代碼和我們經(jīng)常寫的流操作非常類似:
InputStream is = new DataInputStream(new FileInputStream(new File(“c:/boot.ini”)));
這就是設(shè)計(jì)模式中經(jīng)常提到的“裝飾者模式”。
針對接口編程
從上面的例子我們可以看出,當(dāng)代碼寫到:
String sql = translator.translateSelectTop(tableName, count);
的時(shí)候,代碼編寫者根本不關(guān)心translator這個(gè)變量到底是哪個(gè)類的實(shí)例,它只知道它調(diào)用了接口約定支持的translateSelectTop方法。
當(dāng)一個(gè)對象需要與其他對象協(xié)作完成一項(xiàng)任務(wù)時(shí),它就需要知道那個(gè)對象,這樣才能調(diào)用那個(gè)對象的方法來獲得服務(wù),這種對象對另一個(gè)協(xié)作對象的依賴就叫做關(guān)聯(lián)。如果一個(gè)關(guān)聯(lián)不是針對具體類,而是針對接口的時(shí)候,任何實(shí)現(xiàn)這個(gè)接口的類都可以滿足要求,因?yàn)檎{(diào)用者僅僅關(guān)心被依賴的對象是不是實(shí)現(xiàn)了特定接口。
當(dāng)發(fā)送的請求和具體的請求響應(yīng)者之間的關(guān)系在運(yùn)行的時(shí)候才能確定的時(shí)候,我們就稱之為動(dòng)態(tài)綁定。動(dòng)態(tài)綁定允許在運(yùn)行期用具有相同接口的對象進(jìn)行替換,從而實(shí)現(xiàn)多態(tài)。多態(tài)使得對象間彼此獨(dú)立,所有的交互操作都通過接口進(jìn)行,并可以在運(yùn)行時(shí)改變它們之間的依賴關(guān)系。
針對接口編程,而不是針對實(shí)現(xiàn)編程是面向?qū)ο箝_發(fā)中的一個(gè)非常重要的原則,也是設(shè)計(jì)模式的精髓!
針對接口編程有數(shù)不清的例子,比如在Hibernate中,集合屬性必須聲明為Set、Map、List等接口類型,而不能聲明為HashSet、HashMap、ArrayList等具體的類型,這是因?yàn)镠ibernate在為了實(shí)現(xiàn)LazyLoad,自己開發(fā)了能實(shí)現(xiàn)LazyLoad功能的實(shí)現(xiàn)了Set、Map、List等接口的類,因?yàn)槲覀兊膶傩缘念愋椭宦暶鳛檫@些屬性為這些接口的類型,因此Hibernate才敢放心大膽的返回這些特定的實(shí)現(xiàn)類。
現(xiàn)實(shí)的開發(fā)過程中有如下一些違反針對接口編程原則的陋習(xí):
陋習(xí)1
ArrayList list = new ArrayList();
for(int i=0;i<10;i++)
{
list.add(……);
}
這里使用的是ArrayList的add方法,而add方法是定義在List接口中的,因此沒有必要聲明list變量為ArrayList類型,修改如下:
List list = new ArrayList();
for(int i=0;i<10;i++)
{
list.add(……);
}
陋習(xí)2
public void fooBar(HashMap map)
{
Object obj = map.get(“something”);
……
}
在這個(gè)方法中只是調(diào)用Map接口的get方法來取數(shù)據(jù),所以就不能要求調(diào)用者一定要傳遞一個(gè)HashMap類型的變量進(jìn)來。修改如下:
public void fooBar(Map map)
{
Object obj = map.get(“something”);
……
}
這樣修改以后用戶為了防止傳遞給fooBar方法的Map被修改,用戶就可以這樣調(diào)用了:
Map unModMap = Collections.unmodifiableMap(map);
obj.fooBar(unModMap);
Collections.unmodifiableMap是JDK提供的一個(gè)工具類,可以返回一個(gè)對map的包裝,返回的map是不可修改的,這也是裝飾者模式的典型應(yīng)用。
試想如果我們把接口聲明為public void fooBar(HashMap map)用戶還能這么調(diào)用嗎?
1.4 抽象類
抽象類的主要作用就是為它的派生類定義公共接口,抽象類把它的部分操作的實(shí)現(xiàn)延遲到派生類中來,派生類也能覆蓋抽象基類的方法,這樣可以很容易的定義新類。抽象類提供了一個(gè)繼承的出發(fā)點(diǎn),我們經(jīng)常定義一個(gè)頂層的抽象類,然后將某些位置的實(shí)現(xiàn)定義為抽象的,也就是我們僅僅定義了實(shí)現(xiàn)的接口,而沒有定義實(shí)現(xiàn)的細(xì)節(jié)。
一個(gè)抽象類應(yīng)該盡可能多的擁有共同的代碼,但是不能把只有特定子類才需要的方法移動(dòng)到抽象類中。Eclipse的某些實(shí)現(xiàn)方式在這一點(diǎn)上就做的不是很好,Eclipse的一些界面類中提供了諸如CreateEmailField之類的方法來創(chuàng)建界面對象,這些方法并不是所有子類都用得到的,應(yīng)該把它們抽取到一個(gè)工具類中更好。同樣的錯(cuò)誤在我們的案例的JCownewDialog中也是存在的,這個(gè)類中就提供了CreateOKBtn、CreateCanceBtn兩個(gè)方法用來創(chuàng)建確定、取消按鈕。
在設(shè)計(jì)模式中,最能體現(xiàn)抽象類優(yōu)點(diǎn)的就是模版方法模式。模版方法模式定義了一個(gè)算法的骨架,而具體的實(shí)現(xiàn)步驟則由具體的子類類來實(shí)現(xiàn)。JDK中的InputStream類是模版方法的典型代表,它對skip等方法給出了實(shí)現(xiàn),而將read等方法定義為抽象方法等待子類去實(shí)現(xiàn)。后邊案例中的PISAbstractAction等類也是模版方法的一個(gè)應(yīng)用。
在實(shí)際開發(fā)中接口和抽象類從兩個(gè)方向?qū)ο到y(tǒng)的復(fù)用做出了貢獻(xiàn),接口定義了系統(tǒng)的服務(wù)契約,而抽象類則為這些服務(wù)定義了公共的實(shí)現(xiàn),子類完全可以從這些抽象類繼承,這樣就不用自己實(shí)現(xiàn)自己所不關(guān)心的方法,如果抽象類提供的服務(wù)實(shí)現(xiàn)不滿足自己的要求,那么就可以自己從頭實(shí)現(xiàn)接口的服務(wù)契約。
歡迎來訪!^.^!
本BLOG僅用于個(gè)人學(xué)習(xí)交流!
目的在于記錄個(gè)人成長.
所有文字均屬于個(gè)人理解.
如有錯(cuò)誤,望多多指教!不勝感激!