癥前兆
記得有個(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)變得清晰。