面向對象設計模式與原則
設計模式簡介

每一個模式描述了一個在我們周圍不斷重復發生的問題,以及該問題的解決方案的核心。
-- Christopher Alexander

設計模式描述了軟件設計過程中某一類常見問題的一般性的解決方案。面向對象設計模式描述了面向對象設計過程中、特定場景下、類與相互通信的對象之間常見的組織關系。

GoF23 種設計模式

歷史性著作《設計模式:可復用面向對象軟件的基礎》一書中描述了23種經典面向對象設計模式,創立了模式在軟件設計中的地位。該書
四位作者被人們并稱為Gang of Four (GoF),“四人組”,該書描述的23種經典設計模式又被人們稱為GoF23 種設計模式。

由于《設計模式:可復用面向對象軟件的基礎》一書確定了設計模式的地位,人們通常所說的設計模式隱含地表示“面向對象設計模式”。
但這并不意味“設計模式”就等于“面向對象設計模式”,也不意味著GoF23種模式就表示了所有的“面向對象設計模式”。除了“面向對象設計模式”外,還有其他設計模式。除了GoF23 種設計模式外,還有更多的面向對象設計模式。

GoF23 種設計模式是學習面向對象設計模式的起點,而非終點;本培訓課程的目標是讓學員在建立在有效方法的基礎上,掌握GoF23種設計模式。

設計模式與面向對象

面向對象設計模式解決的是“類與相互通信的對象之間的組織關系,包括它們的角色、職責、協作方式幾個方面。

面向對象設計模式是“好的面向對象設計”,所謂“好的面向對象設計”是那些可以滿足“應對變化,提高復用”的設計。

面向對象設計模式描述的是軟件設計,因此它是獨立于編程語言的,但是面向對象設計模式的最終實現仍然要使用面向對象編程語言來表達,本課程基于C#語言,但實際上它適用于支持.NET框架的所有.NET語言,如Visual Basic.NET、C++/CLI等。

面向對象設計模式不像算法技巧,可以照搬照用,它是建立在對“面向對象”純熟、深入的理解的基礎上的經驗性認識。掌握面向對象設計模式的前提是首先掌握“面向對象”!

從編程語言直觀了解面向對象

各種面向對象編程語言相互有別,但都能看到它們對面向對象三大機制的支持,即: “封裝、繼承、多態”

– 封裝,隱藏內部實現
– 繼承,復用現有代碼
– 多態,改寫對象行為

