Guru of the Week 條款30附錄:介面原則 (轉)
(至此,GotW1~30即《Exceptional C++》的原型,補全。)
Herb Sutter在March 1998於C++ Report上發表的文章《What's In a Class - The Interface Principle》,屬《Exceptional C++》的Item 32~34。
介面原則(the Interface Principle):namespace prefix = o ns = "urn:schemas--com::office" />
類裡面是什麼?-介面原則
This article appeared in C++ Report, 10(3), March 1998.
我開始於一個簡單而令人困惑的問題:
l 類裡面是什麼?也就是,什麼組成了類及其介面?
更深層的問題是:
l 它怎樣和C風格的面向聯絡起來?
l 它怎樣和C++的Koenig lookup聯絡起來?和稱為Myers Example的例子又有什麼聯絡?(我都會討論。)
l 它怎樣影響我們分析class間的依賴關係以及設計物件模型?
那麼,“class裡面是什麼?”
首先,回顧一下一個class的傳統定義:
Class(定義)
一個class描述了一組資料及操作這些資料的。
員們常常無意中曲解了這個定義,改為:“噢,一個class,也就是出現在其定義體中的東西--成員資料和成員函式。”於是事情變味了,因為它限定“函式”為只是“成員函式”。看這個:
//*** Example 1 (a)
class X { /*...*/ };
/*...*/
void f( const X& );
問題是:f是X的一部分嗎?一些人會立即答“No”,因為f是一個非成員函式(或“自由函式”)。另外一些人則認識到關鍵性的東西:如果Example 1 (a)的程式碼出現在一個頭中,也就是:
//*** Example 1 (b)
class X { /*...*/
public:
void f() const;
};
再考慮一下。不考慮訪問〖注1〗,f還是相同的,接受一個指向X的指標或引用。只不過現在這個引數是隱含的。於是,如果Example 1 (a)全部出現在同一標頭檔案中,我們將發現,雖然f不是X的成員函式,但強烈地依賴於X。我將在下一節展示這個關聯關係的真正含義。
換種方式,如果X和f不出現在同一標頭檔案中,那麼f只不過是個早已存在的函式,而不是X的一部分(即使f需要一個X型別的實參)。我們只是例行公事般寫了一個函式,其形參型別來自於庫函式標頭檔案,很明確,我們自己的函式不是庫函式中的類的一部分。
The Interface Principle
介面原則(Interface Principle)
依靠那個例子,我將提出介面原則:
介面原則
對於一個類X,所有的函式,包括自由函式,只要同時滿足
(a) “提到”X,並且
(b) 與X“同期提供”
那麼就是X的邏輯組成部分,因為它們形成了X的介面。
根據定義,所有成員函式都是X的“組成部分”:
(a) 每個成員函式都必須“提到”X(非static的成員函式有一個隱含引數,型別是X*(WQ:C++標準制定後,精確地說,應該是X* const)或const X*(WQ:const X* const);static的成員函式也是在X的作用範圍內的);並且
(b) 每個成員函式都與X“同期提供”(在X的定義體中)。
用介面原則來分析Example 1 (a),其結論和我們最初的看法相同:很明確,f“提到”X。如果f也與X“同期提供”(例如,它們存在於相同的標頭檔案和/或名稱空間中〖注2〗),根據介面原則,f是X的邏輯組成部分,因為它是X的介面的一部分。
介面原則在判斷什麼是一個class的組成部分時,是個有用的試金石。把自由函式判定為一個class的組成部分,這違反直覺嗎?那麼,給這個例子再加些砝碼,將f改得更常見些:
//*** Example 1 (c)
class X { /*...*/ };
/*...*/
ostream& operator<
現在,介面原則的基本理念就很清楚了,因為我們理解這個特別的自由函式是怎麼回事:如果operator<
據此,讓我們回到class的傳統定義上:
Class(定義)
一個class描述了一組資料及操作這些資料的函式。
這個定義非常正確,因為它沒有說到“函式”是否為“成員”。
這個介面原則是OO的原則呢,還是隻是C++特有的原則?
我用了C++的術語如“namespace”來描述“與...同期提供”的含義的,所以,介面原則是C++特有的?還是它是OO的通行原則,並適用於其它語言?
考慮一下這個很類似的例子,從另一個語言(實際上,是個非OO的語言):C。
/*** Example 2 (a) ***/
struct _iobuf { /*...data goes here...*/ };
typedef struct _iobuf FILE;
FILE* fopen ( const char* filename,
const char* mode );
int fclose( FILE* stream );
int fseek ( FILE* stream,
long offset,
int origin );
long ftell ( FILE* stream );
/* etc. */
這是在沒有class的語言中實現OO程式碼的典型“處理技巧”:提供一個結構來包容物件的資料,提供函式--肯定是非成員的--接受或返回指向這個結構的指標。這些自由函式構造(fopen),析構(fclose)和操作(fseek、ftell等等……)這些資料。
這個技巧是有缺點的(例如,它依賴於使用者能忍住不直接胡亂運算元據),但它“確實”是OO的程式碼--總之,一個class是一組資料及操作這些資料的函式。雖然這些函式都是非成員的,但它們仍然是FILE的介面的一部分。
現在,考慮一下怎樣將Example 2 (a)用一個有class的語言重寫:
//*** Example 2 (b)
class FILE {
public:
FILE( const char* filename,
const char* mode );
~FILE();
int fseek( long offset, int origin );
long ftell();
/* etc. */
private:
/*...data goes here...*/
};
那個FILE*的引數變成為隱含引數。現在就明確了,fseek是FILE的一部分,和Example 2 (a)中它還不是成員函式時一樣。我們甚至可以將函式實現為一部分是成員函式,一部分是非成員函式:
//*** Example 2 (c)
class FILE {
public:
FILE( const char* filename,
const char* mode );
~FILE();
long ftell();
/* etc. */
private:
/*...data goes here...*/
};
int fseek( FILE* stream,
long offset,
int origin );
很清楚,是不是成員函式並不是問題的關鍵。只要它們“提及”了FILE並且與FILE“同期提供”,它們就是FILE的組成部分。在Example 2 (a)中,所有的函式都是非成員函式,因為在C語言中,它們只能如此。即使在C++中,一個class的介面函式中,有些必須(或應該)是非成員函式:operator<
介紹Koenig Lookup
介面原則還有更深遠的意義,如果你認識到它和Koenig Lookup幹了相同的事的話〖注4〗。此處,我將用兩個例子來解說和定義Koenig Lookup。下一節,我將用Myers Example來展示它為何與介面原則有直接聯絡。
這是我們需要Koenig lookup的原因,來自於C++標準中的例子:
//*** Example 3 (a)
namespace NS {
class T { };
void f(T);
}
NS::T parm;
int main() {
f(parm); // OK: calls NS::f
}
很漂亮,不是嗎?“明顯”,程式設計師不需要明確地寫為NS::f(parm),因為f(parm)“明確”意味著NS::f(parm),對吧?但,對我們顯而易見的東西對來說並不總是顯而易見,尤其是考慮到連個將名稱f帶入程式碼空間的“using”都沒用。Koenig lookup讓編譯器得以完成正確的事情。
它是這麼工作的:所謂“名稱搜尋”就是當你寫下一個“f(parm)”時,編譯器必須要決策出你想調哪個叫f的函式。(由於過載和作用域的原因,可能會有幾個叫f的函式。)Koenig lookup是這麼說的,如果你傳給函式一個class型別的實參(此處是parm,型別為NS::T),為了查詢這個函式名,編譯器被要求不僅要搜尋如區域性作用域這樣的常規空間,還要搜尋包含實參型別的名稱空間(此處是NS)〖注5〗。於是,Example 3 (a)中是這樣的:傳給f的引數型別為T,T定義於namespace NS,編譯器要考慮namespace NS中的f--不要大驚小怪了。
不用明確限定f是件好事,因為這樣我們就很容易限定函式名了:
//*** Example 3 (b)
#include
#include
// declares the free function
// std::operator<< for strings
int main() {
std::string hello = "Hello, world";
std::cout << hello; // OK: calls
} // std::operator<<
這種情況下,沒有Koenig lookup的話,編譯器將無法找到operator<
總結:如果你在同一名稱空間中提供一個class和一個“提及”此class的自由函式〖注6〗,編譯器在兩者間建立一個強關聯〖注7〗。再讓我們回到介面原則上,考慮這個Myers Example:
更多的Koenig Lookup:Myers Example
考慮第一個(略為)簡化的例子:
//*** Example 4 (a)
namespace NS { // typically from some
class T { }; // header T.h
}
void f( NS::T );
int main() {
NS::T parm;
f(parm); // OK: calls global f
}
Namespace NS提供了一個型別T,而在它外面提供了一個全域性函式f,碰巧此函式接受一個T的引數。很好,是蔚藍的,世界充滿和平,一切都很美好。
時間在流逝。有一天,NS的作者基於需要增加了一個函式:
//*** Example 4 (b)
namespace NS { // typically from some
class T { }; // header T.h
void f( T ); //
}
void f( NS::T );
int main() {
NS::T parm;
f(parm); // ambiguous: NS::f
} // or global f?
在名稱空間中增加一個函式的行為“破壞”了名稱空間外面的程式碼,即使使用者程式碼沒有用“using”將NS中的名稱帶到它自己的作用域中!但等一下,事情是變好了--Nathan Myers〖注8〗指出了在名稱空間與Koenig lookup之間的有趣行為:
//*** The Myers Example: "Before"
namespace A {
class X { };
}
namespace B {
void f( A::X );
void g( A::X pa) {
f(parm); // OK: calls B::f
}
}
很好,天很藍……。一天,A的作者基於需要增加了另外一個函式:
//*** The Myers Example: "After"
namespace A {
class X { };
void f( X ); //
}
namespace B {
void f( A::X );
void g( A::X parm ) {
f(parm); // ambiguous: A::f or B::f?
}
}
“啊?”你可能會問。“名稱空間賣點就是防止名字衝突,不是嗎?但,在一個名稱空間中增加函式卻看起來造成'破壞'了另一個完全隔離的名稱空間中的程式碼。”是的,namespace B中的程式碼被破壞了,只不過是因為它“提及”了來自於namespace A中的一個型別。B中的程式碼沒有在任何地方寫出“using namespace; A”,也沒有寫出“using A::X;”。
這不是問題,B沒有被“破壞”。事實上,這是應該發生的正確行為〖注9〗。如果在X所處的名稱空間中有應該函式f(X),那麼,根據介面原則,f是X的介面的一部分。f是一個自由函式根本不是關鍵;想確認它仍然是X的在邏輯組成部分,只要給它另外一個名字:
//*** Restating the Myers Example: "After"
namespace A {
class X { };
ostream& operator<
}
namespace B {
ostream& operator<
void g( A::X parm ) {
cout << parm; // ambiguous:
} // A::operator<< or
} // B::operator<
如果使用者程式碼提供了一個“提及”X的函式,而它與X所處的名稱空間提供的某個函式簽名重合時,呼叫將有二義性。B必須明確表明它想呼叫哪個函式,它自己的還是與X“同期提供”的。這正是我們期望介面原則應該提供的東西:
介面原則
對於一個類X,所有的函式,包括自由函式,只要同時滿足
(a) “提到”X,並且
(b) 與X“同期提供”
就是X的邏輯組成部分,因為它們組成了X的介面。
簡而言之,介面原則與Koenig lookup的行為相同並不是意外。Koenig lookup的行為正是建立在介面原則的基礎上的。
(下面“關聯有多強?”這節展示了為什麼成員函式class之間仍然有比非成員函式更強的關聯關係)
“組成部分”的關聯有多強?
雖然介面原則說明成員和非成員函式都是class的邏輯“組成部分”,但它並沒說成員和非成員是平等的。例如,成員函式自動得到class內部的全部訪問許可權,而非成員函式只有在它們被申明為友元時才能得到相同的許可權。同樣,在名稱搜尋(包括Koenig lookup)中,C++語言特意表明成員函式與class之間有比非成員函式更強的關聯關係:
//*** NOT the Myers Example
namespace A {
class X { };
void f( X );
}
class B {
// class, not namespace
void f( A::X );
void g( A::X parm ) {
f(parm); // OK: B::f,
// not ambiguous
}
};
我們現在討論的是class B而不是namespace B,所以這沒有二義:當編譯器找到一個叫f的成員函式時,它不會自找麻煩地使用Koenig lookup來搜尋自由函式。
所以,在兩個主要問題上--訪問許可權規則和名稱搜尋規則--即使根據介面原則,當一個函式是一個class的組成部分時,成員函式與class之間有比非成員函式更強的關聯關係。
一個class依賴於什麼?
“class裡面是什麼”並不只是一個哲理問題。它是一個根基性問題,因為,沒有正確的答案的話,我們就不能恰當地分析class的依賴關係。
為了證明這一點,看一下這個似乎不相關的問題:怎麼實現一個class的operator<
第一種:
//*** Example 5 (a) -- nonvirtual streaming
class X {
/*...ostream is never mentioned here...*/
};
ostream& operator<
/* code to output an X to a stream */
return o;
}
這是第二個:
//*** Example 5 (b) -- virtual streaming
class X { /*...*/
public:
virtual ostream& print( ostream& o ) {
/* code to output an X to a stream */
return o;
}
};
ostream& operator<
return x.print();
}
假設兩種情況下,class和函式都出現在相同的標頭檔案和/或名稱空間中。你選擇哪一個?取捨是什麼?歷來,C++的資深程式設計師用這種方式分析了這些選則:
l 選擇(a)的好處是X有更低的依賴性。因為X沒有成員函式“提及”了“流(ostream)”,X(看起來)不依賴於流。選擇(a)同樣也避免了一個額外的虛擬函式呼叫的開銷。
l 選擇(b)的好處是所有X的派生類都能正確print,即使傳給operator<
這個分析是有瑕疵的。運用介面原則,我們能發現為什麼--選擇(a)的好處是假象,因為:
l 根據介面原則,只要operator<
l 兩種情況下,operator<
l 因為兩種情況下operator<
所以,我們素來認為的選擇(a)的主要好處根本就不存在--兩種情況下X都依賴於流!如果(通常也如此)operator<
隨著第一大好處的幻像的破滅,選擇(a)就只剩下沒有虛擬函式呼叫開銷的好處了。不借助於介面原則的話,我們就沒法如此容易地在這個很常見的例子上分析依賴性上的真象(以及事實上的取捨)。
底線:區分成員還是非成員沒有太大的意義(尤其是在分析依賴性時),而這正是介面原則所要闡述的。
一些有趣(甚至是詫異)的結果
通常,如果A和B都是clss,並且f(A,B)是一個自由函式:
l 如果A與f同期提供,那麼f是A的組成部分,並且A將依賴B。
l 如果B與f同期提供,那麼f是B的組成部分,並且B將依賴A。
l 如果A、B、f都是同期提供的,那麼f同時是A和B的組成部分,並且A與B是迴圈依賴。這具有根本性的意義--如果一個庫的作者提供了兩個class及同時涉及二者的操作,那麼這個操作恐怕被規定為必須同步使用。現在,介面原則對這個迴圈依賴給出了一個嚴格的證明。
最後,我們到了一個真的很有趣的狀態。通常,如果A和B都是class,並且A::g(B)是A的一個成員函式:
l 因為A::g(B)的存在,很明顯,A總是依賴B。沒有疑義。
l 如果A和B是同期提供的,那麼A::g(B)與B當然也是同期提供的。於是,因為A::g(B)同時滿足“提及”B和與B“同期提供”,根據介面原則(恐怕有些詫異):A::g(B)是B的組成部分,而又因為A::g(B)使用了一個(隱含的)A*引數,所以B依賴A。因為A也依賴B,這意味著A和B迴圈依賴。
首先,它看起來只是“將一個class的成員函式判定為也是另一個class的組成部分”的推論,但這隻在A和B是同期提供時才成立。再想一下:如果A和B是同期提供的(也就是說,在同一標頭檔案中),並且A在一個成員函式中提及了B,“直覺”也告訴我們A和B恐怕是迴圈依賴的。它們之間肯定是強耦合的,它們同期提供和互相作用的事實意味著:(a)它們應該同步使用,(b)更改一個也會影響另一個。
問題是:在此以前,除了“直覺”很難用實物證明A與B間的迴圈依賴。現在,這個迴圈依賴可以用介面原則的來推導證明了。
注意:與class不同,namespace不需要一次申明完畢,這個“同期提供”取決於namespace當前的可見部分:
//*** Example 6 (a)
//---file a.h---
namespace N { class B; } // forward decl
namespace N { class A; } // forward decl
class N::A { public: void g(B); };
//---file b.h---
namespace N { class B { /*...*/ }; }
A的使用者包含了a.h,於是,A和B是同期提供的並是迴圈依賴的。B的使用者包含了b.h,於是A和B不是同期提供的。
總結
我希望你得到3個想法:
l 介面原則:對於class X,所有的函式,包括自由函式,只要同時滿足(a)“提及”X,(b)與X“同期提供”,那麼它就是X的邏輯組成部分,因為它們是X的介面的一部分。
l 因此,成員和非成員函式都是一個class的邏輯組成部分。只不過成員函式比非成員函式有更強的關聯關係。
l 在介面原則中,對“同期提供”的最有用的解釋是“出現在相同的標頭檔案和/或名稱空間中”。如果函式與class出現在相同的標頭檔案中,在依賴性分析時,它是此class的組成部分。如果函式與類出現在相同的名稱空間中,在物件引用和名稱搜尋時,它是此class的組成部分。
注1.即使最初f是一個友元,情況還是這樣的。
注2.我們在本文後面的篇幅中將詳細討論名稱空間之間的關聯關係,因為它指出了介面原則實際上和Koenig lookup的行為是一致的。
注3.成員和非成員函式間的相似性,在另外一些可過載的運算子上表現得甚至更強烈。例如,當你寫下“a + b”時,你可以呼叫a.operator+(b)也可以是operator+(a,b),這取決於a和b的型別。
注4.以Andrew Koenig命名的,因為最初由他寫出了這個定義,他是AT&T's C++ team 和C++ standards committee的長期會員。參見由 A. Koenig 和B. Moo寫的《Ruminations on C++》(Addison-Wesley, 1997)。
注5.還有其它一些細節,但本質就是這個。
注6.透過傳值、傳引用、傳指標,或其它方式。
注7.要承認,其關聯關係要比class與其成員間的關聯關係要弱那麼一點點。後文有“關聯有多強”這麼一節。
注8.Nathan也是C++ standards committee的長期會員,並且是標準中的locale facility的主要作者。
注9.這個特別的例子出現在November 1997的Morristown會議上,它激起我思考成員關係和依賴關係。Myers Example的意思很簡單:名稱空間不象人們通常想象的那樣密不透風,但它們在隔離性上已足夠完美並恰好滿足它們本職工作。來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-992751/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Guru of the Week 條款19:自動轉換 (轉)
- Guru of the Week 條款27:轉呼叫函式 (轉)函式
- Guru of the Week 條款28:“Fast Pimpl”技術 (轉)AST
- Guru of the Week 條款09:記憶體管理(上篇) (轉)記憶體
- Guru of the Week 條款10:記憶體管理(下篇) (轉)記憶體
- Guru of the Week 條款24:編譯級防火牆 (轉)編譯防火牆
- Guru of the Week 條款05:覆寫虛擬函式 (轉)函式
- Guru of the Week 條款13:物件導向程式設計 (轉)物件程式設計
- Guru of the Week 條款07:編譯期的依賴性 (轉)編譯
- Guru of the Week 條款11:物件等同(Object Identity)問題 (轉)物件ObjectIDE
- Guru of the Week 條款14:類之間的關係(上篇) (轉)
- Guru of the Week 條款15:類之間的關係(下篇) (轉)
- Guru of the Week 條款16:具有最大可複用性的通用Containers (轉)AI
- Guru of the Week 條款08:GotW挑戰篇——異常處理的安全性 (轉)Go
- Guru of the Week 條款23:物件的生存期(第二部分) (轉)物件
- Guru of the Week 條款22:物件的生存期(第一部分) (轉)物件
- Guru of the Week 條款21:程式碼的複雜性(第二部分) (轉)
- C++ articles:Guru of the Week #1 (轉)C++
- Guru of the week:#18 迭代指標. (轉)指標
- Guru of the Week 條款20:程式碼的複雜性(第一部分) (轉)
- Guru of the week:#17 型別對映. (轉)型別
- Guru of the week:#19 自動型別轉換. (轉)型別
- C++ articles:Guru of the Week #4 -- Class Mechantics (轉)C++
- Guru of The week #20 程式碼的複雜性 Ⅰ. (轉)
- C++ articles:Guru of the Week #3:使用標準庫 (轉)C++
- Guru of the Week #5:虛擬函式的重新定義 (轉)函式
- CPA一--十三條會計原則(轉載)
- Apache 架構師總結的 30 條架構原則Apache架構
- 設計原則:介面隔離原則(ISP)
- 設計原則之【介面隔離原則】
- Apache 的架構師們遵循的 30 條設計原則Apache架構
- 軟體設計原則—介面隔離原則
- 3條招聘原則成就亞馬遜亞馬遜
- OO幾條設計原則
- 物件導向設計原則之介面隔離原則物件
- 設計原則-依賴反轉原則
- 五 :ISP(介面分離原則)
- 一款優秀的 SDK 介面設計十大原則。