【選擇恐懼症】接口?虛基類?

症前兆

記得有個朋友跟我討論過這樣的一個問題,說到他剛剛學習接口虛基類的相關知識時以爲很迷茫,不知道何時該用接口,何時該使用虛基類。後來慢慢地發現接口能作的事情,虛基類也可以實現,甚至有更多的特色。再後來就慢慢地放棄了接口,把全部的設計和實現都採用虛基類來替代。不能說我這個朋友這樣的處理有錯,可是就我我的對接口和虛基類的理解來講,這樣的作法是有不妥的地方。數據庫

症分析

所謂的接口簡單的來講就是個「門口」,而這個"門口"是安裝在某個模塊或者服務上,其目的就是爲了讓外面的世界經過這個「門口」能夠訪問到模塊上的功能或服務。因爲是跟外部環境作對接,所以給它定義爲--接口。而虛基類則更像一間毛胚房,整個架子已經有了(包括門口),想要什麼東西就直接往裏面放,可是擺放的東西跟整個架子的設計有關,不是全部的東西都能亂擺,就好像本來規劃爲洗手間的空間,總不能把牀擺在裏面吧(固然,你樂意也是能夠的。)。編程

症解答

說到這裏,其實已經可以感受到它們的區別是什麼了,表面上虛基類感受更增強大一點,能夠像接口那樣聲明一系列的方法(這裏的方法是沒有實現體的,在虛基類中咱們把這類方法叫「虛方法」),又能定義一些共有的屬性;可是,由於虛基類也是一個類型,是必需要繼承與它纔可以擁有這樣的一些特性,因此這就是它的限制和約束。編程語言

接口總的來講是比虛基類要更加靈活一點,由於它沒有涉及到類的層面,只跟類中方法綁定,不須要指定其類型。也就是說類型實現了接口中所定義的方法,那麼,則能夠爲外部提供這樣的功能。說得通俗一點就是門口你能夠隨便在哪間房子上開。而虛基類則不具備這樣的能力。咱們用代碼來解釋一下上面所說的。學習

//定義接口
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);
    }
}

症總結

接口 用於提供給外部調用的入口,根據功能領域的不一樣來劃分不一樣的接口。其不與類型綁定,只跟類型中的成員方法相關。方便往後內部的升級改造,不影響對外提供的服務。

虛基類 用於內部封裝類型的共有特徵,因爲虛基類不能直接實例化,所以能夠起到屏蔽子類實現細節的效果。搭配類工廠來實現不一樣業務分派給不一樣的子類來進行處理。

在不少高級語言中二者都有定義(即便沒有也能夠代碼層面去模仿和約定),善用這兩種定義可以使本身的設計變得簡單,結構變得清晰。

相關文章
相關標籤/搜索