Code Complete II《軟體建構之道 2》#6 讀書心得與整理

  1. Code Complete II《軟體建構之道 2》#6 讀書心得與整理
    1. 第六章 工作類別 Working Classes
      1. 6.1 類別基礎 Class Foundations: Abstract Data Types(ADTs)
        1. 需要ADT的範例 Example of the Need for an ADT
        2. 使用ADT的好處 Benfits of Using ADTs
        3. 其它ADT的範例 More Example of ADTs
        4. 在非物件導向的環境中,以ADT處理多重資料執行個體 Handing Multiple Instances of Data with ADTs in Non-Object-Oriented Environments
        5. ADT與類別
      2. 6.2 良好的類別介面 Good Class Interface
        1. 良好的抽象概念 Good Abstraction
        2. 良好的封裝 Good Encapsulation
      3. 6.3 設計與實作問題 Design and Implementation
        1. 內含「has a」的關係 Containment( "has a" Relationships)
        2. 繼承「is a」的關係 Inherlatance( "is a" Relationships)
        3. 成員函式與資料 Member Function and Data
        4. 建構函式 Constructors
      4. 6.4 建立類別的理由 Reasons to Creats a Class
        1. 要避免的類別 Classes to Avoid
      5. 6.5 程式語言相關問題 Language-Specific Issues
      6. 6.6 超越類別:封裝 Beyond Clesses: Packages
        1. 其它(就是書單,所以略)

Code Complete II《軟體建構之道 2》#6 讀書心得與整理

原文連結: https://darkblack01.blogspot.com/2012/09/code-complate-ii-26.html
移植時的最後更新日期: 2014-06-27T09:01:37.284+08:00

第六章 工作類別
Working Classes

電腦時代        初期       1970~1980    21世紀
--------------------------------------------------->
思考程式        陳述式     常式              類別
設計的概念    (算式)  (副程式)      (物件導向)


6.1 類別基礎
Class Foundations: Abstract Data Types(ADTs)

抽象資料型別(ADT)=「資料」+「運用資料的操作」

設定字型大小12pt, 16px

需要ADT的範例
Example of the Need for an ADT

沒有ADT(見招拆招法):(在此,是假設宣告了一個struct,直接存取成員)
currFont.size = 16;
currentFont.size = PointsToPixels(12); //若有建一組程式庫副程式
currentFont.sizeInPixels = PointsToPixels(12); //提供更具意義的樣式名稱

不可以同時使用這兩種
currentFont.sizeInPixels;
currentFont.sizeInPoints;
一個struct中有兩個值代表字型大小,那要聽誰的?(單位不同,程式碼並不知道)

要設定成粗體字:
currentFont.attribute = currentFont.attribute | 0x02;
currentFont.attribute = currentFont.attribute | BOLD; //簡單狀況,寫出比上面的例子更清楚
currentFont.bold = true;
直接控制資料成員,限制了currentFont的使用方式

使用ADT的好處
Benfits of Using ADTs

有用ADT
優點:
  • 隱藏實作細節
  • 變更不需牽動整個程式
  • 介面更平易近人
currentFont.attribute = currentFont.attribule | 0x02  //檢查這種程式碼是煩人
//可能出現錯誤的結構名稱、欄位名稱、運算子、屬性值...
currentFont.SetBoldOn() //檢查這種程式碼的正確性,是小菜一碟
//可能出現錯誤的常式名稱
  • 易於改程式
  • 程式的正確性容易判別
  • 程式的自我註解更為完整
currentFont.attribute = currentFont.attribule | 0x02
0x02改成其它更可以代表語意的文字,但是都不會比SetBoldOn()好讀
(相差30%的錯誤發生率)
  • 不需要在程式中把資料傳來傳去
  • 你可以操弄現實世界的實體,而非僅止於低階的程式實作結構
currentFont.SetSizeInPoints(sizeInPoints);
currentFont.SetSizeInPixels(sizeInPixels);
currentFont.SetBoldOn();
currentFont.SetBoldOff();
currentFont.SetItalicOn();
currentFont.SetItalicOff();
currentFont.SetTypeFace( faceName );
和見招拆招法的差別之處,將字型操作隔離在一組常式之中。
(用文字取代算式之後,看不見算式就是「隔離」)

其它ADT的範例
More Example of ADTs

核子反應爐冷卻系統
coolingSystem.GetTemperature();
coolingSystem.SetCirculationRate(rate);
coolingSystem.OpenFalve(valveNumber);
coolingSystem.CloseValve(valveNumber);
任何現實世界的動作,都可以做成常式的名稱取代一堆算式。

