namespace std { class string;} //前置聲明(不正確)
class Date;//前置聲明
class Address;//前置聲明
class Person{
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name() const;
std::string birthDate() const;
std::string address() const;
...
};
如果這樣,Person的客戶就只有在Person介面被修改時才重新編譯。
兩個問題:第一,string不是個class,它是個typedef。因此string前置聲明並不正確,而且你本來就不應該嘗試手工聲明一部分標準程式庫。你應該僅僅使用適當的#includes完成目的。標準標頭檔不太可能成為編譯瓶頸,
第二,編譯器必須在編譯期間知道對象的大小:
int main()
{
int x;
Person p(params);
}
編譯器知道必須分配足夠空間放置一個Person,但是他必須知道一個Person對象多大,獲得這一資訊的唯一辦法是詢問class定義式。然而,如果class定義式可以合法的不列出實現細目,編譯器如何知道該分配多少空間?
此問題在smalltalk,java等語言上並不存在,因為當我們以那種語言定義對象時,編譯器只分配足夠空間給一個指標(用於指向該對象)使用。就是說它們將上述代碼視同這樣子:
int main()
{
int x;
Person* p;
}
這當然也是合法的c++代碼,所以你可以玩玩“將對象實現細目隱藏在一個指標背後”的遊戲。可以把Person分割為兩個classes,一個提供介面,另一個負責實現介面。負責實現的那個所謂的implementation class取名為PersonImpl,Person將定義如下:
#include <string>
#include <memory>
class PersonImpl;
class Date;
class Address;
class Person{
public:
Person(const std::string& name, const Date& birthday, const Address& addr);
std::string name()const;
std::string birthDate() const;
std::string address()const;
...
private:
std::tr1::shared_ptr<PersonImpl> pImpl; //指向實現物的指標
};
這裡,Person只內含一個指標成員,指向其實作類別(PersonImpl)。這個設計常被稱為pimpl idiom(pimpl是“pointer to implementation”的縮寫)。
這樣,Person的客戶就完全與Date,Address以及Person的實現細目分離了。那些classes的任何實現修改都不需要Person用戶端重新編譯。
這個分離的關鍵在於以“聲明的依存性”替換“定義的依存性”,那正是編譯依存性最小化的本質:讓標頭檔儘可能自我滿足,萬一做不到,則讓它與其他檔案內的聲明式(而非定義式)相依。其他每件事都源自於這個簡單的涉及策略。
如果用object reference 或 object pointer可以完成任務,就不要用objects。
可以只靠聲明式定義出指向該類型的pointer和reference;但如果定義某類型的objects,就需要用到該類型的定義式。
如果能夠,盡量以class聲明式替換class定義式。
當你聲明一個函數而它用到某個class時,你並不需要該class的定義式,縱使函數以by value方式傳遞該類型的參數(或傳回值)亦然:
class Date; //class 聲明式
Date today();
void clearAppiontments(Date d);
聲明today函數和clearAppointments函數無需定義Date,但是一旦有任何人調用那些函數,調用之前Date定義式一定得先曝光才行。如果能夠將“提供class定義式”(通過#include完成)的義務從“函式宣告所在”之標頭檔移轉到“內含函數調用”之客戶檔案,便可將“並非真正必要之類型定義”與用戶端之間的編譯依存性去除掉。
為聲明式和定義式提供不同的標頭檔。
因此程式庫客戶應該總是#inlcude一個聲明檔案而非前置聲明若干函數,
#include "datefwd,h" //這個標頭檔內聲明class Date
Date today();
void clearAppointments(Date d);
只含聲明式的那個標頭檔名為“datefwd.h”,像標準程式庫的標頭檔“<iosfwd>”。他分外彰顯“本條款適用於templates也適用於non-templates”。許多建置環境中template定義式同常被置於標頭檔中,但也有某些建置環境允許tamplates定義式放在“非標頭檔中”,這樣就可以將“只含聲明式”的標頭檔提供給templates。
這種使用pimpl idiom的classes,往往被稱為Handle classes。
這種classes的辦法之一就是將他們的所有函數轉交給相應的實作類別(implementation classes)並由後者完成實際工作。
#include "Person.h"
#include "PersonImpl.h"
Person::Person(const std::string& name, const Date& birthday, const Address& addr)
: pImpl(new PersonImpl(name, birthday, addr))
{}
std::string Person::name() const
{
return pImpl->name();
}
另一個製作Handle class的辦法是,令Person稱為一種特殊的abstract base class(抽象基類)稱為Interface classes。這種class的目的是詳細一一描述derived classes的介面,因此它通常不帶成員變數,也沒有建構函式,只有一個virtual解構函式以及一組pure virtual函數,又來敘述整個介面。
一個針對Person而寫的Interface class或許看起來像這樣:
class Person{
public:
virtual ~Person();
virtual std::string name() const = 0;
virtual std::string birthday() const = 0;
virtual std::string address() const = 0;
...
};
這個class的客戶必須以Person的pointers和reference來撰寫應用程式,不能針對“內含pure virtual函數”的Person classes具現出實體。除非Interface class的介面被修改否則其客戶不需要重新編譯。
Interface class 的客戶必須有辦法為這種class建立新對象。它們通常調用一個特殊函數,此函數扮演一個“真正將被具現化”的那個derived class的建構函式角色。通常稱為工廠factory函數或virtual 建構函式。它們返回指標,指向動態分配所得對象,而該對象支援interface class的介面。這樣的函數又往往在interface class內被聲明為static:
class Person{
public:
...
static std::tr1::shared_ptr<Person>
create(const std::string& name, const Date& birthday, const Address& addr);
};
客戶可能會這樣使用它們:
std::string name;
Date dateBirth;
Address address;
std::tr1::shared_ptr<Person> pp(Person::create(name, dateBirth, address));
...
std::cout << pp->name()
<< "was born on "
<< PP->birthDate()
<< " and now lives at "
<< pp->address();
...
當然支援interface class介面的那個具象類(concrete classes)必須被定義出來,而真正的建構函式必須被調用。
假設有個derived class RealPerson,提供繼承而來的virtual函數的實現:
class RealPerson : public Person{
public:
RealPerson(const std::string& name, const Date& birthday, const Address& addr)
: theName(name), theBirthDate(birthday), theAddress(addr)
{}
virtual ~RealPerson(){}
std::string name() const;
std::string birthDate() const;
std::string address() const;
private:
std::string theName;
Date theBirthDate;
Address theAddress;
};
有了RealPerson之後,寫出Person::create就真的一點也不稀奇了:
std::tr1::shared_ptr<Person> Person::create(const std::string& name, const Date& birthday, const Address& addr)
{
return std::tr1::shared_ptr<Person>(new RealPerson(name, birthday, addr));
}
一個更現實的Person::create實現代碼會建立不同類型的derived class對象,取決於諸如額外參數值、獨自檔案或資料庫的資料、環境變數等等。
RealPerson示範實現了Interface class的兩個最常見機制之一:從interface class繼承介面規格,然後實現出介面所覆蓋的函數。
程式庫標頭檔應該以“安全且僅有聲明式”(full and declaration-only forms)的形式存在。這種做法不論是否涉及templates都適用。