[C\C++]何謂封裝

封裝是OOP(object-oriented programming)裏最基本的一個概念,但並不是每個人都能理解它。我說的理解,是指一種深度的自我思維。
    先來看看現在在各類VC教程中關於對象的講解中經常能看見的如下的一個類的設計。
    class Person
    { private: char m_Name[20]; unsigned long m_Age; bool m_Sex;
      public:  const char* GetName() const;  void SetName( const char* );
               unsigned long GetAge() const; void SetAge( unsigned long );
               bool GetSex() const;          void SetSex( bool );
    };
    上面將成員變量全部定義爲private,然後又提供三對Get/Set函數來存取上面的三個成員變量(因爲它們是private,外界不能直接存取),這三對函數都是public的,爲什麼要這樣?那些教材將此稱作封裝,是對類Person的內部內存佈局的封裝,這樣外界就不知道其在內存上是如何佈局的並進而可以保證內存的有效性(只由類自身操作其實例)。
    首先要確認上面設計的荒謬性,它是正宗的“有門沒鎖”毫無意義。
    再看所謂的對內存佈局的封裝。假設上面是在Person.h中的聲明,然後在b.cpp中要使用類Person,於是在b.cpp加了#include "Person.h",然後加上如下代碼:
    struct PERSON { char m_Name[20]; unsigned long m_Age; bool m_Sex; };
    Person a, b; PERSON *pP = ( PERSON* )&a; pP->m_Age = 40;
    上面直接修改了Person的實例a的成員Person::m_Age,如何能隱藏內存佈局?!請回想聲明的作用,類的內存佈局是編譯器生成對象時必須的,根本不能對任何使用對象的代碼隱藏有關對象實現的任何東西,否則編譯器無法編譯相應的代碼。
    那麼從語義上來看。Person映射的不是真實世界中的人的概念,應該是存放某個數據庫中的某個記錄人員信息的表中的記錄的緩衝區,那麼緩衝區應該具備那三對Get/Set所代表的功能嗎?緩衝區是緩衝數據用的,緩衝後被其它操作使用,就好像箱子,只是放東西用。故上面的三對Get/Set沒有存在的必要,而三個成員變量則不能是private。事實上,在這種語意下,Person更適合作爲一個struct,而不是一個class。當然,如果Person映射的並不是緩衝區,而在其它的世界中具備像上面那樣表現的語義,則像上面那樣定義就沒有問題,但如果是因爲對內存佈局的封裝而那樣定義類則是大錯特錯的。
    上面錯誤的根本在於沒有理解何謂封裝。爲了說明封裝,先看下MFC(Microsoft Foundation Class Library)中的類CFile的定義。從名字就可看出它映射的是操作系統中文件的概念,但它卻有這樣的成員函數——CFile::Open、CFile::Close、CFile::Read、CFile::Write,有什麼問題?這四個成員函數映射的都是對文件的操作而不是文件所具備的功能,分別爲打開文件、關閉文件、從文件讀數據、向文件寫數據。這不是和前面說的成員函數的語義相背嗎?上面四個操作有個共性,都是施加於文件這個資源上的操作,可以將它們叫做“被功能”,如文件具有“被打開”的功能,具有“被讀取”的功能,但應注意它們實際並不是文件的功能。
    按照原來的說法,應該將文件映射爲一個結構,如FILE,然後上面的四個操作應映射成四個函數,再利用名字空間的功能,如下:
    namespace OFILE
    {
        bool Open( FILE&, … );  bool Close( FILE&, … );
        bool Read( FILE&, … );  bool Write( FILE&, … );
    }
    上面的名字空間OFILE表示裏面的四個函數都是對文件的操作,但四個函數都帶有一個FILE&的參數。回想非靜態成員函數都有個隱藏的參數this,因此,一個了不起的想法誕生了。
    將所有對某種資源的操作的集合看成是一種資源,把它映射成一個類,則這個類的對象就是對某個對象的操作,此法被稱作封裝,而那個類被稱作包裝類或封裝類。很明顯,包裝類映射的是“對某種資源的操作”,是一抽象概念,即包裝類的對象都是無狀態對象(指邏輯上應該是無狀態對象,但如果多個操作間有聯繫,則還是可能有狀態的,但此時它的語義也相應地有些變化。如多一個CFile::Flush成員函數,用於刷新緩衝區內容,則此時就至少有一個狀態——緩衝區,還可有一個狀態記錄是否已經調用過CFile::Write,沒有則不用刷新)。
    現在應能瞭解封裝的含義了。將對某種資源的操作封裝成一個類,此包裝類映射的不是世界中定義的某一“名詞性概念”,而是世界的“動詞性概念”或算法中“對某一概念的操作”這個人爲定出來的抽象概念。由於包裝類是對某種資源的操作的封裝,則包裝類對象一定有個屬性指明被操作的對象,對於MFC中的CFile,就是CFile::m_hFile成員變量(類型爲HANDLE),其在包裝類對象的主要運作過程(前面的CFile::Read和CFile::Write)中被讀。
    有什麼好處?封裝提供了一種手段以將世界中的部分“動詞性概念”轉換成對象,使得程序的架構更加簡單(多條“動詞性概念”變成一個“名詞性概念”,減少了“動詞性概念”的數量),更趨於面向對象的編程思想。
    但應區別開包裝類對象和被包裝的對象。包裝類對象只是個外殼,而被包裝的對象一定是個具有狀態的對象,因爲操作就是改變資源的狀態。對於CFile,CFile的實例是包裝類對象,其保持着一個對被包裝對象——文件內核對象(Windows操作系統中定義的一種資源,用HANDLE的實例表徵)——的引用,放在CFile::m_hFile中。因此,包裝類對象是獨立於被包裝對象的。即CFile a;,此時a.m_hFile的值爲0或-1,表示其引用的對象是無效的,因此如果a.Read( … );將失敗,因爲操作施加的資源是無效的。對此,就應先調用a.Open( … );以將a和一特定的文件內核對象綁定起來,而調用a.Close( … );將解除綁定。注意CFile::Close調用後只是解除了綁定,並不代表a已經被銷燬了,因爲a映射的並不是文件內核對象,而是對文件內核對象操作的包裝類對象。
    說到這裏,你是否開始對類的封裝概念的理解有所改變?這也正是我一指想指出的,何時該用struct,何時該用class。你或許會突然想到一個很實際的問題,老虎能夠吃兔子,兔子能夠被吃,那這裏應該是老虎有個功能是“吃兔子”還是多個兔子的包裝類來封裝“吃兔子”的操作?這其實不存在任何問題,“老虎吃兔子”和“兔子被吃”完全是兩個不同的操作,前者涉及兩種資源,後者只涉及一種資源,因此可以同時實現兩者,具體應視各自在相應世界中的語意。如果說“吃”這個動作,對於老虎本身來說沒有任何改變,那麼將“吃”這個操作封裝於“老虎”上,是毫無意思的。我提到“老虎吃兔子”涉及兩種資源,即“兔子消失”,而“老虎由飢餓的狀態變爲飽的狀態”,所以這個“吃”才能作用於“老虎”身上。
    封裝是OOP的根本,只有自己真的去體會了它的含義,才能真正明白OO。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章