面向接口编程思想(转)
http://www.cnblogs.com/Gavinzhao/archive/2009/11/10/1599700.html
本文基于署名-非商業性使用 3.0許可協議發布,歡迎轉載,演繹,但是必須保留本文的署名張洋(包含鏈接),且不得用于商業目的。如您有任何疑問或者授權方面的協商,請與我聯系。
我想,對于各位使用面向對象編程語言的程序員來說,“接口”這個名詞一定不陌生,但是不知各位有沒有這樣的疑惑:接口有什么用途?它和抽象類有什么區別?能不能用抽象類代替接口呢?而且,作為程序員,一定經常聽到“面向接口編程”這個短語,那么它是什么意思?有什么思想內涵?和面向對象編程是什么關系?本文將一一解答這些疑問。
1.面向接口編程和面向對象編程是什么關系
????? 首先,面向接口編程和面向對象編程并不是平級的,它并不是比面向對象編程更先進的一種獨立的編程思想,而是附屬于面向對象思想體系,屬于其一部分。或者說,它是面向對象編程體系中的思想精髓之一。
2.接口的本質
????? 接口,在表面上是由幾個沒有主體代碼的方法定義組成的集合體,有唯一的名稱,可以被類或其他接口所實現(或者也可以說繼承)。它在形式上可能是如下的樣子:
{
????void?Method1();
????void?Method2(int?para1);
????void?Method3(string?para2,string?para3);
}
????? 那么,接口的本質是什么呢?或者說接口存在的意義是什么。我認為可以從以下兩個視角考慮:
??????1)接口是一組規則的集合,它規定了實現本接口的類或接口必須擁有的一組規則。體現了自然界“如果你是……則必須能……”的理念。
????? 例如,在自然界中,人都能吃飯,即“如果你是人,則必須能吃飯”。那么模擬到計算機程序中,就應該有一個IPerson(習慣上,接口名由“I”開頭)接口,并有一個方法叫Eat(),然后我們規定,每一個表示“人”的類,必須實現IPerson接口,這就模擬了自然界“如果你是人,則必須能吃飯”這條規則。
????? 從這里,我想各位也能看到些許面向對象思想的東西。面向對象思想的核心之一,就是模擬真實世界,把真實世界中的事物抽象成類,整個程序靠各個類的實例互相通信、互相協作完成系統功能,這非常符合真實世界的運行狀況,也是面向對象思想的精髓。
??????2)接口是在一定粒度視圖上同類事物的抽象表示。注意這里我強調了在一定粒度視圖上,因為“同類事物”這個概念是相對的,它因為粒度視圖不同而不同。
????? 例如,在我的眼里,我是一個人,和一頭豬有本質區別,我可以接受我和我同學是同類這個說法,但絕不能接受我和一頭豬是同類。但是,如果在一個動物學家眼里,我和豬應該是同類,因為我們都是動物,他可以認為“人”和“豬”都實現了IAnimal這個接口,而他在研究動物行為時,不會把我和豬分開對待,而會從“動物”這個較大的粒度上研究,但他會認為我和一棵樹有本質區別。
??????現在換了一個遺傳學家,情況又不同了,因為生物都能遺傳,所以在他眼里,我不僅和豬沒區別,和一只蚊子、一個細菌、一顆樹、一個蘑菇乃至一個SARS病毒都沒什么區別,因為他會認為我們都實現了IDescendable這個接口(注:descend?vi. 遺傳),即我們都是可遺傳的東西,他不會分別研究我們,而會將所有生物作為同類進行研究,在他眼里沒有人和病毒之分,只有可遺傳的物質和不可遺傳的物質。但至少,我和一塊石頭還是有區別的。
????? 可不幸的事情發生了,某日,地球上出現了一位偉大的人,他叫列寧,他在熟讀馬克思、恩格斯的辯證唯物主義思想巨著后,頗有心得,于是他下了一個著名的定義:所謂物質,就是能被意識所反映的客觀實在。至此,我和一塊石頭、一絲空氣、一條成語和傳輸手機信號的電磁場已經沒什么區別了,因為在列寧的眼里,我們都是可以被意識所反映的客觀實在。如果列寧是一名程序員,他會這么說:所謂物質,就是所有同時實現了“IReflectabe”和“IEsse”兩個接口的類所生成的實例。(注:reflect v. 反映? esse n. 客觀實在)
????? 也許你會覺得我上面的例子像在瞎掰,但是,這正是接口得以存在的意義。面向對象思想和核心之一叫做多態性,什么叫多態性?說白了就是在某個粒度視圖層面上對同類事物不加區別的對待而統一處理。而之所以敢這樣做,就是因為有接口的存在。像那個遺傳學家,他明白所有生物都實現了IDescendable接口,那只要是生物,一定有Descend()這個方法,于是他就可以統一研究,而不至于分別研究每一種生物而最終累死。
????? 可能這里還不能給你一個關于接口本質和作用的直觀印象。那么在后文的例子和對幾個設計模式的解析中,你將會更直觀體驗到接口的內涵。
3.面向接口編程綜述
????? 通過上文,我想大家對接口和接口的思想內涵有了一個了解,那么什么是面向接口編程呢?我個人的定義是:在系統分析和架構中,分清層次和依賴關系,每個層次不是直接向其上層提供服務(即不是直接實例化在上層中),而是通過定義一組接口,僅向上層暴露其接口功能,上層對于下層僅僅是接口依賴,而不依賴具體類。
????? 這樣做的好處是顯而易見的,首先對系統靈活性大有好處。當下層需要改變時,只要接口及接口功能不變,則上層不用做任何修改。甚至可以在不改動上層代碼時將下層整個替換掉,就像我們將一個WD的60G硬盤換成一個希捷的160G的硬盤,計算機其他地方不用做任何改動,而是把原硬盤拔下來、新硬盤插上就行了,因為計算機其他部分不依賴具體硬盤,而只依賴一個IDE接口,只要硬盤實現了這個接口,就可以替換上去。從這里看,程序中的接口和現實中的接口極為相似,所以我一直認為,接口(interface)這個詞用的真是神似!
????? 使用接口的另一個好處就是不同部件或層次的開發人員可以并行開工,就像造硬盤的不用等造CPU的,也不用等造顯示器的,只要接口一致,設計合理,完全可以并行進行開發,從而提高效率。
????? 本篇文章先到這里。最后我想再啰嗦一句:面向對象的精髓是模擬現實,這也可以說是我這篇文章的靈魂。所以,多從現實中思考面向對象的東西,對提高系統分析設計能力大有脾益。
????? 下篇文章,我將用一個實例來展示接口編程的基本方法。
????? 而第三篇,我將解析經典設計模式中的一些面向接口編程思想,并解析一下.NET分層架構中的面向接口思想。
對本文的補充:
??????仔細看了各位的回復,非常高興能和大家一起討論技術問題。感謝給出肯定的朋友,也要感謝提出意見和質疑的朋友,這促使我更深入思考一些東西,希望能借此進步。在這里我想補充一些東西,以討論一些回復中比較集中的問題。
1.關于“面向接口編程”中的“接口”與具體面向對象語言中“接口”兩個詞
????? 看到有朋友提出“面向接口編程”中的“接口”二字應該比單純編程語言中的interface范圍更大。我經過思考,覺得很有道理。這里我寫的確實不太合理。我想,面向對象語言中的“接口”是指具體的一種代碼結構,例如C#中用interface關鍵字定義的接口。而“面向接口編程”中的“接口”可以說是一種從軟件架構的角度、從一個更抽象的層面上指那種用于隱藏具體底層類和實現多態性的結構部件。從這個意義上說,如果定義一個抽象類,并且目的是為了實現多態,那么我認為把這個抽象類也稱為“接口”是合理的。但是用抽象類實現多態合理不合理?在下面第二條討論。
????? 概括來說,我覺得兩個“接口”的概念既相互區別又相互聯系。“面向接口編程”中的接口是一種思想層面的用于實現多態性、提高軟件靈活性和可維護性的架構部件,而具體語言中的“接口”是將這種思想中的部件具體實施到代碼里的手段。
2.關于抽象類與接口
????? 看到回復中這是討論的比較激烈的一個問題。很抱歉我考慮不周沒有在文章中討論這個問題。我個人對這個問題的理解如下:
????? 如果單從具體代碼來看,對這兩個概念很容易模糊,甚至覺得接口就是多余的,因為單從具體功能來看,除多重繼承外(C#,Java中),抽象類似乎完全能取代接口。但是,難道接口的存在是為了實現多重繼承?當然不是。我認為,抽象類和接口的區別在于使用動機。使用抽象類是為了代碼的復用,而使用接口的動機是為了實現多態性。所以,如果你在為某個地方該使用接口還是抽象類而猶豫不決時,那么可以想想你的動機是什么。
????? 看到有朋友對IPerson這個接口的質疑,我個人的理解是,IPerson這個接口該不該定義,關鍵看具體應用中是怎么個情況。如果我們的項目中有Women和Man,都繼承Person,而且Women和Man絕大多數方法都相同,只有一個方法DoSomethingInWC()不同(例子比較粗俗,各位見諒),那么當然定義一個AbstractPerson抽象類比較合理,因為它可以把其他所有方法都包含進去,子類只定義DoSomethingInWC(),大大減少了重復代碼量。
????? 但是,如果我們程序中的Women和Man兩個類基本沒有共同代碼,而且有一個PersonHandle類需要實例化他們,并且不希望知道他們是男是女,而只需把他們當作人看待,并實現多態,那么定義成接口就有必要了。
????? 總而言之,接口與抽象類的區別主要在于使用的動機,而不在于其本身。而一個東西該定義成抽象類還是接口,要根據具體環境的上下文決定。
????? 再者,我認為接口和抽象類的另一個區別在于,抽象類和它的子類之間應該是一般和特殊的關系,而接口僅僅是它的子類應該實現的一組規則。(當然,有時也可能存在一般與特殊的關系,但我們使用接口的目的不在這里)如,交通工具定義成抽象類,汽車、飛機、輪船定義成子類,是可以接受的,因為汽車、飛機、輪船都是一種特殊的交通工具。再譬如Icomparable接口,它只是說,實現這個接口的類必須要可以進行比較,這是一條規則。如果Car這個類實現了Icomparable,只是說,我們的Car中有一個方法可以對兩個Car的實例進行比較,可能是比哪輛車更貴,也可能比哪輛車更大,這都無所謂,但我們不能說“汽車是一種特殊的可以比較”,這在文法上都不通。
--------------------------------------------------
通過上一篇文章的討論,我想各位朋友對“面接接口編程”有了一個大致的了解。那么在這一篇里,我們用一個例子,讓各位對這個重要的編程思想有個直觀的印象。為充分考慮到初學者,所以這個例子非常簡單,望各位高手見諒。
問題的提出?
定義:現在我們要開發一個應用,模擬移動存儲設備的讀寫,即計算機與U盤、MP3、移動硬盤等設備進行數據交換。
上下文(環境):已知要實現U盤、MP3播放器、移動硬盤三種移動存儲設備,要求計算機能同這三種設備進行數據交換,并且以后可能會有新的第三方的移動存儲設備,所以計算機必須有擴展性,能與目前未知而以后可能會出現的存儲設備進行數據交換。各個存儲設備間讀、寫的實現方法不同,U盤和移動硬盤只有這兩個方法,MP3Player還有一個PlayMusic方法。
名詞定義:數據交換={讀,寫}
?看到上面的問題,我想各位腦子中一定有了不少想法,這是個很好解決的問題,很多方案都能達到效果。下面,我列舉幾個典型的方案。
解決方案列舉
方案一:分別定義FlashDisk、MP3Player、MobileHardDisk三個類,實現各自的Read和Write方法。然后在Computer類中實例化上述三個類,為每個類分別寫讀、寫方法。例如,為FlashDisk寫ReadFromFlashDisk、WriteToFlashDisk兩個方法。總共六個方法。
方案二:定義抽象類MobileStorage,在里面寫虛方法Read和Write,三個存儲設備繼承此抽象類,并重寫Read和Write方法。Computer類中包含一個類型為MobileStorage的成員變量,并為其編寫get/set器,這樣Computer中只需要兩個方法:ReadData和WriteData,并通過多態性實現不同移動設備的讀寫。
方案三:與方案二基本相同,只是不定義抽象類,而是定義接口IMobileStorage,移動存儲器類實現此接口。Computer中通過依賴接口IMobileStorage實現多態性。
方案四:定義接口IReadable和IWritable,兩個接口分別只包含Read和Write,然后定義接口IMobileStorage接口繼承自IReadable和IWritable,剩下的實現與方案三相同。
?下面,我們來分析一下以上四種方案:
首先,方案一最直白,實現起來最簡單,但是它有一個致命的弱點:可擴展性差。或者說,不符合“開放-關閉原則”(注:意為對擴展開放,對修改關閉)。當將來有了第三方擴展移動存儲設備時,必須對Computer進行修改。這就如在一個真實的計算機上,為每一種移動存儲設備實現一個不同的插口、并分別有各自的驅動程序。當有了一種新的移動存儲設備后,我們就要將計算機大卸八塊,然后增加一個新的插口,在編寫一套針對此新設備的驅動程序。這種設計顯然不可取。
此方案的另一個缺點在于,冗余代碼多。如果有100種移動存儲,那我們的Computer中豈不是要至少寫200個方法,這是不能接受的!
我們再來看方案二和方案三,之所以將這兩個方案放在一起討論,是因為他們基本是一個方案(從思想層面上來說),只不過實現手段不同,一個是使用了抽象類,一個是使用了接口,而且最終達到的目的應該是一樣的。
我們先來評價這種方案:首先它解決了代碼冗余的問題,因為可以動態替換移動設備,并且都實現了共同的接口,所以不管有多少種移動設備,只要一個Read方法和一個Write方法,多態性就幫我們解決問題了。而對第一個問題,由于可以運行時動態替換,而不必將移動存儲類硬編碼在Computer中,所以有了新的第三方設備,完全可以替換進去運行。這就是所謂的“依賴接口,而不是依賴與具體類”,不信你看看,Computer類只有一個MobileStorage類型或IMobileStorage類型的成員變量,至于這個變量具體是什么類型,它并不知道,這取決于我們在運行時給這個變量的賦值。如此一來,Computer和移動存儲器類的耦合度大大下降。
那么這里該選抽象類還是接口呢?還記得第一篇文章我對抽象類和接口選擇的建議嗎?看動機。這里,我們的動機顯然是實現多態性而不是為了代碼復用,所以當然要用接口。
最后我們再來看一看方案四,它和方案三很類似,只是將“可讀”和“可寫”兩個規則分別抽象成了接口,然后讓IMobileStorage再繼承它們。這樣做,顯然進一步提高了靈活性,但是,這有沒有設計過度的嫌疑呢?我的觀點是:這要看具體情況。如果我們的應用中可能會出現一些類,這些類只實現讀方法或只實現寫方法,如只讀光盤,那么這樣做也是可以的。如果我們知道以后出現的東西都是能讀又能寫的,那這兩個接口就沒有必要了。其實如果將只讀設備的Write方法留空或拋出異常,也可以不要這兩個接口。總之一句話:理論是死的,人是活的,一切從現實需要來,防止設計不足,也要防止設計過度。
在這里,我們姑且認為以后的移動存儲都是能讀又能寫的,所以我們選方案三。
實現
下面,我們要將解決方案加以實現。我選擇的語言是C#,但是在代碼中不會用到C#特有的性質,所以使用其他語言的朋友一樣可以參考。
首先編寫IMobileStorage接口:
Code:IMobileStorage
1namespace?InterfaceExample2{
3????public?interface?IMobileStorage
4????{
5????????void?Read();//從自身讀數據
6????????void?Write();//將數據寫入自身
7????}
8}
代碼比較簡單,只有兩個方法,沒什么好說的,接下來是三個移動存儲設備的具體實現代碼:
U盤
Code:FlashDisk
?1namespace?InterfaceExample?2{
?3????public?class?FlashDisk?:?IMobileStorage
?4????{
?5????????public?void?Read()
?6????????{
?7????????????Console.WriteLine("Reading?from?FlashDisk……");
?8????????????Console.WriteLine("Read?finished!");
?9????????}
10
11????????public?void?Write()
12????????{
13????????????Console.WriteLine("Writing?to?FlashDisk……");
14????????????Console.WriteLine("Write?finished!");
15????????}
16????}
17}
MP3
Code:MP3Player
?1namespace?InterfaceExample?2{
?3????public?class?MP3Player?:?IMobileStorage
?4????{
?5????????public?void?Read()
?6????????{
?7????????????Console.WriteLine("Reading?from?MP3Player……");
?8????????????Console.WriteLine("Read?finished!");
?9????????}
10
11????????public?void?Write()
12????????{
13????????????Console.WriteLine("Writing?to?MP3Player……");
14????????????Console.WriteLine("Write?finished!");
15????????}
16
17????????public?void?PlayMusic()
18????????{
19????????????Console.WriteLine("Music?is?playing……");
20????????}
21????}
22}
移動硬盤
Code:MobileHardDisk
?1namespace?InterfaceExample?2{
?3????public?class?MobileHardDisk?:?IMobileStorage
?4????{
?5????????public?void?Read()
?6????????{
?7????????????Console.WriteLine("Reading?from?MobileHardDisk……");
?8????????????Console.WriteLine("Read?finished!");
?9????????}
10
11????????public?void?Write()
12????????{
13????????????Console.WriteLine("Writing?to?MobileHardDisk……");
14????????????Console.WriteLine("Write?finished!");
15????????}
16????}
17}
可以看到,它們都實現了IMobileStorage接口,并重寫了各自不同的Read和Write方法。下面,我們來寫Computer:
Code:Computer
?1namespace?InterfaceExample?2{
?3????public?class?Computer
?4????{
?5????????private?IMobileStorage?_usbDrive;
?6
?7????????public?IMobileStorage?UsbDrive
?8????????{
?9????????????get
10????????????{
11????????????????return?this._usbDrive;
12????????????}
13????????????set
14????????????{
15????????????????this._usbDrive?=?value;
16????????????}
17????????}
18
19????????public?Computer()
20????????{
21????????}
22
23????????public?Computer(IMobileStorage?usbDrive)
24????????{
25????????????this.UsbDrive?=?usbDrive;
26????????}
27????
28????????public?void?ReadData()
29????????{
30????????????this._usbDrive.Read();
31????????}
32
33????????public?void?WriteData()
34????????{
35????????????this._usbDrive.Write();
36????????}
37????}
38}
其中的UsbDrive就是可替換的移動存儲設備,之所以用這個名字,是為了讓大家覺得直觀,就像我們平常使用電腦上的USB插口插拔設備一樣。
OK!下面我們來測試我們的“電腦”和“移動存儲設備”是否工作正常。我是用的C#控制臺程序,具體代碼如下:
Code:測試代碼
?1namespace?InterfaceExample?2{
?3????class?Program
?4????{
?5????????static?void?Main(string[]?args)
?6????????{
?7????????????Computer?computer?=?new?Computer();
?8????????????IMobileStorage?mp3Player?=?new?MP3Player();
?9????????????IMobileStorage?flashDisk?=?new?FlashDisk();
10????????????IMobileStorage?mobileHardDisk?=?new?MobileHardDisk();
11
12????????????Console.WriteLine("I?inserted?my?MP3?Player?into?my?computer?and?copy?some?music?to?it:");
13????????????computer.UsbDrive?=?mp3Player;
14????????????computer.WriteData();
15????????????Console.WriteLine();
16
17????????????Console.WriteLine("Well,I?also?want?to?copy?a?great?movie?to?my?computer?from?a?mobile?hard?disk:");
18????????????computer.UsbDrive?=?mobileHardDisk;
19????????????computer.ReadData();
20????????????Console.WriteLine();
21
22????????????Console.WriteLine("OK!I?have?to?read?some?files?from?my?flash?disk?and?copy?another?file?to?it:");
23????????????computer.UsbDrive?=?flashDisk;
24????????????computer.ReadData();
25????????????computer.WriteData();
26????????????Console.ReadLine();
27????????}
28????}
29}
現在編譯、運行程序,如果沒有問題,將看到如下運行結果:
圖2.1 各種移動存儲設備測試結果
好的,看來我們的系統工作良好。
后來……
剛過了一個星期,就有人送來了新的移動存儲設備NewMobileStorage,讓我測試能不能用,我微微一笑,心想這不是小菜一碟,讓我們看看面向接口編程的威力吧!將測試程序修改成如下:
Code:測試代碼
?1namespace?InterfaceExample?2{
?3????class?Program
?4????{
?5????????static?void?Main(string[]?args)
?6????????{
?7????????????Computer?computer?=?new?Computer();
?8????????????IMobileStorage?newMobileStorage?=?new?NewMobileStorage();
?9
10????????????Console.WriteLine("Now,I?am?testing?the?new?mobile?storage:");
11????????????computer.UsbDrive?=?newMobileStorage;
12????????????computer.ReadData();
13????????????computer.WriteData();
14????????????Console.ReadLine();
15????????}
16????}
17}
編譯、運行、看結果:
哈哈,神奇吧,Computer一點都不用改動,就可以使新的設備正常運行。這就是所謂“對擴展開放,對修改關閉”。
圖2.2 新設備擴展測試結果
又過了幾天,有人通知我說又有一個叫SuperStorage的移動設備要接到我們的Computer上,我心想來吧,管你是“超級存儲”還是“特級存儲”,我的“面向接口編程大法”把你們統統搞定。
但是,當設備真的送來,我傻眼了,開發這個新設備的團隊沒有拿到我們的IMobileStorage接口,自然也沒有遵照這個約定。這個設備的讀、寫方法不叫Read和Write,而是叫rd和wt,這下完了……不符合接口啊,插不上。但是,不要著急,我們回到現實來找找解決的辦法。我們一起想想:如果你的Computer上只有USB接口,而有人拿來一個PS/2的鼠標要插上用,你該怎么辦?想起來了吧,是不是有一種叫“PS/2-USB”轉換器的東西?也叫適配器,可以進行不同接口的轉換。對了!程序中也有轉換器。
這里,我要引入一個設計模式,叫“Adapter”。它的作用就如現實中的適配器一樣,把接口不一致的兩個插件接合起來。由于本篇不是講設計模式的,而且Adapter設計模式很好理解,所以我就不細講了,先來看我設計的類圖吧:
如圖所示,雖然SuperStorage沒有實現IMobileStorage,但我們定義了一個實現IMobileStorage的SuperStorageAdapter,它聚合了一個SuperStorage,并將rd和wt適配為Read和Write,SuperStorageAdapter
圖2.3 Adapter模式應用示意
具體代碼如下:
Code:SuperStorageAdapter
?1namespace?InterfaceExample?2{
?3????public?class?SuperStorageAdapter?:?IMobileStorage
?4????{
?5????????private?SuperStorage?_superStorage;
?6
?7????????public?SuperStorage?SuperStorage
?8????????{
?9????????????get
10????????????{
11????????????????return?this._superStorage;
12????????????}
13????????????set
14????????????{
15????????????????this._superStorage?=?value;
16????????????}
17????????}
18????
19????????public?void?Read()
20????????{
21????????????this._superStorage.rd();
22????????}
23
24????????public?void?Write()
25????????{
26????????????this._superStorage.wt();
27????????}
28????}
29}
好,現在我們來測試適配過的新設備,測試代碼如下:
Code:測試代碼
?1namespace?InterfaceExample?2{
?3????class?Program
?4????{
?5????????static?void?Main(string[]?args)
?6????????{
?7????????????Computer?computer?=?new?Computer();
?8????????????SuperStorageAdapter?superStorageAdapter?=?new?SuperStorageAdapter();
?9????????????SuperStorage?superStorage?=?new?SuperStorage();
10????????????superStorageAdapter.SuperStorage?=?superStorage;
11
12????????????Console.WriteLine("Now,I?am?testing?the?new?super?storage?with?adapter:");
13????????????computer.UsbDrive?=?superStorageAdapter;
14????????????computer.ReadData();
15????????????computer.WriteData();
16????????????Console.ReadLine();
17????????}
18????}
19}
運行后會得到如下結果:
圖2.4 利用Adapter模式運行新設備測試結果
OK!雖然遇到了一些困難,不過在設計模式的幫助下,我們還是在沒有修改Computer任何代碼的情況下實現了新設備的運行。
?好了,理論在第一篇講得足夠多了,所以這里我就不多講了。希望各位朋友結合第一篇的理論和這個例子,仔細思考面向接口的問題。當然,不要忘了結合現實。
下一篇,我將解析經典設計模式中的面向接口編程思想和.NET平臺分層架構中接口的運用。
本文基于署名-非商業性使用 3.0許可協議發布,歡迎轉載,演繹,但是必須保留本文的署名張洋(包含鏈接),且不得用于商業目的。如您有任何疑問或者授權方面的協商,請與我聯系。
-----------------------------------------
通過前面兩篇,我想各位朋友對“面向接口編程”的思想有了一定認識,并通過第二篇的例子,獲得了一定的直觀印象。但是,第二篇中的例子旨在展示面向接口編程的實現方法,比較簡單,不能體現出面向接口編程的優勢和這種思想的內涵。那么,這一篇作為本系列的終結篇,將通過分析幾個比較有深度的模式或架構,解析隱藏其背后的面向接口思想。這篇我將要分析的分別是MVC模式和.NET平臺的分層架構。
這篇的內容可能會比較抽象,望諒解。
1.從MVC開始
MVC簡介:
本文不打算詳細解釋MVC架構,而是把重點放在其中的面向接口思想上。所以在這里,只對MVC做一個簡略的介紹。
MVC是一種用于表示層設計的復合設計模式。M、V、C分別表示模型(Model)、View(視圖)、Controller(控制器)。它們的職責如下:
模型:用于存儲應用中的數據及運行邏輯,是應用的實體。
視圖:負責可視部分,用于與用戶交互及呈現數據。視圖只負責顯示,不負責將用戶的操作行為解釋給模型。
控制器:負責將用戶的行為解釋給模型。根據指定的策略和用戶的操作,調用模型的邏輯。
關于三者的關系,我畫了一張圖,大家請看:
圖3.1 MVC模式示意它們之間的交互有以下幾種:
1.當用戶在視圖上做任何需要調用模型的操作時,它的請求將被控制器?截獲。
2.控制器按照自身指定的策略,將用戶行為翻譯成模型操作,調用模型相應邏輯實現。
3.控制器可能會在接到視圖操作時,指定視圖做某些改變。
4.當模型的狀態發生改變時,將通過某種方式通知視圖。
5.視圖可以從模型獲取狀態,從而改變自己的顯示。
?MVC介紹完了,那么可能會有人問,我們的主題呢?面向接口思想呢?其實,MVC中處處都存在面向接口的影子。下面,我對其中幾個側面進行解釋。
1.首先我們可以看到,視圖和模型是有直接交互的,也就是上面的4、5兩點。但是有一點可能會讓你吃驚:它們兩個誰也不“認識”誰,即它們相互并不知道對方是做什么的、有什么屬性、有什么方法,但是它們能交互。這是怎么做到的呢?因為它們個各知道對方實現了某一個接口。
此乃面向接口思想一大作用:使相互不認識的類進行交互。這樣做是很有好處的,首先它們之間的耦合度大大降低,其次雙方都可以進行替換,只要實現了相同的接口,就沒有問題。
打個不太恰當的比喻。我們都知道120這個電話號碼,是急救電話。其實120就是個接口,因為當你撥打這個電話時,你不知道那邊是哪所醫院,甚至不知道那邊是不是醫院,你只知道電話那頭的地方可以救人,也可以說實現了IHelp接口。這樣,你通過一個號碼可以說同全部的救人機構聯系起來了,當有緊急事件,接線控制那邊會將你的請求接到最近可用的機構,你就可以最快的得到幫助。
現在我們假設沒有使用面向接口思想,來看看會發生什么恐怖的事情:首先,我家的120號碼是綁定在本市第一人民醫院的,即當我撥打120時,只能撥通第一人民醫院。如果有一天我食物中毒了,急忙撥通了120,但是電話那邊告訴我他們醫院的救護車都派出去了,我問那怎么接通別家醫院的電話,那邊的MM很溫柔的告訴我,讓我打電話給網通公司,然后重新為我布線。于是我吐血而亡……
言歸正傳。這里,我要引入一個設計模式,叫觀察著(Observer)模式。這個模式大約是這樣的:整個模式中有兩種實體:觀察者和被觀察者,它們分別實現一個接口,這里我們姑且叫做IObserver與IObserverSubject。IObserver只有一個方法,例如叫Update,當被觀察者狀態改變時,調用這個方法,用來通知觀察者。IObserverSubject接口有兩個方法,都是供觀察者調用。一個用來將觀察者注冊為此被觀察者的觀察對象,另一個用于將觀察者移除。
一般情況下,一個被觀察者對應多個觀察者。
在MVC中,視圖是觀察者,模型是被觀察者,當模型狀態改變時,調用所有觀察者的Update方法,通知視圖模型有變,視圖在Update方法里寫下響應代碼,完成操作。通過這個方法,視圖和模型就可以在僅依賴接口的情形下進行交互,而不必強耦合,而且在模型不變的情況下,視圖可以隨意替換。(只要實現了IObserver)
2. 在MVC中另一個使用接口的地方就是控制器,這里我要首先引入一個設計模式:策略模式(Strategy)。在MVC中,控制器就使用了這個模式。
剛才我說過,視圖負責與用戶交互,但是,它只負責界面顯示部分,至于當用戶做了某個操作(如單擊某個按鈕)后系統應該怎么反應,視圖并不負責,它只是將這個動作交給控制器,控制器根據內置的策略,將用戶操作翻譯成模型的邏輯。這就是說,同一個視圖、同一種操作,模型可以做出不同的反應,這取決與控制器的內置策略。所以,我們的系統中可以有很多控制器,它們有不同的策略,當視圖希望改變策略時,它可以更換控制器。怎么實現呢?這就需要視圖不能和具體控制器耦合,而是要僅依賴一個控制器接口(如IController),并聚合一個IController的實例。當希望更改策略時,可以在系統運行時動態更換Controller,這就是策略模式的實現。
關于MVC的接口思想就先介紹到這里。其實MVC中還有很多地方用到面向接口,由于本文不是專門介紹MVC或設計模式的,所以對用到的模式沒有做詳解,而是把重點放在其中的面向接口思想上。如果沒有設計模式的基礎,讀上文可能會有些困難,希望各位見諒!我打算在以后專門寫文章來解析MVC。
2..NET平臺下分層架構的面向接口思想
我們知道,在做大一點的系統應用時(特別是B/S架構),比較好的方法是分層架構。所謂分層架構,是指將系統從職責上分成若干層,每層各司其職,上層依賴下層完成操作。
在.NET平臺上,比較經典的分層架構是三層架構,從下到上依次是:數據訪問層、業務邏輯層、表示層。各層職責如下:
數據訪問層:負責與數據源交互,完成數據訪問等一系列操作。
業務邏輯層:完成與系統業務有關的邏輯操作。
表示層:負責與用戶交互、呈現數據等一切與系統表示有關的操作。
剛才我們說過,分層架構下是向下依賴的(不考慮依賴倒置),也就是業務邏輯層要調用數據訪問層完成與數據源有關的操作,而表示層調用業務邏輯層完成業務邏輯工作。但是,表示層對數據訪問層是沒有依賴的。
在這個架構中,每一層都不是一個類,而是一個類族,例如,在一個CMS系統中,數據訪問層可能會有一系列的類,分別負責用戶、文章、評論等業務實體的數據訪問操作,而業務邏輯層也一樣。如果我們直接依賴,即業務邏輯層實例化數據訪問層的類,表示層再實例化業務邏輯層的類,會造成強耦合。如果我想把數據庫從SQLServer換成MySQL,則要改變整個業務邏輯層代碼,這是個不好的設計。(還記得“開放-關閉”原則嗎)所以,一般的做法是,為數據訪問層和業務邏輯層分別定義一族接口,業務邏輯層不依賴具體的數據訪問層,而是僅依賴數據訪問層的接口族,表示層也一樣,依賴業務邏輯層的接口族。如此一來,當要更換數據庫時,我們就不必改寫整個業務邏輯層,因為業務邏輯層里根本沒有任何數據訪問層中的具體類,而全是通過接口實現的。在.NET中,只要配合配置文件和反射機制,再運用Abstract Factory設計模式,就可以實現“依賴注入”,即在不改動代碼的情況下根據配置選擇相應的層次組件。這樣,我們就可以為不通數據庫分別實現數據訪問層,也可以編寫ORM的數據訪問層,甚至是基于XML的,只要實現了數據訪問層接口族,就可以和業務邏輯層無縫連接,從而極大提高了軟件的靈活性和可維護性。當然要更改業務邏輯層也是一樣。
如果說,前面的例子都是從微觀視角討論接口,那么,這個例子則從宏觀視角展現了面向接口編程的內涵和優勢。很抱歉在這里不能對這個架構深入講解,有興趣的朋友可以參考微軟的官方示例.NET PetShop4。(但是請注意,這個示例中業務邏輯層沒有定義接口族,而是強耦合于表示層中,這可能是因為考慮到在這個系統中業務邏輯沒有更改的可能。另外由于是個示例,不是真正的B2C系統,所以業務邏輯層很簡單。)
好了,本系列文章就到這里。希望各位朋友通過這三篇文章,能對“面向接口編程”有一定的了解。當然,我只是起到一個拋磚引玉的作用,其真正的內涵和精髓,還需要各位從實踐中慢慢認識。還有,就是面向接口思想不是孤立的,它和設計模式等內容都是面向對象大系中的精華,而且是相互滲透、相互聯系的。其實,很多設計模式就是面向接口思想的體現。我們應該把這些放在一起學習,從而真正提供自己的面向對象思考能力和實戰能力。
總結
以上是生活随笔為你收集整理的面向接口编程思想(转)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 物联网空开价格_智慧物联网空开断路器
- 下一篇: JS事件冒泡浏览器兼容