使用面向對象編程語言(如C#),可以推動程序員以面向對象的思維來思考軟件設計結構,從而強化面向對象的編程范式。

C#是一門支持面向對象編程的優秀語言,包括:各種級別的封裝支持;單實現繼承+多接口實現;抽象方法與虛方法重寫。

但OOPL并非面向對象的全部

通過面向對象編程語言(OOPL)認識到的面向對象,并不是面向對象的全部,甚至只是淺陋的面向對象。

OOPL的三大機制“封裝、繼承、多態” 可以表達面向對象的所有概念,但這三大機制本身并沒有刻畫出面向對象的核心精神。換言之,既可以用這三大機制做出“好的面向對象設計”,也可以用這三大機制 做出“差的面向對象設計”。不是使用了面向對象的語言(例如C#),就實現了面向對象的設計與開發!因此我們不能依賴編程語言的面向對象機制,來掌握面向對象。

OOPL沒有回答面向對象的根本性問題——我們為什么要使用面向對象?我們應該怎樣使用三大機制來實現“好的面向對象”? 我們應該遵循什么樣的面向對象原則?

任何一個嚴肅的面向對象程序員(例如C#程序員),都需要系統地學習面向對象的知識,單純從編程語言上獲得的面向對象知識,不能夠勝任面向對象設計與開發。

從一個示例談起

示例場景:

我們需要設計一個人事管理系統,其中的一個功能是對各種不同類型的員工,計算其當月的工資——不同類型的員工,擁有不同的薪金計算制度。

結構化做法

1.獲得人事系統中所有可能的員工類型
2.根據不同的員工類型所對應的不同的薪金制度,計算其工資
enumEmployeeType
{
Engineer;
Sales;
Manager;

}
// 計算工資程序
if ( type == EmployeeType.Engineer)
{
……
}
else if (type == Employeetype.Sales)
{
……
}

面向對象設計

1.根據不同的員工類型設計不同的類,并使這些類繼承自一個Employee抽象類,其中有一個抽象方法GetSalary。
2.在各個不同的員工類中,根據自己的薪金制度,重寫(override)GetSalary方法。
abstract class Employee
{

public abstract int GetSalary();
}
class Engineer: Employee
{

public override int GetSalary()
{

}
}
class Sales: Employee
{

public override int GetSalary()
{

}
}
// 顯示工資程序
Employee e = emFactory.GetEmployee(id);
MessageBox.Show( e.GetSalary());

示例場景:

現在需求改變了……隨著客戶公司業務規模的拓展,又出現了更多類型的員工,比如鐘點工、計件工……等等,這對人事管理系統提出了挑戰——原有的程序必須改變。

結構化做法

幾乎所有涉及到員工類型的地方(當然包括“計算工資程序”)都需要做改變……這些代碼都需要重新編譯,重新部署…….

面向對象做法

只需要在新的文件里增添新的員工類,讓其繼承自Employee抽象類,并重寫GetSalary()方法,然后在EmployeeFactory.GetEmployee方法中根據相關條件,產生新的員工類型就可以了。其他地方(顯示工資程序、Engineer類、Sales類等)則不需要做任何改變。

重新認識面向對象

對于前面的例子,從宏觀層面來看,面向對象的構建方式更能適應軟件的變化,能將變化所帶來的影響減為最小

從微觀層面來看,面向對象的方式更強調各個類的“責任”,新增員工類型不會影響原來員工類型的實現代碼——這更符合真實的世界,也
更能控制變化所影響的范圍,畢竟Engineer類不應該為新增的“鐘點工”來買單……

對象是什么?

– 從概念層面講,對象是某種擁有責任的抽象。
– 從規格層面講,對象是一系列可以被其他對象使用的公共接口。
– 從語言實現層面來看,對象封裝了代碼和數據。

有了這些認識之后,怎樣才能設計“好的面向對象”?

– 遵循一定的面向對象設計原則
– 熟悉一些典型的面向對象設計模式

從設計原則到設計模式

針對接口編程,而不是針對實現編程

– 客戶無需知道所使用對象的特定類型,只需要知道對象擁有客戶所期望的接口。

優先使用對象組合,而不是類繼承

– 類繼承通常為“白箱復用”,對象組合通常為“黑箱復用”。繼承在某種程度上破壞了封裝性,子類父類耦合度高;而對象組合則只要求被組合的對象具有良好定義的接口,耦合度低。

封裝變化點

– 使用封裝來創建對象之間的分界層,讓設計者可以在分界層的一側進行修改,而不會對另一側產生不良的影響,從而實現層次間的松耦合。

使用重構得到模式——設計模式的應用不宜先入為主,一上來就使用設計模式是對設計模式的最大誤用。沒有一步到位的設計模式。敏捷軟件開發實踐提倡的“Refactoring to Patterns ”是目前普遍公認的最好的使用設計模式的方法。

幾條更具體的設計原則

單一職責原則(SRP):

– 一個類應該僅有一個引起它變化的原因。

開放封閉原則(OCP):

– 類模塊應該是可擴展的,但是不可修改(對擴展開放,對更改封閉)

Liskov 替換原則(LSP):
– 子類必須能夠替換它們的基類

. 依賴倒置原則(DIP):

– 高層模塊不應該依賴于低層模塊,二者都應該依賴于抽象。

– 抽象不應該依賴于實現細節,實現細節應該依賴于抽象。

接口隔離原則(ISP):

– 不應該強迫客戶程序依賴于它們不用的方法。

總結

設計模式描述了軟件設計過程中某一類常見問題的一般性的解決方 案。面向對象設計模式描述了面向對象設計過程中、特定場景下、類與相互通信的對象之間常見的組織關系。

深刻理解面向對象是學好設計模式的基礎,掌握一定的面向對象設計原則才能把握面向對象設計模式的精髓,從而實現靈活運用設計模
式。

三大基本面向對象設計原則

– 針對接口編程,而不是針對實現編程
– 優先使用對象組合,而不是類繼承
– 封裝變化點