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

癥前兆

記得有個(gè)朋友跟我討論過(guò)這樣的一個(gè)問(wèn)題,說(shuō)到他剛剛學(xué)習(xí)接口虛基類的相關(guān)知識(shí)時(shí)覺(jué)得很迷茫,不知道什么時(shí)候該用接口,什么時(shí)候該使用虛基類。后來(lái)慢慢地發(fā)現(xiàn)接口能做的事情,虛基類也能夠?qū)崿F(xiàn),甚至有更多的特點(diǎn)。再后來(lái)就慢慢地放棄了接口,把所有的設(shè)計(jì)和實(shí)現(xiàn)都采用虛基類來(lái)替代。不能說(shuō)我這個(gè)朋友這樣的處理有錯(cuò),但是就我個(gè)人對(duì)接口和虛基類的理解來(lái)說(shuō),這樣的做法是有不妥的地方。

癥分析

所謂的接口簡(jiǎn)單的來(lái)說(shuō)就是個(gè)“門口”,而這個(gè)"門口"是安裝在某個(gè)模塊或者服務(wù)上,其目的就是為了讓外面的世界通過(guò)這個(gè)“門口”可以訪問(wèn)到模塊上的功能或服務(wù)。由于是跟外部環(huán)境做對(duì)接,因此給它定義為--接口。而虛基類則更像一間毛胚房,整個(gè)架子已經(jīng)有了(包括門口),想要什么東西就直接往里面放,但是擺放的東西跟整個(gè)架子的設(shè)計(jì)有關(guān),不是所有的東西都能亂擺,就好像原本規(guī)劃為洗手間的空間,總不能把床擺在里面吧(當(dāng)然,你樂(lè)意也是可以的。)。

癥解答

說(shuō)到這里,其實(shí)已經(jīng)能夠感覺(jué)到它們的區(qū)別是什么了,表面上虛基類感覺(jué)更加強(qiáng)大一點(diǎn),可以像接口那樣聲明一系列的方法(這里的方法是沒(méi)有實(shí)現(xiàn)體的,在虛基類中我們把這類方法叫“虛方法”),又能定義一些共有的屬性;但是,因?yàn)?strong>虛基類也是一個(gè)類型,是必須要繼承與它才能夠擁有這樣的一些特性,所以這就是它的限制和約束。

接口總的來(lái)說(shuō)是比虛基類要更加靈活一點(diǎn),因?yàn)樗鼪](méi)有涉及到類的層面,只跟類中方法綁定,不需要指定其類型。也就是說(shuō)類型實(shí)現(xiàn)了接口中所定義的方法,那么,則可以為外部提供這樣的功能。說(shuō)得通俗一點(diǎn)就是門口你可以隨便在哪間房子上開。而虛基類則不具有這樣的能力。我們用代碼來(lái)解釋一下上面所說(shuō)的。

//定義接口
interface IAction 
{
    function run();
}

//定義一個(gè)Person類
class Person : IAction
{
    function run()
    {
          print("person run...");
    }
}

//定義一個(gè)Dog類
class Dog : IAction
{
    function run()
    {
        print("dog run...");
    }
}

上面代碼中定義了一個(gè)IAction的接口(一般的高級(jí)編程語(yǔ)言中都用interface這個(gè)詞來(lái)表示接口,在Objective-C中則使用了Protocol一詞來(lái)表示接口,其實(shí)也挺貼切,因?yàn)橐{(diào)用接口的功能就是要按照其指定的協(xié)議來(lái)實(shí)現(xiàn),包括傳什么樣參數(shù),返回什么值),Person和Dog分別實(shí)現(xiàn)了IAction接口,可以看到Person和Dog是兩個(gè)毫無(wú)關(guān)系的類型。

如果換作是虛基類則無(wú)法將這兩種類型關(guān)聯(lián)起來(lái),因?yàn)閷?shí)現(xiàn)的類型必須繼承該虛基類,但是,有一種變通的做法就是對(duì)要關(guān)聯(lián)的類型進(jìn)行更高層次的抽象,那上面的例子來(lái)說(shuō),因?yàn)镻erson和Dog都屬于動(dòng)物,因此我們可以把虛基類定義為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...");
    }
}

通過(guò)這樣的做法確實(shí)是能夠達(dá)到想要的效果, 但是如果你之前已經(jīng)設(shè)計(jì)好了一個(gè)虛基類,對(duì)于后續(xù)需要在設(shè)計(jì)中加入這種不相關(guān)的類型,那么你就需要調(diào)整之前設(shè)計(jì)好的虛基類了,明顯要花費(fèi)額外的時(shí)間去做一些重構(gòu)。

