Guru of the Week 條款30附錄:介面原則 (轉)

worldblog發表於2007-12-13
Guru of the Week 條款30附錄:介面原則 (轉)[@more@]

(至此,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   // this header

  //  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/,如需轉載,請註明出處,否則將追究法律責任。

相關文章