C++ 隱性介面與顯性介面

介面(Interface),我稱之為一種 行為合約,代表是若是你的物件持有這種合約,那麼我對此物件,操作了合約上的任何動作,都不會有問題。是一個讓 caller 與 callee 做為溝通的工具。一般所說的介面,都是指物件導向(OO)上的顯性介面,這篇文章就是要來簡單描述一下,另一種隱性介面是什麼。

顯性介面

以往的顯性介面使用上,你的每個遵守合約物件,一定要 繼承/實作 了某個 Interface Class, Compiler 對於合約檢查,早在物件的型別是否符合就決定了。

隱性介面

C++ 的 template ,使得隱性介面成為可能,它能夠使得 行為合約的粒度,從 型別 降成更低的 能夠完成函式呼叫。使用了隱性介面,操作的物件不需要是某某 Interface Class 的子類別,不需要是某某特定型別,它的合約條件是只要 有用到的函式呼叫,都要能夠支援就好,就算你的函式簽名不一樣也可以,它只要求其 行為 是否能夠執行。Compiler 對於此種合約檢查,則是延遲到函式呼叫的那一行,才去做判斷。以下我們就看 Code,來表示隱性介面的不同之處:

下面有定義了兩個 Class ,分別是 CatDog,他們都 繼承/遵守 了一個 CryOutAble 型別/合約。還有另一個 Class 叫 Alien,完全獨立於 CatDog ,相同之處單單只有都能夠呼叫一個 cryOut(); 並且回傳某個東西。

遵守顯性介面的一族 Classes
class CryOutAble {public: 
    virtual int cryOut() = 0;
};
class Cat: public CryOutAble {public:
    virtual int cryOut() {cout << "MEOW! MEOW!" << endl; 
                          return 100;}
};
class Dog: public CryOutAble{public:
    virtual int cryOut() {cout << "GULF! GULF!" << endl; 
                          return 200;} 
};
一個 Class,它遵守一個[能夠呼叫 cryOut() 並且回傳某個東西]的隱性介面
class Alien {public:
    const char* cryOut() {cout << "ALIEN NEEDS LOVE!" << endl; 
                          return "REALLY!";}
};

操作顯式介面

在使用顯性介面的操作上,就算你只是想呼叫 cryOut() 僅此而己,但是只要你傳入的物件,不是繼承此 Interface Class ,就會無法通過編譯。

void ExplicitlyInterface(const CryOutAble& speaker)
{
    speaker.cryOut();
}
Cat cat;
Dog dog;
Alien alien;
ExplicitlyInterface(cat);  // OK! cat 是 CryOutAble 的子類別,印出 MEOW! MEOW!
ExplicitlyInterface(dog);  // OK! dog 是 CryOutAble 的子類別,印出 GULF! GULF!
ExplicitlyInterface(alien); // Compile Error! alien 沒有繼承 CryOutAble

操作隱式介面

我們現在來看使用隱性介面的操作,它對於物件的要求,只要能夠支援 呼叫cryOut() 並且能夠回傳某個東西 就好,只有無法支援此項要求的物件,才會無法通過編譯。

一個遵守 **能夠呼叫 cryOut() 並且回傳某值** 的隱性介面
template <typename T>
void ImplicitlyInterface(const T& speaker)
{
      // auto 會去由 compiler 推導回傳值的型別是什麼,所以此行的要求是只要能夠回傳某值就好。
    auto i = speaker.cryOut(); 
    cout << typeid(i).name() << endl;
}
2 個無法滿足隱性介面的 Class
// 擁有 cryOut() 但是無法回傳某值
class Bear {public:
    void cryOut() {cout << "a hee-ahee ha-hee" << endl; }
};
// 根本沒有 cryOut
class Fox {public:
    void possibleCryOut() {cout << "a hee-ahee ha-hee" << endl; }
}
Cat cat;
Dog dog;
Alien alien;
Bear bear;
Fox fox;

ImplicitlyInterface(cat); Compile OK! cat 可以 cryOut 並傳回某個東西(int)
ImplicitlyInterface(dog); Compile OK! dog 可以 cryOut 並傳回某個東西(int)
ImplicitlyInterface(alien); Compile OK! alien 可以 cryOut 並傳回某個東西(const char*)
ImplicitlyInterface(bear); Compiler Error! bear 雖然可以 cryOut,不能夠回傳某個東西(void)
ImplicitlyInterface(fox); Compiler Error! fox 不能夠 cryOut

STL 實際應用

STL 中的 container (vector, deque, map, etc) 跟 string ,都沒有相同的 Base Class ,但是它們都能呼叫 size()empty(),另外也能夠經由 begin()end() 來得到一組 Iterator 來使用於迴圈;各種型別的專屬 Iterator 與 C++ 的指標更是完全不一樣,但是只因為它們都能夠支援 i++ 和 *i 等等行為,就可以相容於 STL Algorithm 的各項操作。

結論

使用隱性介面,可以把不同族的 Class 結合在一起, 使得各 低藕合性 的 Classes 能夠運作良好;使用 template 所以 不失型別安全不造成效能降低,因為合約檢查是在 Compile-time 完成; 操作更靈活,因為合約保證的粒度從 Interface Class 中所有的 函式都要有,且都要符合函式 signature 分割成更小的 能夠滿足函式呼叫的行為 即可, Andrei Alexandrescu 在 Modern C++ Design 的 Policy-based Class Design 更是以此為基石。

其他

若以多型(polymorphism)的角度來看,顯式介面使用得是 dynamic polymorphism,在 runtime 的時候,依其 dynamic type 使用 vtable 或 vptr 決議其行為(method)。隱式介面則是在 compile time 時,具現化不同的 class,其行為(method)則是在 compile time 即決議完成,所以是一種 static polymorphism。在少了 runtime 的 dispatch 動作,效率上來說比較快的。

參考資料:

Effective C++ Rule41 - Understand implicit interfaces and compile-time polymorphism
Modern C++ Design Chapter - Policy Based Class Design

comments powered by Disqus