所以,設(shè)計(jì)時(shí)要選擇使用接口還是虛基類?我個(gè)人覺(jué)得虛基類不適合作為提供外部調(diào)用。因?yàn)樗c類型結(jié)構(gòu)綁定,日后如果要進(jìn)行調(diào)整就會(huì)影響對(duì)外行為。但是它可以作為內(nèi)部某些業(yè)務(wù)處理的公共封裝,配合類工廠模式屏蔽類型上的差異。例如寫一個(gè)數(shù)據(jù)存儲(chǔ)服務(wù),它可能是文件存儲(chǔ),也可能是數(shù)據(jù)庫(kù)存儲(chǔ),我們可以進(jìn)行如下定義:

//定義數(shù)據(jù)存儲(chǔ)服務(wù)的虛基類
virtual class DataStoreService
{
    //定義保存數(shù)據(jù)的純虛方法
    virtual function saveData(data : Object) : void;
}

//定義文件數(shù)據(jù)存儲(chǔ)服務(wù)類型
class FileStoreService : DataStoreService
{
    var _file:File;

    function saveData(data : Object) : void
    {
        _file.writeData(data);
        _file.save();
    }
}

//定義數(shù)據(jù)庫(kù)存儲(chǔ)服務(wù)類型
class DatabaseStoreService : DataStoreService
{
    var _db:Database;

    function saveData(data : Object) : void
    {
        _db.insertData(data);
        _db.flush();
    }
}

//定義一個(gè)數(shù)據(jù)存儲(chǔ)類工廠
class DataStoreFactory
{

    //定義數(shù)據(jù)存儲(chǔ)方式
    enum DataStoreType
    {
        File,
        Database
    }
    
    //獲取數(shù)據(jù)存儲(chǔ)服務(wù)方法
    function getDataStoreService(type : DataStoreType) : DataStoreService
    {
        switch (type)
        {
            case File:
                return new FileStoreService();
            case Database:
                return new DatabaseStoreService();
        }
    }
}

如上述代碼所示(上面寫的都是偽代碼,只用于說(shuō)明意圖),只要使用DataStoreFactory然后根據(jù)自己需要的存儲(chǔ)類型就能獲取到不同的存儲(chǔ)服務(wù),而返回的類型是定義的虛基類DataStoreService,這樣就能夠很好地屏蔽FileStoreService和DatabaseStoreService中的一些設(shè)計(jì)細(xì)節(jié),因?yàn)閷?duì)于調(diào)用的人來(lái)說(shuō)這些都可以是透明的。

接口正是我們需要對(duì)外提供功能的一個(gè)比較好的方案。一來(lái)它不跟類型掛鉤,二來(lái)又能像虛基類中的純虛函一樣可以屏蔽內(nèi)部實(shí)現(xiàn),對(duì)調(diào)用者透明不需要他理解里面的實(shí)現(xiàn)原理,只管調(diào)用和取得結(jié)果。第三個(gè)就是對(duì)于日后內(nèi)部設(shè)計(jì)的升級(jí)改造時(shí),無(wú)需改變接口的定義,只要把內(nèi)部實(shí)現(xiàn)進(jìn)行調(diào)整即可。我們來(lái)舉個(gè)例子,假如之前我們一直使用文件作為主要的存儲(chǔ)方式,那么使用接口來(lái)實(shí)現(xiàn),可以類似如下代碼:

//定義數(shù)據(jù)存儲(chǔ)服務(wù)接口
interface IDataStoreService
{
    function saveData(data : Object) : void;
}

//定義文件存儲(chǔ)服務(wù),該類型不對(duì)外公開
class FileStoreService : IDataStoreService
{
    var _file : File;
    
    function saveData(data : Object) : void
    {
        _file.writeData(data);
        _file.save();
    }
}

//對(duì)外公開的Api類型
class Api 
{
    function getDataStoreSerivce( ) : IDataStoreService
    {
        return new FileStoreService( );
    }
}

值得注意的是,我們?cè)谠O(shè)計(jì)時(shí)必須是要有一個(gè)對(duì)外公開的類,否則無(wú)法讓外部可以訪問(wèn)到內(nèi)部所提供的接口,上面代碼提供公開類就是Api類型。從代碼上來(lái)看我們的Api類型的getDataStoreService方法只返回了一個(gè)IDataStoreService的接口,并不涉及到FileStoreService。所以,當(dāng)我們?cè)谶M(jìn)行改造時(shí),可以直接把文件存儲(chǔ)改為數(shù)據(jù)庫(kù)存儲(chǔ),也不會(huì)對(duì)外部調(diào)用造成任何影響,如下面代碼變更:

//定義數(shù)據(jù)存儲(chǔ)服務(wù)接口
interface IDataStoreService
{
    function saveData(data : Object) : void;
}

//定義數(shù)據(jù)庫(kù)存儲(chǔ)服務(wù)類型
class DatabaseStoreService : IDataStoreService
{
    var _db:Database;

    function saveData(data : Object) : void
    {
        _db.insertData(data);
        _db.flush();
    }
}

