記得有個朋友跟我討論過這樣的一個問題,說到他剛剛學習接口和虛基類的相關知識時以爲很迷茫,不知道何時該用接口,何時該使用虛基類。後來慢慢地發現接口能作的事情,虛基類也可以實現,甚至有更多的特色。再後來就慢慢地放棄了接口,把全部的設計和實現都採用虛基類來替代。不能說我這個朋友這樣的處理有錯,可是就我我的對接口和虛基類的理解來講,這樣的作法是有不妥的地方。數據庫
所謂的接口簡單的來講就是個「門口」,而這個"門口"是安裝在某個模塊或者服務上,其目的就是爲了讓外面的世界經過這個「門口」能夠訪問到模塊上的功能或服務。因爲是跟外部環境作對接,所以給它定義爲--接口。而虛基類則更像一間毛胚房,整個架子已經有了(包括門口),想要什麼東西就直接往裏面放,可是擺放的東西跟整個架子的設計有關,不是全部的東西都能亂擺,就好像本來規劃爲洗手間的空間,總不能把牀擺在裏面吧(固然,你樂意也是能夠的。)。編程
說到這裏,其實已經可以感受到它們的區別是什麼了,表面上虛基類感受更增強大一點,能夠像接口那樣聲明一系列的方法(這裏的方法是沒有實現體的,在虛基類中咱們把這類方法叫「虛方法」),又能定義一些共有的屬性;可是,由於虛基類也是一個類型,是必需要繼承與它纔可以擁有這樣的一些特性,因此這就是它的限制和約束。編程語言
而接口總的來講是比虛基類要更加靈活一點,由於它沒有涉及到類的層面,只跟類中方法綁定,不須要指定其類型。也就是說類型實現了接口中所定義的方法,那麼,則能夠爲外部提供這樣的功能。說得通俗一點就是門口你能夠隨便在哪間房子上開。而虛基類則不具備這樣的能力。咱們用代碼來解釋一下上面所說的。學習
//定義接口 interface IAction { function run(); } //定義一個Person類 class Person : IAction { function run() { print("person run..."); } } //定義一個Dog類 class Dog : IAction { function run() { print("dog run..."); } }
上面代碼中定義了一個IAction的接口(通常的高級編程語言中都用interface這個詞來表示接口,在Objective-C中則使用了Protocol一詞來表示接口,其實也挺貼切,由於要調用接口的功能就是要按照其指定的協議來實現,包括傳什麼樣參數,返回什麼值),Person和Dog分別實現了IAction接口,能夠看到Person和Dog是兩個毫無關係的類型。spa
若是換做是虛基類則沒法將這兩種類型關聯起來,由於實現的類型必須繼承該虛基類,可是,有一種變通的作法就是對要關聯的類型進行更高層次的抽象,那上面的例子來講,由於Person和Dog都屬於動物,所以咱們能夠把虛基類定義爲Animal類型。則有下面的作法:設計
//定義虛基類Animal virtual class Animal { //定義虛方法run virtual function run() : void; } //繼承於Animal的Person類 class Person : Animal { function run() { print("person run..."); } } //繼承於Animal的Dog類 class Dog : Animal { function run() { print("dog run..."); } }
經過這樣的作法確實是可以達到想要的效果, 可是若是你以前已經設計好了一個虛基類,對於後續須要在設計中加入這種不相關的類型,那麼你就須要調整以前設計好的虛基類了,明顯要花費額外的時間去作一些重構。code
因此,設計時要選擇使用接口仍是虛基類?我我的以爲虛基類不適合做爲提供外部調用。由於他與類型結構綁定,往後若是要進行調整就會影響對外行爲。可是它能夠做爲內部某些業務處理的公共封裝,配合類工廠模式屏蔽類型上的差別。例如寫一個數據存儲服務,它多是文件存儲,也多是數據庫存儲,咱們能夠進行以下定義:繼承
//定義數據存儲服務的虛基類 virtual class DataStoreService { //定義保存數據的純虛方法 virtual function saveData(data : Object) : void; } //定義文件數據存儲服務類型 class FileStoreService : DataStoreService { var _file:File; function saveData(data : Object) : void { _file.writeData(data); _file.save(); } } //定義數據庫存儲服務類型 class DatabaseStoreService : DataStoreService { var _db:Database; function saveData(data : Object) : void { _db.insertData(data); _db.flush(); } } //定義一個數據存儲類工廠 class DataStoreFactory { //定義數據存儲方式 enum DataStoreType { File, Database } //獲取數據存儲服務方法 function getDataStoreService(type : DataStoreType) : DataStoreService { switch (type) { case File: return new FileStoreService(); case Database: return new DatabaseStoreService(); } } }
如上述代碼所示(上面寫的都是僞代碼,只用於說明意圖),只要使用DataStoreFactory而後根據本身須要的存儲類型就能獲取到不一樣的存儲服務,而返回的類型是定義的虛基類DataStoreService,這樣就可以很好地屏蔽FileStoreService和DatabaseStoreService中的一些設計細節,由於對於調用的人來講這些均可以是透明的。接口
而接口正是咱們須要對外提供功能的一個比較好的方案。一來它不跟類型掛鉤,二來又能像虛基類中的純虛函同樣能夠屏蔽內部實現,對調用者透明不須要他理解裏面的實現原理,只管調用和取得結果。第三個就是對於往後內部設計的升級改造時,無需改變接口的定義,只要把內部實現進行調整便可。咱們來舉個例子,假如以前咱們一直使用文件做爲主要的存儲方式,那麼使用接口來實現,能夠相似以下代碼:get
//定義數據存儲服務接口 interface IDataStoreService { function saveData(data : Object) : void; } //定義文件存儲服務,該類型不對外公開 class FileStoreService : IDataStoreService { var _file : File; function saveData(data : Object) : void { _file.writeData(data); _file.save(); } } //對外公開的Api類型 class Api { function getDataStoreSerivce( ) : IDataStoreService { return new FileStoreService( ); } }
值得注意的是,咱們在設計時必須是要有一個對外公開的類,不然沒法讓外部能夠訪問到內部所提供的接口,上面代碼提供公開類就是Api類型。從代碼上來看咱們的Api類型的getDataStoreService方法只返回了一個IDataStoreService的接口,並不涉及到FileStoreService。因此,當咱們在進行改造時,能夠直接把文件存儲改成數據庫存儲,也不會對外部調用形成任何影響,以下面代碼變動:
//定義數據存儲服務接口 interface IDataStoreService { function saveData(data : Object) : void; } //定義數據庫存儲服務類型 class DatabaseStoreService : IDataStoreService { var _db:Database; function saveData(data : Object) : void { _db.insertData(data); _db.flush(); } } //對外公開的Api類型 class Api { function getDataStoreSerivce( ) : IDataStoreService { return new DatabaseStoreService( ); } }
回到最初我朋友的那個問題,其實要使用虛基類仍是接口來實現功能,這二者實際上是沒有任何衝突的,最好是二者結合使用,虛基類做爲內部封裝的公共元素而存在,能夠根據領域的不一樣劃分多個不一樣的虛基類,而在虛基類中定義的某項功能須要暴露給外界調用時,則可使用接口來定義,一樣根據不一樣的領域能夠劃分多個不一樣的接口。仍是根據上面的例子,咱們把虛基類和接口相結合,造成一個完整的數據存儲服務模塊:
//定義數據存儲服務接口 interface IDataStoreService { function saveData(data : Object) : void; } //定義數據存儲服務的虛基類 virtual class DataStoreService : IDataStoreService { //實現接口方法 function saveData(data : Object) : void { //因爲實現接口的類型不容許不實現接口方法, //所以這裏保留一個空實現方法,等待它的子類重寫該方法。 } } //定義文件數據存儲服務類型 class FileStoreService : DataStoreService { var _file:File; function saveData(data : Object) : void { _file.writeData(data); _file.save(); } } //定義數據庫存儲服務類型 class DatabaseStoreService : DataStoreService { var _db:Database; function saveData(data : Object) : void { _db.insertData(data); _db.flush(); } } //定義一個數據存儲類工廠 class DataStoreFactory { //定義數據存儲方式 enum DataStoreType { File, Database } //獲取數據存儲服務方法 function getDataStoreService(type : DataStoreType) : DataStoreService { switch (type) { case File: return new FileStoreService(); case Database: return new DatabaseStoreService(); } } } //對外公開的Api類型 class Api { function getDataStoreSerivce( ) : IDataStoreService { return DataStoreFactory.getDataStoreService(DataStoreType.Database); } }
接口 用於提供給外部調用的入口,根據功能領域的不一樣來劃分不一樣的接口。其不與類型綁定,只跟類型中的成員方法相關。方便往後內部的升級改造,不影響對外提供的服務。
虛基類 用於內部封裝類型的共有特徵,因爲虛基類不能直接實例化,所以能夠起到屏蔽子類實現細節的效果。搭配類工廠來實現不一樣業務分派給不一樣的子類來進行處理。
在不少高級語言中二者都有定義(即便沒有也能夠代碼層面去模仿和約定),善用這兩種定義可以使本身的設計變得簡單,結構變得清晰。