【选择恐惧症】接口?虚基类?

症前兆

记得有个朋友跟我讨论过这样的一个问题,说到他刚刚学习接口虚基类的相关知识时觉得很迷茫,不知道什么时候该用接口,什么时候该使用虚基类。后来慢慢地发现接口能做的事情,虚基类也能够实现,甚至有更多的特点。再后来就慢慢地放弃了接口,把所有的设计和实现都采用虚基类来替代。不能说我这个朋友这样的处理有错,但是就我个人对接口和虚基类的理解来说,这样的做法是有不妥的地方。

症分析

所谓的接口简单的来说就是个“门口”,而这个"门口"是安装在某个模块或者服务上,其目的就是为了让外面的世界通过这个“门口”可以访问到模块上的功能或服务。由于是跟外部环境做对接,因此给它定义为--接口。而虚基类则更像一间毛胚房,整个架子已经有了(包括门口),想要什么东西就直接往里面放,但是摆放的东西跟整个架子的设计有关,不是所有的东西都能乱摆,就好像原本规划为洗手间的空间,总不能把床摆在里面吧(当然,你乐意也是可以的。)。

症解答

说到这里,其实已经能够感觉到它们的区别是什么了,表面上虚基类感觉更加强大一点,可以像接口那样声明一系列的方法(这里的方法是没有实现体的,在虚基类中我们把这类方法叫“虚方法”),又能定义一些共有的属性;但是,因为虚基类也是一个类型,是必须要继承与它才能够拥有这样的一些特性,所以这就是它的限制和约束。

接口总的来说是比虚基类要更加灵活一点,因为它没有涉及到类的层面,只跟类中方法绑定,不需要指定其类型。也就是说类型实现了接口中所定义的方法,那么,则可以为外部提供这样的功能。说得通俗一点就是门口你可以随便在哪间房子上开。而虚基类则不具有这样的能力。我们用代码来解释一下上面所说的。

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

症总结

接口 用于提供给外部调用的入口,根据功能领域的不同来划分不同的接口。其不与类型绑定,只跟类型中的成员方法相关。方便日后内部的升级改造,不影响对外提供的服务。

虚基类 用于内部封装类型的共有特征,由于虚基类不能直接实例化,因此可以起到屏蔽子类实现细节的效果。搭配类工厂来实现不同业务分派给不同的子类来进行处理。

在很多高级语言中两者都有定义(即使没有也可以代码层面去模仿和约定),善用这两种定义能够使自己的设计变得简单,结构变得清晰。

其他症状

《【选择恐惧症】需不需要通用设计?》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 226,097评论 6 523
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 97,198评论 3 410
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 173,602评论 0 370
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,750评论 1 304
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,684评论 6 404
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,151评论 1 317
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,349评论 3 433
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 41,430评论 0 282
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,986评论 1 328
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,969评论 3 351
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 42,071评论 1 359
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,667评论 5 352
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,358评论 3 342
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,757评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,944评论 1 278
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,684评论 3 384
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,123评论 2 368

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,688评论 18 399
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,528评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,759评论 18 139
  • 女儿让我带她去练跳绳,我爽快的答应了。晚饭后,带上二宝,我们仨就来到了广场,开始跳绳了,第一次一分钟跳了45个...
    仲蕊蕊妈妈阅读 189评论 0 0
  • 今天是出行的第三天,住在推门就是南湖的卿一位客栈,白天客栈门前小路上人流不息,院里却非常安静!我喜欢他们家宽大的木...
    美丽的小鱼阅读 212评论 0 0