//對(duì)外公開的Api類型
class Api 
{
    function getDataStoreSerivce( ) : IDataStoreService
    {
        return new DatabaseStoreService( );
    }
}

回到最初我朋友的那個(gè)問(wèn)題,其實(shí)要使用虛基類還是接口來(lái)實(shí)現(xiàn)功能,這兩者其實(shí)是沒(méi)有任何沖突的,最好是兩者結(jié)合使用,虛基類作為內(nèi)部封裝的公共元素而存在,可以根據(jù)領(lǐng)域的不同劃分多個(gè)不同的虛基類,而在虛基類中定義的某項(xiàng)功能需要暴露給外界調(diào)用時(shí),則可以使用接口來(lái)定義,同樣根據(jù)不同的領(lǐng)域可以劃分多個(gè)不同的接口。還是根據(jù)上面的例子,我們把虛基類接口相結(jié)合,形成一個(gè)完整的數(shù)據(jù)存儲(chǔ)服務(wù)模塊:

//定義數(shù)據(jù)存儲(chǔ)服務(wù)接口
interface IDataStoreService
{
    function saveData(data : Object) : void;
}

//定義數(shù)據(jù)存儲(chǔ)服務(wù)的虛基類
virtual class DataStoreService : IDataStoreService
{
    //實(shí)現(xiàn)接口方法
    function saveData(data : Object) : void
    {
        //由于實(shí)現(xiàn)接口的類型不允許不實(shí)現(xiàn)接口方法,
        //因此這里保留一個(gè)空實(shí)現(xiàn)方法,等待它的子類重寫該方法。
    }
}

//定義文件數(shù)據(jù)存儲(chǔ)服務(wù)類型
class FileStoreService : DataStoreService
{
    var _file:File;

    function saveData(data : Object) : void
    {
        _file.writeData(data);
        _file.save();
    }
}

//定義數(shù)據(jù)庫(kù)存儲(chǔ)服務(wù)類型
class DatabaseStoreService : DataStoreService
{
    var _db:Database;

    function saveData(data : Object) : void
    {
        _db.insertData(data);
        _db.flush();
    }
}

//定義一個(gè)數(shù)據(jù)存儲(chǔ)類工廠
class DataStoreFactory
{

    //定義數(shù)據(jù)存儲(chǔ)方式
    enum DataStoreType
    {
        File,
        Database
    }
    
    //獲取數(shù)據(jù)存儲(chǔ)服務(wù)方法
    function getDataStoreService(type : DataStoreType) : DataStoreService
    {
        switch (type)
        {
            case File:
                return new FileStoreService();
            case Database:
                return new DatabaseStoreService();
        }
    }
}

//對(duì)外公開的Api類型
class Api 
{
    function getDataStoreSerivce( ) : IDataStoreService
    {
        return DataStoreFactory.getDataStoreService(DataStoreType.Database);
    }
}

癥總結(jié)

接口 用于提供給外部調(diào)用的入口,根據(jù)功能領(lǐng)域的不同來(lái)劃分不同的接口。其不與類型綁定,只跟類型中的成員方法相關(guān)。方便日后內(nèi)部的升級(jí)改造,不影響對(duì)外提供的服務(wù)。

虛基類 用于內(nèi)部封裝類型的共有特征,由于虛基類不能直接實(shí)例化,因此可以起到屏蔽子類實(shí)現(xiàn)細(xì)節(jié)的效果。搭配類工廠來(lái)實(shí)現(xiàn)不同業(yè)務(wù)分派給不同的子類來(lái)進(jìn)行處理。

在很多高級(jí)語(yǔ)言中兩者都有定義(即使沒(méi)有也可以代碼層面去模仿和約定),善用這兩種定義能夠使自己的設(shè)計(jì)變得簡(jiǎn)單,結(jié)構(gòu)變得清晰。

其他癥狀

《【選擇恐懼癥】需不需要通用設(shè)計(jì)?》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1. Java基礎(chǔ)部分 基礎(chǔ)部分的順序:基本語(yǔ)法,類相關(guān)的語(yǔ)法,內(nèi)部類的語(yǔ)法,繼承相關(guān)的語(yǔ)法,異常的語(yǔ)法,線程的語(yǔ)...
    子非魚_t_閱讀 34,696評(píng)論 18 399
  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 179,036評(píng)論 25 709
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,554評(píng)論 19 139
  • 女兒讓我?guī)ゾ毺K,我爽快的答應(yīng)了。晚飯后,帶上二寶,我們仨就來(lái)到了廣場(chǎng),開始跳繩了,第一次一分鐘跳了45個(gè)...
    仲蕊蕊媽媽閱讀 237評(píng)論 0 0
  • 今天是出行的第三天,住在推門就是南湖的卿一位客棧,白天客棧門前小路上人流不息,院里卻非常安靜!我喜歡他們家寬大的木...
    美麗的小魚閱讀 244評(píng)論 0 0

友情鏈接更多精彩內(nèi)容