第六章 工作類別
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);
其它三種方式- 在你每次使ADT服務時明確指定執行個體,將fontId傳遞給每個操縱字型的常式。
- 明確提供ADT服務使用的資料,建立起一個Font型別,設計一些ADT服務常服務常式。
- 使用隱含的執行個體,特定的字型執行個體設定為目前所使用的字型,當其它服務呼叫時,便會使用目前的字型。
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介面的一部份」,它會破壞封裝性。
不要因為某個常式僅使用公用常式,就把該常式放入公開介面
還是要注意概念整體性
不要為了寫作時的便利性而損及閱讀時的便利性
要非常、非常地小心封裝的語意違規
也就是假設實作為已知的任何行為
過於緊密的結合
良好的類別介面設計考慮順序:
- 抽象概念
- 降低藕合,增加內聚
- 良好的封裝
6.3 設計與實作問題
Design and Implementation
討論類別實作的作法:- 內含
- 繼承
- 成員函式與資料
- 類別藕合
- 建構函式
- 數據與參考物件
- ...等相關問題
內含「has a」的關係
Containment( "has a" Relationships)
在「內含」實作「擁有」的概念
在類別中宣告的東西=擁有東西。
在非公開繼承中實作「擁有」的概念,做為最後手段
意思就是「這招能不用就不用」
繼承「is a」的關係
Inherlatance( "is a" Relationships)
繼承的目的在於「定義基礎類別,進而建立較為簡單的程式碼」使用之前,先思考兩個問題:
- 成員常式而言,常式是否可為衍生類別所見?是否有預設的實作?預設實作是否可加以覆寫?
- 資料成員而言,資料成員是否為衍生所見
新類別=舊類別的專門化
「繼承」只有兩種使用方式
- 小心使用
- 禁止使用
我和讀書會的朋友討論:
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
為了達成模組性:從「陳述式」→「子常式」→「類別」手動封裝的三個重點:
區別公用類別與封裝私用類別的命名慣例
辨識類別「所屬封裝的命名」或「程式碼組識」慣例
定義哪些封裝可以使用其它封裝的規則,包括用途是否可為「繼承」、「內含」,或兩者兼備
以上是語言沒有封裝時,要封裝的另類做法(替代方案)
看出
「將程式『設計成語言』」
還是
「『用語言』進行程式設計」
沒有留言:
張貼留言
(什麼是留言欄訊息?)