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

第六章 工作類別
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

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

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

它(就是書單,所以略)

沒有留言:

張貼留言

(什麼是留言欄訊息?)