记得有个朋友跟我讨论过这样的一个问题,说到他刚刚学习接口和虚基类的相关知识时以为很迷茫,不知道何时该用接口,何时该使用虚基类。后来慢慢地发现接口能作的事情,虚基类也可以实现,甚至有更多的特色。再后来就慢慢地放弃了接口,把全部的设计和实现都采用虚基类来替代。不能说我这个朋友这样的处理有错,可是就我我的对接口和虚基类的理解来讲,这样的作法是有不妥的地方。数据库
所谓的接口简单的来讲就是个“门口”,而这个"门口"是安装在某个模块或者服务上,其目的就是为了让外面的世界经过这个“门口”能够访问到模块上的功能或服务。因为是跟外部环境作对接,所以给它定义为--接口。而虚基类则更像一间毛胚房,整个架子已经有了(包括门口),想要什么东西就直接往里面放,可是摆放的东西跟整个架子的设计有关,不是全部的东西都能乱摆,就好像本来规划为洗手间的空间,总不能把床摆在里面吧(固然,你乐意也是能够的。)。编程
说到这里,其实已经可以感受到它们的区别是什么了,表面上虚基类感受更增强大一点,能够像接口那样声明一系列的方法(这里的方法是没有实现体的,在虚基类中咱们把这类方法叫“虚方法”),又能定义一些共有的属性;可是,由于虚基类也是一个类型,是必需要继承与它才可以拥有这样的一些特性,因此这就是它的限制和约束。编程语言
而接口总的来讲是比虚基类要更加灵活一点,由于它没有涉及到类的层面,只跟类中方法绑定,不须要指定其类型。也就是说类型实现了接口中所定义的方法,那么,则能够为外部提供这样的功能。说得通俗一点就是门口你能够随便在哪间房子上开。而虚基类则不具备这样的能力。咱们用代码来解释一下上面所说的。学习
//定义接口 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); } }
接口 用于提供给外部调用的入口,根据功能领域的不一样来划分不一样的接口。其不与类型绑定,只跟类型中的成员方法相关。方便往后内部的升级改造,不影响对外提供的服务。
虚基类 用于内部封装类型的共有特征,因为虚基类不能直接实例化,所以能够起到屏蔽子类实现细节的效果。搭配类工厂来实现不一样业务分派给不一样的子类来进行处理。
在不少高级语言中二者都有定义(即便没有也能够代码层面去模仿和约定),善用这两种定义可以使本身的设计变得简单,结构变得清晰。