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

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

症前兆

記得有個朋友跟我討論過這樣的一個問題,說到他剛剛學習接口虛基類的相關知識時覺得很迷茫,不知道什麼時候該用接口,什麼時候該使用虛基類。後來慢慢地發現接口能做的事情,虛基類也能夠實現,甚至有更多的特點。再後來就慢慢地放棄了接口,把所有的設計和實現都採用虛基類來替代。不能說我這個朋友這樣的處理有錯,但是就我個人對接口和虛基類的理解來說,這樣的做法是有不妥的地方。

症分析

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

症解答

說到這裏,其實已經能夠感覺到它們的區別是什麼了,表面上虛基類感覺更加強大一點,可以像接口那樣聲明一系列的方法(這裏的方法是沒有實現體的,在虛基類中我們把這類方法叫“虛方法”),又能定義一些共有的屬性;但是,因爲虛基類也是一個類型,是必須要繼承與它才能夠擁有這樣的一些特性,所以這就是它的限制和約束。

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

//定義接口
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是兩個毫無關係的類型。

如果換作是虛基類則無法將這兩種類型關聯起來,因爲實現的類型必須繼承該虛基類,但是,有一種變通的做法就是對要關聯的類型進行更高層次的抽象,那上面的例子來說,因爲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...");
    }
}

通過這樣的做法確實是能夠達到想要的效果, 但是如果你之前已經設計好了一個虛基類,對於後續需要在設計中加入這種不相關的類型,那麼你就需要調整之前設計好的虛基類了,明顯要花費額外的時間去做一些重構。

所以,設計時要選擇使用接口還是虛基類?我個人覺得虛基類不適合作爲提供外部調用。因爲他與類型結構綁定,日後如果要進行調整就會影響對外行爲。但是它可以作爲內部某些業務處理的公共封裝,配合類工廠模式屏蔽類型上的差異。例如寫一個數據存儲服務,它可能是文件存儲,也可能是數據庫存儲,我們可以進行如下定義:

//定義數據存儲服務的虛基類
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中的一些設計細節,因爲對於調用的人來說這些都可以是透明的。

接口正是我們需要對外提供功能的一個比較好的方案。一來它不跟類型掛鉤,二來又能像虛基類中的純虛函一樣可以屏蔽內部實現,對調用者透明不需要他理解裏面的實現原理,只管調用和取得結果。第三個就是對於日後內部設計的升級改造時,無需改變接口的定義,只要把內部實現進行調整即可。我們來舉個例子,假如之前我們一直使用文件作爲主要的存儲方式,那麼使用接口來實現,可以類似如下代碼:

//定義數據存儲服務接口
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);
    }
}

症總結

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

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

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

#其他症狀

《【選擇恐懼症】需不需要通用設計?》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章