研究這些範例後,引申出下列幾個方針:
  • 將一般低資料型別建立為ADT或做為ADT使用,而非保持在低階資料型別的層次上
  • 將一般物件(如檔案)視為ADT
  • 再簡單的項目也要當作ADT看待
  • 在引用ADT時不要依賴儲存介質
RateFile.Read()  //費率表很大,超大,只能儲存在磁碟上,當作是「費率檔」使用;
贅述了不必要的資料相關資訊,程式碼會有錯誤的隱喻發生,類別存取常式的名稱應和資料儲存方式有關,而是依據抽象資料本身
rateTable.Read()  //比較恰當(對於一個程式設計師看得到的部份來說)

在非物件導向的環境中,以ADT處理多重資料執行個體
Handing Multiple Instances of Data with ADTs in Non-Object-Oriented Environments

字型ADT原本提供以下服務:
currentFont.SetSize(sizeInPoints)
currentFont.SetBoldOn();
currentFont.SetBoldOff();
currentFont.SetItalicOn();
currentFont.SetItalicOff();
currentFont.SetTypeFace(faceName);
在非物件導向中(沒有類別的情況)
SetCurrentFontSize(sizeInPoints)
SetCurrentFontBoldOn();
SetCurrentFontBoldOff();
SetCurrentFontItalicOn();
SetCurrentFontItalicOff();
SetCurrentFontFontTypeFace(faceName);
處理一個以上的字型,新增服務來建立及刪除字型執行個體
Creat(fontId);
DeleteFont(fontId);
SetCurrent(fontId);
其它三種方式
  1. 在你每次使ADT服務時明確指定執行個體,將fontId傳遞給每個操縱字型的常式。
  2. 明確提供ADT服務使用的資料,建立起一個Font型別,設計一些ADT服務常服務常式。
  3. 使用隱含的執行個體,特定的字型執行個體設定為目前所使用的字型,當其它服務呼叫時,便會使用目前的字型。

ADT與類別

ADT≠類別
類別概念的基礎,是抽象資料型別

6.2 良好的類別介面
Good Class Interface

高品質類別是第一步,建立良好的介面

良好的抽象概念
Good Abstraction

維持良好的整體概念性(人月神話提到的其中一重點)讓類別有整體概念,在讀code時可以比較容易「猜中」(直覺的了解)這個類別可以做什麼,而可能做了些什麼。

不好的範例如下(用一個類別代表程式):
class Program{
public:
void InitializeCommandStack();
void PushCommand( Command command );
Command PopCommand();
void ShutdownCommandStack();
void InitializeReportFormatting();
void FormatReport( Report report);
void PrintReport( Report report);
void InitializeGlobalData();
void ShutdownGlobalData();
...
private:
...
};
類別並沒有呈現一致性的抽象概念,沒有內聚力。
比較一致性的Program抽象概念如下:
class Program{
public:
...
void InitializeUserInterface(); ←一樣
void ShutdownUserInterface(); ←一樣
void InitializeReports();
void ShutdownReport();
...
private:
...
};
以下的例子,因為抽象概念未能保持單一性,因此導致類別呈現的介面不一致:
class EmployeeCensus: public ListContainer {
public:
//員工的層次
void AddEmployee( Employee employee );
void RemoveEmployee( Employee employee );

//清單的層次
Empolyee NextItemInList();
Empolyee FirstItem();
Empolyee LastItem();
...
private:
...
};
這類別代表兩種ADT: Empolyee和List Container。因為它未能隱藏程式庫類別的「痕跡」。容器「痕跡」是否應該成為抽象概念的一部份?它通常就是應該在程式中隱藏的細節
隱藏.cpp裡一切細節的範例程式裡有詳細介紹,並且提供 程式碼直接看
class EmployeeCensus {
public:
void AddEmployee( Employee employee ); ←一樣
void RemoveEmployee( Employee employee ); ←一樣

Empolyee NextEmployee();
Empolyee FirstEmployee();
Empolyee LastEmployee();
...
private:
ListContainer m_EmployeeList; //掩護掉的「痕跡」
...
};
請確定能夠了解類別要實作的抽象概念是什麼(這一段必看!重要!棒)
提供具相對性的服務
  • 在實作這一件事的同時,請思考是否要「簡化不必要的作業」
