C箴言:最小化文件之間的編譯依賴

字號:

你進入到你的程序中,并對一個類的實現(xiàn)進行了細微的改變。提醒你一下,不是類的接口,只是實現(xiàn),僅僅是 private 的東西。然后你重建(rebuild)這個程序,預計這個任務應該只花費幾秒鐘。畢竟只有一個類被改變。你在 Build 上點擊或者鍵入 make(或者其它等價行為),接著你被驚呆了,繼而被郁悶,就像你突然意識到整個世界都被重新編譯和連接!當這樣的事情發(fā)生的時候,你不討厭它嗎?
    問題在于 C 沒有做好從實現(xiàn)中剝離接口的工作。一個類定義不僅指定了一個類的接口而且有相當數(shù)量的實現(xiàn)細節(jié)。例如:
    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::string theName; // implementation detail
    Date theBirthDate; // implementation detail
    Address theAddress; // implementation detail
    };
    在這里,如果不訪問 Person 的實現(xiàn)使用到的類,也就是 string,Date 和 Address 的定義,類 Person 就無法編譯。這樣的定義一般通過 #include 指令提供,所以在定義 Person 類的文件中,你很可能會找到類似這樣的東西:
    #include
    #include "date.h"
    #include "address.h"
    不幸的是,這樣就建立了定義 Person 的文件和這些頭文件之間的編譯依賴關(guān)系。如果這些頭文件中的一些發(fā)生了變化,或者這些頭文件所依賴的文件發(fā)生了變化,包含 Person 類的文件和使用了 Person 的文件一樣必須重新編譯,這樣的層疊編譯依賴關(guān)系為項目帶來數(shù)不清的麻煩。
    你也許想知道 C 為什么堅持要將一個類的實現(xiàn)細節(jié)放在類定義中。例如,你為什么不能這樣定義 Person,單獨指定這個類的實現(xiàn)細節(jié)呢?
    namespace std {
    class string; // forward declaration (an incorrect
    } // one - see below)
    class Date; // forward declaration
    class Address; // forward declaration
    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;
    ...
    };
    如果這樣可行,只有在類的接口發(fā)生變化時,Person 的客戶才必須重新編譯。
    這個主意有兩個問題。第一個,string 不是一個類,它是一個 typedef (for basic_string)。造成的結(jié)果就是,string 的前向聲明(forward declaration)是不正確的。正確的前向聲明要復雜得多,因為它包括另外的模板。然而,這還不是要緊的,因為你不應該試著手動聲明標準庫的部件。作為替代,直接使用適當?shù)?#includes 并讓它去做。標準頭文件不太可能成為編譯的瓶頸,特別是在你的構(gòu)建環(huán)境允許你利用預編譯頭文件時。如果解析標準頭文件真的成為一個問題。你也許需要改變你的接口設(shè)計,避免使用導致不受歡迎的 #includes 的標準庫部件。
    第二個(而且更重要的)難點是前向聲明的每一件東西必須讓編譯器在編譯期間知道它的對象的大小??紤]:
    int main()
    {
    int x; // define an int
    Person p( params ); // define a Person
    ...
    }
    當編譯器看到 x 的定義,它們知道它們必須為保存一個 int 分配足夠的空間(一般是在棧上)。這沒什么問題,每一個編譯器都知道一個 int 有多大。當編譯器看到 p 的定義,它們知道它們必須為一個 Person 分配足夠的空間,但是它們怎么推測出一個 Person 對象有多大呢?它們得到這個信息的方法是參考這個類的定義,但是如果一個省略了實現(xiàn)細節(jié)的類定義是合法的,編譯器怎么知道要分配多大的空間呢? 這個問題在諸如 Smalltalk 和 Java 這樣的語言中就不會發(fā)生,因為,在這些語言中,當一個類被定義,編譯器僅僅為一個指向一個對象的指針分配足夠的空間。也就是說,它們處理上面的代碼就像這些代碼是這樣寫的:
    int main()
    {
    int x; // define an int
    Person *p; // define a pointer to a Person
    ...
    }
    當然,這是合法的 C ,所以你也可以自己來玩這種“將類的實現(xiàn)隱藏在一個指針后面”的游戲。對 Person 做這件事的一種方法就是將它分開到兩個類中,一個僅僅提供一個接口,另一個實現(xiàn)這個接口。如果那個實現(xiàn)類名為 PersonImpl,Person 就可以如此定義:
    #include // standard library components
    // shouldn’t be forward-declared
    #include // for tr1::shared_ptr; see below
    class PersonImpl; // forward decl of Person impl. class
    class Date; // forward decls of classes used in
    class Address; // Person interface
    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: // ptr to implementation;
    std::tr1::shared_ptr pImpl;
    }; // std::tr1::shared_ptr
    這樣,主類(Person)除了一個指向它的實現(xiàn)類(PersonImpl)的指針(這里是一個 tr1::shared_ptr ——參見 Item 13)之外不包含其它數(shù)據(jù)成員。這樣一個設(shè)計經(jīng)常被說成是使用了 pimpl 慣用法(指向?qū)崿F(xiàn)的指針 "pointer to implementation")。在這樣的類中,那個指針的名字經(jīng)常是 pImpl,就像上面那個。
    用這樣的設(shè)計,使 Person 的客戶脫離 dates,addresses 和 persons 的細節(jié)。這些類的實現(xiàn)可以隨心所欲地改變,但 Person 的客戶卻不必重新編譯。另外,因為他們看不到 Person 的實現(xiàn)細節(jié),客戶就不太可能寫出以某種方式依賴那?/td>
    這個分離的關(guān)鍵就是用對聲明的依賴替代對定義的依賴。這就是最小化編譯依賴的精髓:只要能實現(xiàn),就讓你的頭文件獨立自足,如果不能,就依賴其它文件中的聲明,而不是定義。其它每一件事都從這個簡單的設(shè)計策略產(chǎn)生。所以:
    當對象的引用和指針可以做到時就避免使用對象。僅需一個類型的聲明,你就可以定義到這個類型的引用或指針。而定義一個類型的對象必須要存在這個類型的定義。
    只要你能做到,就用對類聲明的依賴替代對類定義的依賴。注意你聲明一個使用一個類的函數(shù)時絕對不需要有這個類的定義,即使這個函數(shù)通過傳值方式傳遞或返回這個類:
    class Date; // class declaration
    Date today(); // fine - no definition
    void clearAppointments(Date d); // of Date is needed
    當然,傳值通常不是一個好主意,但是如果你發(fā)現(xiàn)你自己因為某種原因而使用它,依然不能為引入不必要的編譯依賴辯解。
    不聲明 Date 就可以聲明 today 和 clearAppointments 的能力可能會令你感到驚奇,但是它其實并不像看上去那么不同尋常。如果有人調(diào)用這些函數(shù),則 Date 的定義必須在調(diào)用之前被看到。為什么費心去聲明沒有人調(diào)用的函數(shù),你想知道嗎?很簡單。并不是沒有人調(diào)用它們,而是并非每個人都要調(diào)用它們。如果你有一個包含很多函數(shù)聲明的庫,每一個客戶都要調(diào)用每一個函數(shù)是不太可能的。通過將提供類定義的責任從你的聲明函數(shù)的頭文件轉(zhuǎn)移到客戶的包含函數(shù)調(diào)用的文件,你就消除了客戶對他們并不真的需要的類型的依賴。