07良好的類介面

來源:互聯網
上載者:User

標籤:保護   輸入   資料庫   employee   決定   1.3   code   地址   添加   

1. 好的抽象1.1 類的介面應該展現一致的抽象層次

? 在考慮類的時候有一個很好地辦法,就是把類看做一種用來實現抽象資料類型的機制。每一個類應該實現一個 ADT,並且僅實現這個 ADT。如果你發現某個類實現了不止一個ADT,或者你不能確定究竟它實現了何種 ADT,你就應該把這個類重新組織為一個或多個更加明確的 ADT。

? 如果把類的公用子程式看中是潛水艇上用來防止進水的氣鎖閥,那麼類中不一致的公用子程式就相當於是漏水的儀錶盤。這些漏水的儀錶盤可能不會讓水像開啟氣鎖閥那樣迅速進入,但只要有足夠的時間,他們還是能讓潛水艇沉沒。實際上,這就是混雜抽象層的後果。在修改程式時,混雜的抽象層次會讓程式越來越難理解,整個程式也會逐漸墮落直到變得無法維護。

1.2 一定要理解類所實現的抽象是什麼

? 一些類非常相像,你必須非常仔細地理解類的介面應該捕捉的抽象到底是哪一個。我曾經開發過這樣一個程式,使用者可以用表格的形式來編輯資訊。我們想用一個簡單的柵格控制項,但它卻不能給資料輸入儲存格換顏色,因此我們決定用一個能提供這一功能的試算表控制項。

? 試算表控制項要比柵格控制項複雜得多,它提供了150個子程式,而柵格控制項只有15個。由於我們的目標是使用一個柵格控制項而不是試算表控制項,因此我們讓程式員寫一個包裹類,隱藏起“把試算表控制項用作柵格控制項”這一事實。這位程式員強烈抱怨,認為這樣做是毫無必要地增加成本,是官僚作風,然後就走了。幾天以後,他帶來了寫好的包裹類,而這個類竟然忠實地把試算表控制項所擁有的全部150個子程式都暴露出來了。

? 這並不是我們想要的。我們要的是一個柵格控制項介面,這個介面封裝了“肥厚實際上是在用一個更為複雜的試算表控制項”的事實。那位程式員應該只暴露那15個柵格控制項的子程式,再加上第16個支援設定儲存格顏色的子程式。他把全部150個子程式都暴露出來,也就意味著一旦想要修改底層實現細節,我們就得支援150個公用子程式。這位程式員沒有實現我們所需要的封裝,也給他自己帶來了大量無畏的工作。

? 根據具體情況的不同,正確的抽象可能是一個試算表控制項,也可能是一個柵格控制項。當你不得不在兩個相似的抽象之間做出選擇時,請確保你的選擇時正確的。

1.3 提供成對的服務(參考亞控命名規範)

? 大多數操作都有和其相應的、相等的以及相反的操作。如果有一個操作用來把燈開啟,那很可能也需要另一個操作來把燈關閉。如果有一個操作來向列表中添加項目,那很可能也需要另一個操作來從列表中刪除項目。如果有一個操作用來啟用功能表項目,那很可能也也需要另一個操作來屏蔽功能表項目。在設計一個類的時候,要檢查每一個公用子程式,決定是否需要另一個與其互補的操作。不要盲目地建立相反操作,但你一定要考慮,看看是否需要它。

1.4 把不相關的資訊轉移到其他類中

? 有時你會發現,某個類中一半子程式使用著該類的一般資料,而另一半子程式使用另一半資料。這時你其實已經把兩個類混在一起使用了,把他們拆開吧!

1.5 儘可能讓介面可程式化,而不是表達語義