把不相干的資訊移到其連類別中
盡可能讓介面程式化,而非語意化
每個介面都有程式面的一部份和語意面的一部份。
  • 程式面的部份:資料型別與其它介面屬性,可以由編譯器強制執行。
  • 語意面的部份:介面使用方式的假設構成,無法由編譯器強制執行。
在修改程式時,小心介面的抽象概念遭到扭曲
請勿加入不符合介面抽象概念的公用成員
同時考慮抽象概念內聚力
  • 好的抽象概念→形成(較強)→強大的內聚力
  • 好的抽象概念←形成(不強)←強大的內聚力

良好的封裝
Good Encapsulation

抽象概念:提供模型,讓你忽視實作細節
封裝:強制的方式阻止你觀看細節
與其「保持可行性的狀況下採用最嚴謹的隱私等級」
最重要的是「如何才能使介面抽象概念保持在完整的地步。」

勿將成員資料公開
避免將不公開的實作細節放入類別介面中
(參考Effective C++ #34)

請勿對類別的使用者做出任何假設
避免使用Friend類別
《物件導向編程精要》也有說「與class處於同一個namespace的friend,functions也是class介面的一部份」,它會破壞封裝性。
不要因為某個常式僅使用公用常式,就把該常式放入公開介面
還是要注意概念整體性
不要為了寫作時的便利性而損及閱讀時的便利性
要非常、非常地小心封裝的語意違規
也就是假設實作為已知的任何行為
過於緊密的結合

良好的類別介面設計考慮順序:

  1. 抽象概念
  2. 降低藕合,增加內聚
  3. 良好的封裝

6.3 設計與實作問題
Design and Implementation

討論類別實作的作法:
  • 內含
  • 繼承
  • 成員函式與資料
  • 類別藕合
  • 建構函式
  • 數據與參考物件
  • ...等相關問題

內含「has a」的關係
Containment( "has a" Relationships)


在「內含」實作「擁有」的概念
在類別中宣告的東西=擁有東西。
在非公開繼承中實作「擁有」的概念,做為最後手段
意思就是「這招能不用就不用」


繼承「is a」的關係
Inherlatance( "is a" Relationships)

繼承的目的在於「定義基礎類別,進而建立較為簡單的程式碼」
使用之前,先思考兩個問題:
  1. 成員常式而言,常式是否可為衍生類別所見?是否有預設的實作?預設實作是否可加以覆寫?
  2. 資料成員而言,資料成員是否為衍生所見
在公開繼承中實作「相等」的概念
新類別=舊類別的專門化
「繼承」只有兩種使用方式

  1. 小心使用
  2. 禁止使用
不打算讓類別受到繼承,使用non-virtual(C++)、finish(JAVA)、non-overridable(VB)


我和讀書會的朋友討論:
C++要實踐這件事,要C11的新標準才可以。



    遵守Liskov代換原則(LSP)
    除非衍生出來的類別確實是其礎類別專門化之後的結果,否則不該輕言繼承該類
    繼承的優點:
    當程式寫作符合Liskov代換原則時,「繼承」就搖身一變成為減少複雜性的強大工具(也就是正確的用法)
    繼承的缺點:
    如果程式設計人員必不思考子類別實作間語意差異,那麼「繼承」就會增加複雜性。

    不要繼承多餘的事物
                    | 可覆寫       | 不可覆寫
                    | virtual     | non-virtual
    ----------------+-------------+--------------------
    實作:提供預設值   | 可overRide   |  不可overRide
    (有實作)        |             |
    ----------------+-------------+--------------------
    實作:不提供預設值 | Pure virtual |    x
    (沒有實作)      | 可overRide   |

    要介面→用繼承;要實作→用內含

    不要「overRide」non-virtual的成員函式
    將共同介面、資料、行為盡可能地往繼承樹狀結構的上層移動
    當你發現移動時破壞了高層物件的抽象概念時,就是住手的時候
    注意只唷一個執行個體的類別
    class A{};  class B{A a;};         →A和B,說不定可以合成同一個類別。
    注意 只有一個衍生類別的基礎類別
    class A{};  class B : public A{};  →A和B,說不定可以合成同一個類別。
    如果類別覆寫常式,但衍生的常式又不具目的,請留意
    注意表達的概念是否可以獨言運作
    避免深層的繼承樹狀結構
    繼承的原意是簡化程式碼。
    當你在進行大規模型別檢查時,請愛用多型現象
    類似的case→多型取代
    不類似的case→還是用switch-case
    把資料宣告為private而非protected
    衍生類別若要存取基礎類別的成員,請在基礎類別提供protected的函式
    多重繼承(Mix in)混合體
    Mulitiple Inheritance
    (影分身之術解除後的結果。沒看錯!是火影忍者)

    「繼承」哪來那麼多規矩?
    Why Are There So Many Rules for Inheritance

        |  資料     成員函數
    ----+-------------------
    內含 |   共用     不共用
    繼承 | 不共用       共用
    繼承 |   共用       共用
    ----+-------------------
    繼承 | 讓基礎類別控制介面
    內含 |   自己親自控制介面

    成員函式與資料
    Member Function and Data

    盡可能減少類別中的常式
    降低出錯率
    以隱含的方式禁止你不需要的成員函式與運算子產生
    將不要的函式,宣告在private
    盡可能的減少類別呼叫的不同常式
    「扇出」:類別本身所使用的類別愈多,愈容易出錯
    減少對其它類別間常式呼叫
    直接關係(return by refrence)已經夠危險了,間接關係卻還要更危險。
    account.ContactPreson().DaytimeContactInfo().PhoneNumber()
    「迪米特法則(最少知識原則)」 - Lieberherr & Holland

    總而言之,盡可能減少類別與其它類別間合作的程度

    • 減少 其現化的物件種類數量
    • 減少 在具現化物件上進行直接常式叫的種類數量
    • 減少 針對其它其現化物件所回傳的物件,進行式呼叫的數量

    建構函式
    Constructors

    盡可能的初始化各種建構函式中的各項成員資料
    初始化資料(記憶體空間)是一種防禦性程式設計方法
    使用私用建構函式,強制執行單件屬性
    只允許單一物件時(英雄人物類別),隱藏所有建構式,提供靜態的GetInstance()存取這個物件
    //Speak in Java Code
    public class MasId
    {
    private MaxId(){...}; //別人無法宣告
    public static MaxId GetInstance() //用MaxId.GetInstance();呼叫它
    {
    return m_instance;
    }
    private static final MasId m_instace = new MaxId(); //程式一開始就宣告好了(static)
    };
    More Effective C++ #26 有C++的實作方式

    如果你無法確定,請選擇深層副本,不要選擇淺層副本
    深層副本:複製物件。
    淺層副本:物件的指標或參考。
    差別:效能。
    詳情參閱25章 :程式碼微調策略。

    6.4 建立類別的理由
    Reasons to Creats a Class

    建立現實世界模型
    建立抽象物件模型
    降低複雜性
    建立良介面,寫完實作內容便忘記。縮小程式碼規模,改善維度護、正確性,都沒有這理由來得重要。
    隔離複雜性
    複雜演算法、大型資料集、通訊協定,都包起來
    隱藏實作細節
    控制變更所帶來的影響
    最有可能會變動規格的區域成為最容易維護修改的區域
    隱藏全域資料
    全域資料不過是物件資料
    簡化參數傳遞
    簡化參數本身並不是目標,傳一個比傳很多個來得好而已。
    建立中央控制點
    建立可重複使用的程式碼
    使用物件導向,可以使用70%的程式碼
    預先規劃一系列的程式
    包裝相關的作業
    實現特定的重整

    要避免的類別
    Classes to Avoid


    避免建立萬能類別
    去除不相關的類別
    類別中,只有資料沒有行為→解散?
    避免動詞命名的類別
    類別中,只有行為沒有資料→解散?投靠其它類別

    6.5 程式語言相關問題
    Language-Specific Issues

    Java中,常式預設可覆寫(virtual)
    C++中,常式預設不可覆寫,宣告virtual才可覆寫
    VB中,在基礎類別宣告overridable,衍生類別要使用overrides,才可以覆寫
    在此討論隨程式語言不同而有所出入的地方。(略)

    6.6 超越類別:封裝
    Beyond Clesses: Packages

    為了達成模組性:從「陳述式」→「子常式」→「類別」
    手動封裝的三個重點:
    區別公用類別與封裝私用類別的命名慣例
    辨識類別「所屬封裝的命名」或「程式碼組識」慣例
    定義哪些封裝可以使用其它封裝的規則,包括用途是否可為「繼承」、「內含」,或兩者兼備
    以上是語言沒有封裝時,要封裝的另類做法(替代方案)

    看出
    「將程式『設計成語言』」
    還是
    「『用語言』進行程式設計」

    它(就是書單,所以略)