無論之前你是否聽說過面向接口,本文所描述的將是一個全新的設計思想叫面向接口。這里的接口並不是代碼中的 interface 關鍵字,思想和語言是沒有直接關系的,只不過有些語言實現一種思想比較方便而已。
在了解面向接口前,必須先了解面向對象,因為面向接口是從面向對象根據歷史的經驗衍生出來的一種思想。在面向對象中,一切都是對象,對象擁有獨立性:它必須保持一個內部狀態,並且避免一切外界干擾。所以面向對象要求大部分字段都應該私有的,然后提供有限的公開的接口去訪問這些字段。如:
class 書 { private string 名字; public string get名字(){ return this.名字; } }
初學者經常會問:為什么不直接公開這個字段,一定要寫個get名字的函數才能獲取到書的名字。面向對象的專家會回答這是為了類的封裝性(即獨立性)。名字這個字段對外界來說是只讀不可寫的。
面向對象通過強調對象的獨立性保證了高的代碼重用率,降低了類的學習成本(無需關心內部細節)。但是它卻忘了一個事實:世界萬物都有聯系。於是,當兩個對象開始有聯系時,麻煩的問題就出現了。比如現在新增一個人的類:
class 人 { public void 讀書(書 a){ 讀取( a.get名字() ) } }
人可以讀書,但書也可以被讀。那么讀書這個操作是屬於人還是屬於書,在面向對象中是沒有區分的。雖然書本身是一個獨立的對象,但是人在讀書時,卻又不得不打破書的封裝性----人需要讀取書的內容和更多細節。抓着封裝思想不放,就不得不為書增加API才能讓書真正被讀:一個對象提供哪些API是根據需求來決定的。如果需求很多,那么書的API會非常多,當其他人去讀這份源碼時,會發現本來一個很簡單的對象,卻有很多不知道干嘛用的API。很多API是為了某些需求而寫的,作者將它們塞進這個對象,僅僅為了更方便訪問私有字段。這種代碼設計其實已經偏離了面向對象的初衷,但對作者而言,完成項目才是重點。所以他們選擇這種折中的方式:API隨便加,反正達到訪問的目標即可,打破了封裝又如何?所以也有人說:只有有專家才會在一開始就設計好有哪些對象和API,才能寫出真正面向對象的代碼,這些專家被稱為軟件構架師。面向對象拒而不談對象之間的聯系,導致一開始好好的代碼最后變成互相引用的難以維護的代碼。
關於更多面向對象的缺陷,可以見另一篇文章:《面向對象中的設計陷阱》
接下來介紹面向接口。面向接口中,同樣的一切都是對象,但是它將對象分成兩類:生物和非生物。非生物就是沒有生命的對象:比如一本書,一個電腦。在程序中,非生物總是被動的----比如球自己是不會飛的,它只能被踢飛。生物則代表能力的擁有者,它可以處理非生物,可以記憶,可以和其它生物溝通。
比如現實場景:小明的電腦壞了,然后它交給小剛去修。這里其實有三個對象:小明、小剛、電腦。是人都知道:小明和小剛是生物,電腦是非生物。小明需要做這些事情:1. 記住他有一台電腦,這是他的私有財產。2. 通知小剛去修電腦,並且將他的私有財產轉交給小剛。小剛需要做這些事情:修電腦(不管是誰的電腦)。電腦是非生物,因此它不能做任何事情。那么面向接口中,如何將這個現實問題轉換為代碼表示呢?
原則一:所有的非生物不具備任何封裝性。
以電腦為例,雖然我們總將電腦看成一個獨立的整體,但確實是存在一些時刻,它的零件是打散的。電腦本身沒有思考能力,它不能保證自己一定是處於完整的,能用的狀態。因此對非生物來說,它不需要在內部維護一個狀態。但是它可以有一些必要保護措施,來確保它不會被閑人弄壞,但這個措施不是強制的。就好像你弄壞了電腦,錯在你,不是因為電腦質量差(當然好電腦是不會隨便被搞壞的)。
原則二:所有的生物具有封裝性。
比如小明擁有的電腦是他的私有財產,除非他願意,否則沒人可以使用他的電腦。如果小明主動忘記了他有一台電腦,那么這台電腦和小明將失去任何聯系。
原則三:對事不對人。
比如小明的電腦壞了,他不一定就得交給小剛做,他只要交給一個會修電腦的人來做就行,只不過剛好小剛符合要求而已。
總結如上原則,上述現實問題描述成代碼應該是這樣的:
class 電腦{ public bool 還的還是壞的; } class 小明{ private 電腦 a; public void 去修電腦(會修電腦的人 b){ b.修電腦(a); } } class 小剛 : 會修電腦的人{ public void 修電腦(電腦 a) { a.還的還是壞的 = true; } }
以上代碼和面向對象代碼的區別:
1. 電腦擁有 public 字段:因為電腦是非生物,不需要封裝。
2. 去修電腦的參數是會修電腦的人,而不是小剛。
原則四:能力可以隨時擴展。
以上三個原則其實並未體現面向接口的優勢,真正的優勢在於原則四。現實中,你還是你,但是你的能力是在不斷成長的。面向對象中因為對象是獨立封裝的,即對象先天決定它有哪些能力。但是面向接口中,能力是可以后期擴展的。
比如原來會修電腦的人只能是小剛,后面小明自己也會修電腦了,那么,它甚至可以自己修電腦。而這個修改,並不需要在源碼上進行。因為源碼已經很好體現了這些現實邏輯,不管小明自己會不會修,原來的設計是不變的。一個對象可以擁有多個能力,被多方使用,能力可以是先天的,也可以是后期擴展的。對於上例代碼,現在可以通過如下代碼為小明增加修電腦的能力。
extend 小明: 會修電腦的人 { public void 修電腦(電腦 a){ // 修電腦的邏輯 } }
講到現在似乎都沒有提到接口兩個字。接口其實就是一份證書:用於約定一個生物具有哪些能力。上例中的 “會修電腦的人” 就是一種能力的約定-----即它是一個接口。理論上任何能力都可以用接口來描述,但這顯然不現實(生活中不可能為任何能力都提供一份證書:會燒飯證書?會寫字證書?)因此,我們使用同一類生物還借代:比如小剛是會修電腦的人,那么用小剛來借代所有會修電腦的人(就像平時說的鄰居家的孩子來借代擁有各種能力的孩子)。
面向接口中,我們假設一切生物在起初是沒有任何能力的,所有能力都是后期根據需要再提供的。但是這些能力和這個生物本身的資產是沒有關系的。