? 每個介面都有一個可程式化的部分和一個語義部分組成。可程式化的部分由介面中的資料類型和其他屬性構成,編譯器能強制性地要求它們(在編譯時間檢查錯誤)。而語義部分則由“本介面將會被怎樣使用”的假定組成,而這些事無法通過編譯器來強制實施的。語義介面中包含的考慮比如“ RoutineA 必須在 RoutineB之前被調用”或“如果 dataMember 未經初始化就傳給 RoutineA 的話,將會導致 RoutineA 崩潰”。語義介面應通過注釋說明,但要儘可能讓介面不依賴於這些說明。一個介面中任何無法通過編譯器強制實施的部分,就是一個可能被誤用的部分。要想辦法把語義介面的元素抓換為編程介面的元素,比如說用 Asserts 或其他的技術。

1.6 謹防在修改時破壞介面的抽象

? 在對類進行修改和擴充的過程中,你常常會發現額外所需的一些功能。這些功能並不十分適應原有的類介面,可看上去卻也很難用另一種方法來實現。舉例來說,你可能會發現 Employee 類演變成了下面這個樣子。

//在維護時被破壞的類介面class Employee{  public:    FullName GetName() const;    Address GetAddress() const;    PhoneNumber GetWorkPhone() const;    ...    bool IsJobClassificationValid(JobClassification jobClass);    bool IsZipCodeValid(Address address);    bool IsPhoneNumberVaild(PhoneNumber phoneNumber);    SqlQuery GetQueryToCreateNewEmployee() const;    SqlQuery GetQueryToModifyEmployee() const;    SqlQuery GetQueryToRetrieveEmployee() const;    ...  private:    ...};

? 前面程式碼範例中的清晰抽象,現在已經變成了由一些零散功能組成的大雜燴。在僱工和檢查郵遞區號、電話號碼或職位的子程式之間並不存在什麼邏輯上的關聯,那些暴露 SQL 陳述式查詢細節的子程式所處的抽象成比 Employee 類也要低得多,他們都破壞了 Employee 類的抽象。

1.7 不要添加與介面抽象不一致的公用成員

? 每次你向類的介面中添加子程式時,問問“這個子程式與現有介面所提供的抽象一致嗎?”如果發現不一致,就要換另一種方法來進行修改,以便能夠保證抽象的完整性。

1.8 同時考慮抽象性和內聚性

? 抽象性和內聚性這兩個概念之間的關係非常緊密 —— 一個呈現出很好的抽象的類介面通常也有很高的內聚性。而具有很強內聚性的類往往也會呈現為很好地抽象,儘管這種關係並不如前者那麼強。

? 我發現,關注類的介面所表現出來的抽象,比關注類的內聚性更有助於深入地理解類的設計。如果你發現某個類的內聚性很弱,也不知道該怎麼改,那就換一種方法,問問你自己這個類是否表現為一直的抽象。

2. 良好的封裝

? 封裝是一個比抽象更強的概念。抽象通過 提供一個可以讓你忽略實現細節的模型來管理複雜度,而封裝則強制阻止你看到細節 —— 即便你想這麼做。

? 這兩個概念之所以相關,是因為沒有封裝時,抽象往往很容易被打破。依我的經驗,要麼就是封裝與抽象兩者皆有,要麼就是兩者皆失。除此之外,沒有其他可能。

2.1 儘可能地限制類和成員的可 訪問性

? 讓可訪問性儘可能低是促成封裝的原則之一。當你在猶豫某個子程式的可訪問性設為公有、私人亦或受保護時,經驗之舉是應該採用最嚴格且可行的存取層級。我認為這是一個很好地知道建議,但我認為還有更重要的建議,即考慮“採用哪種方式能最好地保護介面抽象的完整性?”如果暴露一個子程式不會讓抽象變得不一致的話,這麼做很可能是可行的。如果你不確定,那麼隱藏通常比少隱藏要好。

2.2 不要公開暴露成員資料

? 暴露成員資料會破壞封裝性,從而限制你對這個抽象的控制能力。一個 Point 類如果暴露了下面這些成員的話:

float x;float y;float z;

他就破壞了封裝性,因為調用方代碼可以自由地使用 Point 類裡面的資料, 而 Point 類卻甚至連這些資料什麼時候被改動過都不知道。然而,如果 Point 類暴露的時這些方法的話:

