標籤:組合 資料庫 change 迭代 user XML 填充 事件 存在
由於對象設計的核心是類,所以下面的原則也都基本都是討論類的設計問題,其它類型的元素都比較簡單,基本上也符合大多數這裡列出的原則。 前面我們分析完了對象設計的基本原則,這裡我將重新溫習一下對象設計的核心原則 - SOLID原則。幾乎所有的設計模式都可以看到這些原則的影子。
單一職責原則(SRP):做一個專一的人 單一職責原則的全稱是Single Responsibility Principle,簡稱是SRP。SRP原則的定義很簡單:
即不能存在多於一個導致類變更的原因。簡單的說就是一個類只負責一項職責。
讓一個類僅負責一項職責,如果一個類有多於一項的職責,這是比較脆弱的設計。因為一旦某一項職責發生了改變,需要去更改代碼,那麼有可能會引起其他職責改變。所謂牽一髮而動全身,這顯然是我們所不願意看到的,所以我們會把這個類分拆開來,由兩個類來分別維護這兩個職責,這樣當一個職責發生改變,需要修改時,不會影響到另一個職責。
做且只做好一件事,這一條原則其實不僅僅適用於對象,也同樣適用於函數,變數等一切編程元素。當然在商業模式中,將一件事做到極致就是成功,我個人覺得這一條也還是成立的。 隨便舉個例子,如果大家有深入研究過迭代器的思想的話,那其實就是把儲存資料和遍曆資料的職責分開了,集合只負責實現儲存資料的功能,而迭代器完成遍曆資料的功能。 再說我看到過的一個例子:說有一個輔助類CommonUtil,在這裡面提供了所有不能歸入其他模組的輔助方法,它的結構如下:
public class CommonUtil{ #region Canvas Helpers public void M1() { } //... #endregion #region Screen Helpers public void M2() { } //... #endregion #region Size Helpers public void M3() { } //... #endregion #region Data Helpers public void M4() { } //... #endregion}
各位,你覺得這個類寫的怎麼樣?
這裡面放進了各種不同類型的輔助方法,每個模組有輔助方法需要找地方放的時候,人們都是自覺的找到了這個類,於是這個類在每個Release中都不斷有新成員加入,於是最終變成了一個龐然大物,使用的時候,光看函數列表都夠大家喝一壺了。 我的想法是,為什麼不拆分成4個小類,每個類專門負責某一類型協助工具功能呢?
開放封閉原則(OCP):改造世界大部分不是破壞原來的秩序 開放封閉原則全稱是Open Closed Principle, 簡稱OCP, 該原則的定義是:
軟體實體應該是可擴充,而不可修改的。也就是說,對擴充是開放的,而對修改是封閉的。
這條原則是所有物件導向原則的核心。
軟體設計所追求的第一個目標就是封裝變化、降低耦合,而開放封閉原則正是對這一目標的最直接體現。其它的原則或多或少都是為了這個目標而努力的,例如以Liskov替換原則實現最佳的、正確的繼承層次,就能保證不會違反開放封閉原則。 軟體設計所最求的第二個目標就是重用。這個是繼承機制的核心動力。由於通常來說抽象的東西最穩定,最不容易變化,所以抽象與繼承是實現開閉原則強大的工具,但不是唯一工具,後面我們會說到實現開閉原則的另一個更加強大,更加靈活的工具:組合。
一言以蔽之,繼承與組合是封裝變化,降低耦合的不二法門。能否合理的使用繼承和組合是體現一名碼農水平高低的又一標準。 在實際的代碼中,添加新的功能一般意味著新的對象,一個好的設計也意味著這個新的修改不要大幅度波及現有的對象。這一條理解起來最簡單,實施起來卻是最困難。無數的模式和解耦方法都是為了達到這個目的而誕生的。 看一個經典的例子:
public class Component{ public enum Status { None, Installed, Uninstalled } Status m_status = Status.None; void Do() { switch (m_status) { case Status.None: Console.WriteLine("Error..."); break; case Status.Installed: Console.WriteLine("Hello!"); break; case Status.Uninstalled: Console.WriteLine("Error..."); break; default: break; } }}
我們這裡定義了一個組件,使用者動態載入,載入完了以後程式就可以用了,為了處理方便,我們給組件定義了一些狀態,在不同的狀態下,這個組件有不同的行為,於是就有了上面的代碼:enum定義狀態,函數中使用switch實現路由。
使用switch分支是一種經典的做法,當組件的狀態類型不存在變化的可能時,該段代碼無可挑剔,堪稱完美。 可是在實際項目中,過了一階段,我們發現組件的狀態不夠,比如說我們需要處理組件還未配置時的行為,於是我們在枚舉中加了一個狀態:Configured,然後在switch中加了一個分支。 又過了一階段,我們又發現還需要處理組件還未初始化時的行為,於是我們在枚舉中又加了一個狀態:Initialized,然後在switch中加了一個分支。 至於以後是否還需要別的狀態,我們目前不得而知,應該說還是有可能的。 上面這個行為是嚴重違反開閉原則的,這個不用多講了吧。那麼如何改進呢?
使用我們最強大的工具吧:使用繼承或/和組合封裝變化點。 這裡我們分析一下,該組件存在變化的地方就是組件的狀態,這是一個變化點,對於變化點,對於變化點不要手軟,封印它。
public class ComponentStaus{ public virtual void Do() { }}public class ComponentNone : ComponentStaus{ public override void Do() { Console.WriteLine("Error..."); }}public class ComponentInitialized : ComponentStaus{ public virtual void Do() { Console.WriteLine("Hello!"); }} public class Component{ ComponentStaus m_status = new ComponentNone(); public void ChangeStatus(ComponentStaus newStatus) { m_status = newStatus; } public void Do() { m_status.Do(); }}
在上面的例子中,我們發現了變化點,然後抽象出一個基類放在那,然後使用繼承機制,讓子類去演繹變化。當我們需要添加新的狀態Configured的時候,我們只要添加一個新的子類ComponentConfigured,讓它從ComponentStaus繼承,並重寫Do方法即可。使用的時候,在合適的時機(如事件處理中)把該子類的執行個體傳給Component就可以了,當然也有可能是Component自己處理事件或方法時自己修改該狀態執行個體。
能看到開閉原則的影子嗎?(當然,不要妄想對修改完全封閉,這個是不可能的,就像組件之間零依賴是不可能的一樣)
裡氏替換原則(LSP):長大後,我就成了你 裡氏替換原則全稱Liskov Substitution Principle,簡稱 LSP,它的定義是:
任何基類可以出現的地方,子類一定可以出現。
LSP原則是繼承複用的基石,只有當衍生類別可以替換掉基類,軟體的功能不受到影響時,基類才能真正被複用,而衍生類別也能夠在基類的基礎上增加新的行為。
LSP原則保證了繼承的正確實現。它希望子類不要破壞父類的介面成員。一旦破壞了,就如果人與人之間破壞合約一樣,有時候會很糟糕。 這個原則看起來也很容易,但是卻也很容易和現實中的概念混淆,看個經典的小例子:長方形與正方形問題。 在我們小學學數學的時候,就知道正方形是特殊的長方形,於是寫代碼的時候,自然的正方形類就繼承自長方形了,代碼如下:
public class Program{ static void Main(string[] args) { Rectangle rect = new Rectangle(); rect.setWidth(100); rect.setHeight(20); Console.WriteLine(rect.Area == 100 * 20); Rectangle squ = new Square(); rect.setWidth(100); rect.setHeight(20); Console.WriteLine(squ.Area == 100 * 20); }} class Rectangle{ public double m_width; public double m_height; public virtual void setWidth(double width) { m_width = width; } public virtual void setHeight(double height) { m_height = height; } public double Area { get { return m_width * m_height; } }} class Square : Rectangle{ public override void setWidth(double width) { m_width = width; m_height = width; } public override void setHeight(double height) { m_width = height; m_height = height; }}
很顯然輸入的不是兩個True,根本原因就在於正方形只有長的概念,而沒有長方形所期望的寬的概念,所以長方形中定義了正方形根本沒有的東西,也就是說長方形不應該是正方形的基類。
當一個基類出現了其子類不想要的介面成員時,繼承關係必然是欠缺考慮的繼承,也必然是違反LSP原則的。這個時候要麼把想辦法把基類的那個成員抽象出去,要麼子類再選擇從合適的基類繼承。記住這個思路,在下一個原則我們還會再相見。 此外,當我在小孩玩橡皮鴨子的時候,常常在想:橡皮鴨子能從鴨子繼承嗎?你覺得呢?
介面分離原則(ISP):不要一口吃成胖子 介面分離原則全稱interface segregation principle,簡稱ISP,它的定義是:
不能強迫使用者去依賴那些他們不使用的介面。換句話說,使用多個專門的介面比使用單一的總介面總要好。
這一原則與單一職責原則息息相關,它們對於高內聚的追求是一致的,但是它更加強調了介面的高內聚性。
看個例子,我們有一個服務介面,是這麼定義的:
interface IService{ void GetUser(); void RegisterUser(); void LoadProducts(); void AddProduct(); void AcceptRequest(); void SendResponse();}
因為是面向所有Client的,所以這個介面提供了所有Client需要的方法,比如使用者的操縱,產品的操作,資料轉送的一些操作,每個Client都可能用到其中的一部分服務。
這個設計運行很好,服務端提供一個類Service實現這個介面,而Client,它通過某些網路服務方式擷取到這個介面IService就可以了,然後直接調用相關方法就可以了。 先說第一點,這個介面違反了單一職責原則,一個字,"
拆"。 再說第二點,每個類型的Client只處理一種對象,比如有的Client,如工資系統只處理User,而倉庫系統只處理Product,介面的其它方法對它們沒用,還是一個字,"
拆"。 於是得到下列介面:
interface IUser{ void GetUser(); void RegisterUser();}interface IProduct{ void LoadProducts(); void AddProduct();}interface IPeer{ void AcceptRequest(); void SendResponse();}class Service : IUser, IProduct, IPeer {}
這樣拿到代理對象後,想處理使用者的Client,將該對象轉換成IUser即可,想處理產品的轉換成IProduct即可。
同樣的,試想一下,如果某一天某Service只提供有關使用者的服務,在原先的設計中會怎麼樣?
依賴倒置原則(DIP):抽象的藝術才有生命力 依賴倒置原則全稱Dependence Inversion Principle,簡稱DIP,它的定義有3點含義:
1、高層模組不應該依賴低層模組,兩者都應該依賴於抽象(抽象類別或介面)2、抽象(抽象類別或介面)不應該依賴於細節(具體實作類別)3、細節(具體實作類別)應該依賴抽象
總結起來,這個原則說的就是每個類與別的類互動時,盡量只使用滿足介面規範的抽象類別。為啥?因為抽象類別實現細節幾乎沒有,沒什麼需要變化的。這一條深刻揭示了抽象的生命力,抽象的對象才是最有表達能力的對象,因為它通常是“無形”的,可以隨時填充相關的細節。
直接看一個例子:
public class Program{ static void Main(string[] args) { UI layer = new UI(); layer.SetDataAccessor(new XmlDataAccessor()); layer.Do(); }} class UI{ DataAccessor m_accessor; public void SetDataAccessor(DataAccessor accessor) { m_accessor = accessor; } public void Do() { m_accessor.GetUser(); }} interface DataAccessor{ void GetUser(); void RegisterUser();} class XmlDataAccessor : DataAccessor{ public void GetUser() { } public void RegisterUser() { }}
這裡上遊的組件UI依賴的是DataAccessor這樣的介面,而不是依賴各種具體的子類,如XmlDataAccessor,這樣當想使用其他的資料庫儲存資料的時候,只要增加新的DatabaseDataAccessor之類的新類,然後在設定的時候設定一下就可以了。這種手段,很多人也稱為"依賴注入"。
好了,核心原則說完了,總結一下,似乎就是一句話:"
類要單純,繼承要謹慎,變化要封裝,抽象類別型要多用"。
Java對象設計通用原則之核心原則