面向對象設計模式與原則[1]
By IT Jack 發表於 2007-10-11 13:29:00
設計模式簡介
每一個模式描述了一個在我們周圍不斷重複發生的問題,以及該問題的解決方案的核心。-- Christopher Alexander
設計模式描述了軟件設計過程中某一類常見問題的一般性的解決方案。面向對象設計模式描述了面向對象設計過程中、特定場景下、類與相互通信的對象之間常見的組織關係。
設計模式與面向對象
面向對象設計模式解決的是“類與相互通信的對象之間的組織關係,包括它們的角色、職責、協作方式幾個方面。
面向對象設計模式是“好的面向對象設計”,所謂“好的面向對象設計”是那些可以滿足“應對變化,提高複用”的設計。
面向對象設計模式描述的是軟件設計,因此它是獨立於編程語言的,但是面向對象設計模式的最終實現仍然要使用面向對象編程語言來表達。
面向對象設計模式不像算法技巧,可以照搬照用,它是建立在對“面向對象”純熟、深入的理解的基礎上的經驗性認識。掌握面向對象設計模式的前提是首先掌握“面向對象”!
從編程語言直觀瞭解面向對象
各種面向對象編程語言相互有別,但都能看到它們對面向對象三大機制的支持,即: “封裝、繼承、多態”
– 封裝,隱藏內部實現
– 繼承,複用現有代碼
– 多態,改寫對象行爲
使用面向對象編程語言(如C#),可以推動程序員以面向對象的思維來思考軟件設計結構,從而強化面向對象的編程範式。
C#是一門支持面向對象編程的優秀語言,包括:各種級別的封裝支持;單實現繼承+多接口實現;抽象方法與虛方法重寫。
但OOPL並非面向對象的全部
通過面向對象編程語言(OOPL)認識到的面向對象,並不是面向對象的全部,甚至只是淺陋的面向對象。
OOPL的三大機制“封裝、繼承、多態” 可以表達面向對象的所有概念,但這三大機制本身並沒有刻畫出面向對象的核心精神。換言之,既可以用這三大機制做出“好的面向對象設計”,也可以用這三大機制 做出“差的面向對象設計”。不是使用了面向對象的語言(例如C#),就實現了面向對象的設計與開發!因此我們不能依賴編程語言的面向對象機制,來掌握面向對象。
OOPL沒有回答面向對象的根本性問題——我們爲什麼要使用面向對象?我們應該怎樣使用三大機制來實現“好的面向對象”? 我們應該遵循什麼樣的面向對象原則?
任何一個嚴肅的面向對象程序員(例如C#程序員),都需要系統地學習面向對象的知識,單純從編程語言上獲得的面向對象知識,不能夠勝任面向對象設計與開發。
面向對象設計模式與原則[2]
By IT Jack 發表於 2007-10-11 13:32:00
從一個示例談起
示例場景:
我們需要設計一個人事管理系統,其中的一個功能是對各種不同類型的員工,計算其當月的工資——不同類型的員工,擁有不同的薪金計算制度。
結構化做法
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());
示例場景:
現在需求改變了……隨着客戶公司業務規模的拓展,又出現了更多類型的員工,比如鐘點工、計件工……等等,這對人事管理系統提出了挑戰——原有的程序必須改變。
面向對象設計模式與原則[3]
By IT Jack 發表於 2007-10-11 13:33:00
結構化做法
幾乎所有涉及到員工類型的地方(當然包括“計算工資程序”)都需要做改變……這些代碼都需要重新編譯,重新部署…….
面向對象做法
只需要在新的文件裏增添新的員工類,讓其繼承自Employee抽象類,並重寫GetSalary()方法,然後在 EmployeeFactory.GetEmployee方法中根據相關條件,產生新的員工類型就可以了。其他地方(顯示工資程序、Engineer類、 Sales類等)則不需要做任何改變。
重新認識面向對象
對於前面的例子,從宏觀層面來看,面向對象的構建方式更能適應軟件的變化,能將變化所帶來的影響減爲最小。
從微觀層面來看,面向對象的方式更強調各個類的“責任”,新增員工類型不會影響原來員工類型的實現代碼——這更符合真實的世界,也更能控制變化所影響的範圍,畢竟Engineer類不應該爲新增的“鐘點工”來買單……
對象是什麼?
– 從概念層面講,對象是某種擁有責任的抽象。
– 從規格層面講,對象是一系列可以被其他對象使用的公共接口。
– 從語言實現層面來看,對象封裝了代碼和數據。
有了這些認識之後,怎樣才能設計“好的面向對象”?
– 遵循一定的面向對象設計原則
– 熟悉一些典型的面向對象設計模式
從設計原則到設計模式
針對接口編程,而不是針對實現編程
– 客戶無需知道所使用對象的特定類型,只需要知道對象擁有客戶所期望的接口。
優先使用對象組合,而不是類繼承
– 類繼承通常爲“白箱複用”,對象組合通常爲“黑箱複用”。繼承在某種程度上破壞了封裝性,子類父類耦合度高;而對象組合則只要求被組合的對象具有良好定義的接口,耦合度低。
封裝變化點
– 使用封裝來創建對象之間的分界層,讓設計者可以在分界層的一側進行修改,而不會對另一側產生不良的影響,從而實現層次間的鬆耦合。
使用重構得到模式——設計模式的應用不宜先入爲主,一上來就使用設計模式是對設計模式的最大誤用。沒有一步到位的設計模式。敏捷軟件開發實踐提倡的“Refactoring to Patterns ”是目前普遍公認的最好的使用設計模式的方法。
幾條更具體的設計原則
單一職責原則(SRP):
– 一個類應該僅有一個引起它變化的原因。
開放封閉原則(OCP):
– 類模塊應該是可擴展的,但是不可修改(對擴展開放,對更改封閉)
Liskov 替換原則(LSP):
– 子類必須能夠替換它們的基類
– 依賴倒置原則(DIP):
– 高層模塊不應該依賴於低層模塊,二者都應該依賴於抽象。
– 抽象不應該依賴於實現細節,實現細節應該依賴於抽象。
接口隔離原則(ISP):
– 不應該強迫客戶程序依賴於它們不用的方法。
面向對象設計模式與原則[4]
By IT Jack 發表於 2007-10-11 13:34:00
總結
設計模式描述了軟件設計過程中某一類常見問題的一般性的解決方案。面向對象設計模式描述了面向對象設計過程中、特定場景下、類與相互通信的對象之間常見的組織關係。
深刻理解面向對象是學好設計模式的基礎,掌握一定的面向對象設計原則才能把握面向對象設計模式的精髓,從而實現靈活運用設計模
式。
三大基本面向對象設計原則
– 針對接口編程,而不是針對實現編程
– 優先使用對象組合,而不是類繼承
– 封裝變化點
使用重構得到模式。敏捷軟件開發實踐提倡的“Refactoring to Patterns”是目前普遍公認的最好的使用設計模式的方法。
附:面向對象設計原則
面向對象設計的基石是“開—閉”原則。
“開一閉”原則講的是:一個軟件實體應當對擴展開放,對修改關閉。
這個規則說的是,在設計一個模塊的時候,應當使這個模塊可以在不被修改的前提下被擴展。
從另外一個角度講,就是所謂的“對可變性封裝原則”。“對可變性封裝原則”意味着兩點:
1 .一種可變性不應當散落在代碼的很多角落裏,而應當被封裝到一個對象裏面。同一種可變性的不同表象意味着同一個繼承等級結構中的具體子類。
2.一種可變性不應當與另一種可變性混合在一起。即類圖的繼承結構一般不應超過兩層。
做到“開—閉”原則不是一件容易的事,但是也有很多規律可循,這些規律同樣也是設計原則,它們是實現開—閉原則的工具。
里氏代換原則
里氏代換原則:如果對每一個類型爲T1的對象o1,都有類型爲T2的對象o2,使得以T1定義的所有程序P在所有對象o1都換成o2時,程序P的行爲沒有變化,那麼類型T2是T1的子類型。
即如果一個軟件實體使用的是基類的話那麼也一定適用於子類。但反過來的代換不成立。
如果有兩個具體類A和B之間的關係違反了里氏代換原則,可以在以下兩種重構方案中選擇一種:
1 創建一個新的抽象類C,作爲兩個具體類的超類,將A和B共同的行爲移動到C中,從而解決A和B行爲不完全一致的問題。
2 從B到A的繼承關係改寫爲委派關係。
依賴倒轉原則
依賴倒轉原則講的是:要依賴於抽象,不要依賴於具體。即針對接口編程,不要針對實現編程。針對接口編程的意思是,應當使用接口和抽象類進行變量的類型聲明、參量的類型聲明,方法的返還類型聲明,以及數據類型的轉換等。不要針對實現編程的意思就是說,不應當使用具體類進行變量的類型聲明、參量的類型聲明,方法 的返還類型聲明,以及數據類型的轉換等。
依賴倒轉原則雖然強大,但卻不易實現,因爲依賴倒轉的緣故,對象的創建很可能要使用對象工廠,以避免對具體類的直接引用,此原則的使用還會導致大量的類。維護這樣的系統需要較好的面向對象的設計知識。
此外,依賴倒轉原則假定所有的具體類都是變化的,這也不總是正確的。有一些具體類可能是相當穩定、不會發生變化的,消費這個具體類實例的客戶端完全可以依賴於這個具體類。
面向對象設計模式與原則[5]
By IT Jack 發表於 2007-10-11 13:35:00
接口隔離原則
接口隔離原則講的是:使用多個專門的接口比使用單一的接口要好。從客戶的角度來說:一個類對另外一個類的依賴性應當是建立在最小的接口上的。如果客戶端只需要某一些方法的話,那麼就應當向客戶端提供這些需要的方法,而不要提供不需要的方法。提供接口意味着向客戶端作出承諾,過多的承諾會給系統的維護造成不必 要的負擔。
合成、聚合複用原則
合成、聚合複用原則就是在一個新的對象裏面使用一些已有的對象,使之成爲新對象的一部份,新的對象通過向這些對象的委派達到複用已有功能的目的。這個原則有一個簡短的描述:要儘量使用合成、聚合,儘量不要使用繼承。
合成、聚合有如下好處:
新對象存取成分對象的唯一方法是通過成分對象的接口。
這種複用是黑箱複用,因爲成分對象的內部細節是新對象所看不到的。
這種複用可以在運行時間內動態進行,新對象可以動態的引用與成分對象類型相同的對象。
合成、聚合可以應用到任何環境中去,而繼承只能應用到一些有限環境中去。
導致錯誤的使用合成、聚合與繼承的一個常見原因是錯誤的把“Has-a”關係當作“Is-a”關係。如果兩個類是“Has-a”關係那麼應使用合成、聚合,如果是“Is-a”關係那麼可使用繼承。
迪米特法則
迪米特法則說的是一個對象應該對其它對象有儘可能少的瞭解。即只與你直接的朋友通信,不要跟陌生人說話。如果需要和陌生人通話,而你的朋友與陌生人是朋友,那麼可以將你對陌生人的調用由你的朋友轉發,使得某人只知道朋友,不知道陌生人。換言之,某人會認爲他所調用的是朋友的方法。
以下條件稱爲朋友的條件:
當前對象本身。
以參量的形式傳入到當前對象方法中的對象。
當前對象的實例變量直接引用的對象。
當前對象的實例變量如果是一個聚集,那麼聚集中的元素也都是朋友。
當前對象所創建的對象。
任何一個對象,如果滿足上面的條件之一,就是當前對象的朋友,否則就是陌生人。
迪米特法則的主要用意是控制信息的過載,在將其運用到系統設計中應注意以下幾點:
在類的劃分上,應當創建有弱耦合的類。類之間的耦合越弱,就越有利於複用。
在類的結構設計上,每一個類都應當儘量降低成員的訪問權限。一個類不應當public自己的屬性,而應當提供取值和賦值的方法讓外界間接訪問自己的屬性。
在類的設計上,只要有可能,一個類應當設計成不變類。
在對其它對象的引用上,一個類對其它對象的引用應該降到最低。