float GetX();float GetY();float GetZ();void SetX(float x);void SetY(float y);void SetZ(float z);

那它還是封裝完好的。你無法得知底層實現用的是不是 float x、y、z,也不會知道 Point 是不是把這些資料儲存為 double 然後再轉換成 float,也不可能知道 Point 是不是把它們儲存在月亮上,然後再從外層空間中的衛星上把它們找回來。

2.3 避免把私用的實現細節放入類的介面中

? 做到真正的 封裝以後,程式員們是根本看不到任何實現細節的。無論是在字面上還是在喻義上,它們都被隱藏起來。然而,包括 C++ 在內的一些流行程式設計語言卻從語言結構上要求程式員在類的介面中透漏實現細節。

//暴露了類內部實現細節class Employee{public:    ...    Employee(        FullName name,        String address,        String workPhone,        String homePhone,        TaxId taxIdNumber,        JobClassification jobClass    );    ...private:    String m_Name;    String m_Address;    int m_jobClass;    ...};

? 把 private 段的聲明放到類的標頭檔中,看上去似乎只是小小地違背了原則,但它實際是在鼓勵程式員查閱實現細節。在這個例子中,客戶代碼本意是要使用 Address 類型來表示地址資訊,但標頭檔中卻把“地址資訊用 String 來儲存”的這一實現細節暴露了出來。

? Scott Meyers 在《Effective C++》一書第 2 版中的第 34 條裡介紹了可以解決這個問題的一個管用技法。他建議你把類的介面與類的實現隔離開,並在類的聲明中 包含一個指標,讓該指標指向類的實現,但不能包含任何其他實現細節。

//隱藏了類的實現細節class Employee{public:    ...    Employee( ... ));    FullName GetName() const;    Address GetAddress() const;    ...private:    EmplyeeImplementation* m_implementation;};

? 現在你就可以把實現細節放到 EmplyeeImplementation 類裡了,這個類只對 Employee 類可見,而對使用 Employee 類的代碼來說則不可見。

? 如果你已經在項目裡寫了很多沒有採用這種方法的代碼,你可能會覺得把大量的現有代碼改成使用這種方法是不值得的。但是當你讀到那些暴露了其實現細節的代碼時,你就應該頂住誘惑,不要到類介面的私用部分去尋找關於實現細節的線索。

2.4 不要對類的使用者做出任何假設

? 類的設和實現應該符合在類的介面中所隱含的契約。它不應該對介面會被如何使用或不會被如何使用做出任何假設 —— 除非在介面中有過明確說明。向下面這樣一段注釋就顯示出這個類過多地假定了它的使用者。

//請把 x , y 和 z 初始化為 1.0,因為如果把它們初始化為 0.0 的話,DerivedClass 就會崩潰。
2.5 避免使用友元類

? 有些場合下,比如說 State 模式中,按照正確的方式使用友元類會有助於管理複雜度。但在一般情況下友元類會破壞封裝,因為它讓你在同一時刻需要考慮更多的代碼量,從而增加了複雜度。

2.6 不要因為一個子程式裡僅使用公開子程式,就把他歸入公開介面

? 一個子程式僅僅使用公用的子程式這一事實並不是十分重要的考慮因素。相反,應該問的問題是,把這個子程式暴露給外界後,介面所展示的抽象是否還是一致的。

2.7 讓閱讀代碼比編寫代碼更方便

? 閱讀代碼的次數要比編寫代碼多得多,即使在開發的初期也是如此。因此,為了讓編寫代碼更方便而降低代碼的可讀性是非常不經濟的。尤其是在建立類的介面時,即使某個子程式與介面的抽象不很相配,有時人們也往往把這個子程式加入到介面裡,從而讓正開發的這個類的某處調用代碼能更方便地使用它。然而,這段子程式的添加正式代碼走下坡路的開始,所以還是不要走出這一步為好。

2.8 要格外警惕從語義上破壞封裝性

? 我曾經認為,只要學會避免語法錯誤,就能穩操勝券。然而很快我就發現,學會避免語法錯誤僅僅是個開始,接踵而來的時無以計數的編碼錯誤,而其中大多數錯誤都比語法錯誤更難於診斷和更正。

? 比較起來,語義上的封裝性和文法上的封裝性二者的難度相差無幾。從文法的角度說,要想避免窺探另一個類的內部實現細節,只要把它內部的子程式和資料都聲明為 private 就可以了,這是相對容易辦到的。然而,要想達到語義上的封裝性就完全是另一碼事兒了。下面是一些類的調用方代碼從語義上破壞其封裝性的例子。

  • 不去調用 A 類的 InitializeOperations() 子程式,因為你知道 A 類的 PreformFirstOperation() 子程式會自動調用它。

  • 不再調用 employee.Retrive(database) 之前去調用 database.Connect() 子程式,因為你知道在未建立資料庫連接的時候 employee.Retrive() 會去串連資料庫。

  • 不去調用 A 類的 Terminate() 子程式,因為你知道 A 類的 PreformFinalOperation() 子程式已經調用它了。

  • 即便在 ObjectA 離開範圍之後,你仍去使用有 ObjectA 建立的、指向 ObjectB 的指標或引用,因為你知道 ObjectA 把 ObjectB 放置在靜態儲存空間中了,因此 ObjectB 肯定還可以用。

  • 使用 ClassB.MAXIMUM_ELEMENTS 而不用 ClassA.MAXIMUM_ELEMENTS,因為你知道它們兩個值是相等的。

    ? 上面這些例子的問題都在於,它們讓調用方代碼不依賴與類的公開介面,而是依賴於類的私人實現。每當你發現自己是通過查看類的內部實現來得知該如何使用這個類的時候,那就不是在針對介面編程,而是在透過介面針對內部實現編程了。如果你透過介面來編程的話,封裝性就被破壞了,而一旦封裝性開始遭到破壞,抽象能力也就快遭殃了。

    ? 如果僅僅根據類的介面文檔還是無法得知如何使用一個類的話,正確的做法不是拉出這個類的原始碼,從中查看其內部實現。這是個好的初衷,但卻是個錯誤的決斷。正確的做法應該是去聯絡類的作者,告訴他“我不知道該怎麼使用這個類。”而對於類的作者來說,正確的做法不是面對面告訴你答案,而是從程式碼程式庫中 check out 類的介面檔案,修改類的介面文檔,再把檔案 check in 回去,然後告訴你“看看現在你知不知道該怎麼用它了。”你希望讓這一次對話出現在介面代碼裡,這樣就能留下來讓以後的程式員也能看到。你不希望讓這一次對話值存在於自己的腦海裡,這樣會給使用該類的調用方代碼烙下語義上額微妙依賴性。你也不想然這一次對話只在個人之見進行,這樣只能讓你的代碼獲益,而對其他人沒有好處。

2.9 留意過於緊密的耦合關係
“耦合”是指兩個類之見關聯的緊密程度,通常,這種關係越松越好,根據這一概念可以得出以下一些指導建議:
  • 儘可能地限制類和成員的可訪問性。
  • 避免友元類,因為它們之間是緊密耦合的。
  • 在基類中把資料聲明為 private 而不是 protected,以降低衍生類別和基類之間耦合的程度。
  • 避免在類的公開介面中暴露成員資料。
  • 要對從語義上破壞封裝性保持警惕。
  • 察覺“ DEmeter 法則”
    耦合性與抽象性和封裝性有著非常密切的聯絡。緊密的耦合性總是發生在抽象不嚴謹或封裝性遭到破壞的時候。如果一個類提供了一套不完整的服務,其他的子程式就可能要去直接讀寫該類的內部資料。這樣一來就把類給拆開了,把它從一個黑盒子變成一個玻璃盒子,從而事實上消除了類的封裝性。

07良好的類介面

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.