开闭原则------(转)
引言
本文是新開設的MSDN軟件設計基礎專欄的第一篇文章。我的目的是以不局限于某種特定工具或者某個(軟件工程)周期方法(lifecycle methodology)的方式來討論設計的模式和原則。換言之,我計劃討論一些可以引導你使用任何技術,或者在任何項目中更好地進行設計的基礎知識。
我喜歡以討論開閉原則和其他由 Robert C.Martin 在其著作《敏捷軟件開發,原則,模式和實踐》中所倡導的相關主題作為開始。不要因為在標題中出現“敏捷”一詞就把書合上了,因為這本書實際上完全是關于如何竭力進行優良軟件設計的。
問下你自己:有多少次你是從零開始去寫一個全新的應用程序?又有多少次你是通過將新功能添加到現有代碼庫(codebase)中來作為開始?恐怕大多數的情況下,你是花費了更多的時間將新功能添加到現有代碼庫中吧。
然后再問自己另一個問題:寫全新的代碼容易還是對現有代碼進行修改容易?通常對我來說寫全新的方法和類要比深入舊代碼中,找出我想要修改的部分容易得多。修改舊有代碼增添了破壞已有功能的風險。對于新代碼來說,你通常只需要測試下新實現的功能就可以了。而當你修改舊有代碼時,你不得不既要測試你更改的部分,還要進行一系列的兼容測試,以保證你沒有破壞任何的舊有代碼。
所以,你通常基于現有的代碼庫進行工作,可是寫全新的代碼又比修改舊的代碼容易得多。你難道不想像寫全新代碼一樣多產、輕松地去對現有的代碼庫進行擴展么?這就是開閉原則一展身手的地方了。我來解釋一下開閉原則,它的意思是:軟件實體應該對于擴展是開放的,而對于修改是關閉的。
從字面上看這好像是矛盾的,實際并非如此。它的全部含義就是你應該這樣去構建一個應用程序:可以在對現有代碼做最小修改的同時添加新的功能。我曾經認為開閉原則僅僅是意味著使用插件(plugins),但并不是這么簡單。
你應該避免一個小小的改動就波及了你應用程序中的多個類。這樣會使程序更加脆弱,更傾向于產生向下兼容的問題,并使擴展付出更高的代價。為了隔離變化,你會想要以一種一旦寫好了就再也不需要修改的方式去寫類和方法。
然而你如何構建代碼以實現隔離變化呢?我想說的第一步就是遵循單一責任原則。
單一責任原則
在遵循開閉原則的過程中,我期望能夠寫出一個類或者方法,在以后我回過頭讀它的時候,會很舒服地看到它能完成它的工作并且我也不需要再修改它。你永遠也達不到真正的開閉天堂,但是通過嚴格地遵循與之相關的單一責任原則:一個類應該有并且只有一個更改的理由,你可以非常靠近地接近它。
寫那些永遠也不需要進行修改的類的最簡單方法就是寫一些只能做一件事情的類。通過這種方式,一個類只有在它所確切負責的那件事更改時它才需要更改。代碼1演示了沒有遵循單一責任原則的一個例子。我真的懷疑你正在像這樣設計一個系統,但是最好記得為什么我們不應該這樣去構建代碼。
代碼1. 這個類負責了太多的事
1 public class OrderProcessingModule { 2 public void Process(OrderStatusMessage orderStatusMessage) { 3 // 從配置文件中讀取連接字符串 4 string connectionString = 5 ConfigurationManager.ConnectionStrings["Main"].ConnectionString; 6 7 Order order = null; 8 9 using (SqlConnection connection = 10 new SqlConnection(connectionString)) { 11 // 從數據庫中獲取一些數據 12 order = fetchData(orderStatusMessage, connection); 13 } 14 15 // 向來自于OrderStatusMessage的訂單提交變更 16 updateTheOrder(order); 17 18 // 國際訂單有一些特定的規則 19 if (order.IsInternational) { 20 processInternationalOrder(order); 21 } 22 23 // 對于大批量訂單我們需要特別處理 24 else if (order.LineItems.Count > 10) { 25 processLargeDomesticOrder(order); 26 } 27 // 小的國內訂單也需要區別處理 28 else { 29 processRegularDomesticOrder(order); 30 } 31 32 // 如果訂單準備好了就發貨 33 if (order.IsReadyToShip()) { 34 ShippingGateway gateway = new ShippingGateway(); 35 36 // 將訂單對象提交運送 37 ShipmentMessage message = createShipmentMessageForOrder(order); 38 gateway.SendShipment(message); 39 } 40 } View CodeOrderProcessingModule真是太忙了。它要進行數據訪問、獲取配置文件信息、為訂單處理執行業務規則(可能本身就非常復雜),并且將完成的訂單轉移出貨。通常的情況是,如果你通過這種方式創建了OrderProcessingModule,你將會經常深入到這段代碼中進行修改。而許多系統需求的變化也會造成OrderProcessingModule的代碼產生非常多的變更,讓系統變得岌岌可危并使變更花費很大代價。
除了這種一大塊代碼的方式,你應該遵循單一責任原則,將整個OrderProcessingModule分成一系列相關類的子系統,每一個類完成它自己特定的職責。舉個例子,你可以將所有數據訪問的功能放到一個新類中,管它叫OrderDataService,而把Order的業務邏輯放到另一個類中(我會在下一節進行更詳細的講述)。
根據開閉原則,通過將業務邏輯和數據訪問的職責劃分到不同的類中,你將可以獨立地改變它們中的一個而不會影響到另一個。數據庫物理部署的變化可能將使你把數據訪問部分完全更換掉(對擴展開放),然而訂單邏輯類依然沒有任何改動(對變更關閉)。
單一責任原則的要點不僅僅是寫一些更小的類和方法。它的要點是每一個類應該實現一系列緊密相關的功能。遵循單一責任原則的最簡單辦法就是不斷地問自己是不是這個類的每一個方法和操作都與這個類的名稱直接相關。如果你找到了一些方法與這個類的名稱不相稱,你可以考慮將這些方法移到另一個類中。
責任鏈模式
業務規則在代碼庫(Codebase)的生命周期中相對于系統的任何其他部分可能面臨更多的變化。在OrderProcessingModule類中,基于接收的訂單的類型,對于訂單的處理有不少的分支邏輯:
1 if (order.IsInternational) { 2 processInternationalOrder(order); 3 }else if (order.LineItems.Count > 10) { 4 processLargeDomesticOrder(order); 5 }else { 6 processRegularDomesticOrder(order); 7 } View Code一個真正的訂單處理系統很有可能在業務增長的時候包含更多類型的訂單 -- 并且要考慮很多的特殊情況,比如對于政府或者受到優待的客戶,以及每周一次的特別供應。對你而言,如果能夠書寫并且測試一些新的訂單處理邏輯而不用冒著破壞現有業務規則的風險將會是一件非常有利的事情。
最后,通過代碼2所示的責任鏈模式,對于這個訂單處理的例子你可以更進一步地運用開閉原則。我所做的第一件事就是把所有的分支判斷由OrderProcessingModule中轉移到一個獨立的類中,這個類實現IOrderHandler接口:
1 public interface IOrderHandler { 2 void ProcessOrder(Order order); 3 bool CanProcess(Order order); 4 } View Code代碼2. 引入責任鏈
1 public class OrderProcessingModule { 2 private IOrderHandler[] _handlers; 3 4 public OrderProcessingModule() { 5 _handlers = new IOrderHandler[] { 6 new InternationalOrderHandler(), 7 new SmallDomesticOrderHandler(), 8 new LargeDomesticOrderHandler(), 9 }; 10 } 11 12 public void Process (OrderStatusMessage orderStatusMessage, 13 Order order) { 14 // 對來自OrderStatusMessage的訂單提交變更 15 updateTheOrder(order); 16 17 // 找出知道如何處理這個訂單的第一個IOrderHandler 18 IOrderHandler handler = 19 Array.Find(_handlers, h => h.CanProcess(order)); 20 21 handler.ProcessOrder(order); 22 } 23 24 private void updateTheOrder(Order order) { 25 } 26 } View Code然后我可以對于每種類型的訂單寫一個獨立的IOrderHandler實現,包含著像這樣的基本邏輯,“我知道如何處理這個訂單,讓我來處理它”。
現在對于每種類型的訂單處理邏輯都分隔到了獨立的處理類中(Handler Class),對于某種類型的訂單你可以更改業務規則而不用擔心會破化其他類型訂單的規則。更好的是,你可以添加全新類型的訂單處理程序而只需要對現有代碼做細小的改動。
舉個例子,比如說,以后某個時候,我需要在系統中為政府的訂單添加支持。通過責任鏈模式,我可以添加一個全新的類,叫做GovernmentOrderHandler,這個類實現IOrderHandler接口。一旦我對GovernmentOrderHanlder按期望的方式所進行的工作感到滿意,通過修改OrderProcessingModule類構造函數的一行代碼,我就可以添加這個新的政府訂單處理規則:
1 public OrderProcessingModule() { 2 _handlers = new IOrderHandler[] { 3 new InternationalOrderHandler(), 4 new SmallDomesticOrderHandler(), 5 new LargeDomesticOrderHandler(), 6 new GovernmentOrderHandler(), // 新添加的處理規則 7 }; 8 } View Code通過在訂單處理規則上遵循開閉原則,我使得在系統中添加新類型的訂單處理邏輯容易得多。我能夠用比在一個類中實現各種類型訂單處理所要面臨的小得多的影響其它類型訂單的風險來完成政府訂單規則的添加。
雙重分發
如果以后上面的步驟變得更加復雜該怎么辦呢?如果僅僅依靠多態無法滿足未來可能出現的所有變化呢?我們可以使用稱為雙重分發的模式將變化推入子類中,通過這種方式,我們不需要破壞現有的接口定義。
舉個例子,比如說我們正在構建一個復雜的桌面應用程序,它能一次顯示某種主面板中的一屏(screen)。每次我在程序中打開一個新屏,我需要做很多的事情。我可能需要更改可用的菜單,檢查那些已經打開的屏幕的狀態,做一些定制整個屏幕顯示的事,并且,yeah,以某種方式顯示新屏。
典型地,我會使用某種Model View Presenter(MVP)模式的變體作為我的桌面應用程序的構架,并且我通常會使用程序控制器(Application Controller)模式去協調應用程序中各種不同MVP組(譯注:因為MVP由三個部分組成,所以將每三個部件分為一組)。通過在MVP中使用一個程序控制器(了解MVP的更多信息,可以參考Jean-Paul Boodhoo在MSDN雜志設計模式專欄中關于MVP模式的文章,http://msdn.microsoft.com/en-us/magazine/cc188690.aspx ),激活屏幕可能會包含下面三個基本的部分:
如果我所需要做得只不過簡單地在激活時顯示ApplicationShell中的視圖,代碼可能如同代碼3所示。對于簡單的應用程序來說這完全是可行的,但是如果程序變得更加復雜會怎樣呢?如果在下一個發布版本中,我有新的需求,在某些屏幕激活的時候向主Shell中添加菜單項?如果對于某些而非全部的視圖,我想要在靠著主屏幕左邊際的新面板中顯示額外的控件?
代碼3.一個簡單的基于視圖的應用程序
1 public interface IApplicationShell { 2 void DisplayMainView(object view); 3 } 4 5 public interface IPresenter { 6 // 僅僅提供對于內部Windows窗體用戶控件或者窗體的訪問 7 object View { get; } 8 } 9 10 public class ApplicationController { 11 private IApplicationShell _shell; 12 13 public ApplicationController(IApplicationShell shell) { 14 _shell = shell; 15 } 16 17 public void ActivateScreen(IPresenter presenter) { 18 teardownCurrentScreen(); 19 20 // 設置新屏幕 21 _shell.DisplayMainView(presenter.View); 22 } 23 24 private void teardownCurrentScreen() { 25 // 移除現存屏幕 26 } 27 } View Code我還想讓構架支持嵌入(pluggable),以便于通過簡單的嵌入新的提供器就可以在程序中添加新屏幕,所以現有提供器的抽象應該對于這些新菜單以及左邊面板的構造函數有所了解。然后我還必須更改ApplicationShell或者程序控制器,以對新菜單項以及左邊面板中額外的控件做出響應。
代碼4 顯示了一種可能的解決方案。我向IPrensenter接口中添加了新的屬性用于對新的菜單項以及任何有可能添加到新的左側面板中的控件進行建模。我同樣為這些新的概念向IApplicationShell添加了一些新的成員。然后我在ApplicationController.ActivateScreen(IPresenter)方法中添加了些新代碼
代碼4. 試圖擴展IPresenter
1 public class MenuCommand{ 2 // ... 3 } 4 public interface IApplicationShell{ 5 void DisplayMainView(object view); 6 7 // 新行為 8 void AddMenuCommands(MenuCommand[] commands); 9 void DisplayInExplorerPane(object paneView); 10 } 11 public interface IPresenter 12 { 13 object View { get; } 14 15 // 新屬性 16 MenuCommand[] Commands{ get; } 17 object[] ExplorerViews { get; } 18 } 19 public class ApplicationController { 20 private IApplicationShell _shell; 21 22 public ApplicationController(IApplicationShell shell){ 23 _shell = shell; 24 } 25 26 public void ActivateScreen(IPresenter presenter) 27 { 28 teardownCurrentScreen(); 29 30 // 設置新屏幕 31 _shell.DisplayMainView(presenter.View); 32 33 // 新代碼 34 _shell.AddMenuCommands(presenter.Commands); 35 foreach (var explorerView in presenter.ExplorerViews){ 36 _shell.DisplayInExplorerPane(explorerView); 37 } 38 } 39 40 private void teardownCurrentScreen() 41 { 42 // 移除現有屏幕 43 } 44 } View Code那么,這個解決方案遵守了開閉原則么?一點也沒有。首先,我必須修改IPresenter接口。因為它是一個接口,我必須在代碼庫中修改IPresenter接口的每一個實現,并且為這些新的方法添加一些空的實現,僅僅為了我的代碼可以再一次編譯通過。這通常是一個無法忍受的改變,尤其是當你不能直接控制這些IPresenter實現中的任何一個的時候。關于這部分我們后面再說。
我同樣需要修改ApplicationController類,以使得它知道主ApplicationShell中的屏幕所可能需要的所有新的定制化類型。最后,我需要修改ApplicationShell以使它支持這些新的Shell定制。變化很小,但是同樣,我很有可能不久以后想要再次添加更多的屏幕定制。
在一個真正的應用程序中,ApplicationControll類可能會變得足夠復雜,而不必承擔額外配置ApplicationShell的責任。我們將這些職責置于每個提供器中可能會更好一些。
通過使用一個名為Presenter的抽象類,而不是使用一個接口將會減少修改每個IPresenter接口的實現的痛苦。像代碼5這樣,我可以僅僅向抽象類中添加一些默認的實現。并且在添加新的行為時我不需要修改任何現有的Presenter實現。
代碼5.使用抽象的Presenter
1 public abstract class BasePresenter 2 { 3 public abstract object View { get;} 4 5 // Commands 的默認實現 6 public virtual MenuCommand[] Commands { 7 get{ 8 return new MenuCommand[0]; 9 } 10 } 11 12 // 默認的 ExplorerViews 13 public virtual object[] ExplorerViews{ 14 get{ 15 return new object[0]; 16 } 17 } 18 } View Code最后,還有一種更靠近開閉原則的方式需要說明。除了在IPresenter和BasePresenter中添加Get選擇器,我可以使用雙重分發模式。
幾天前在實際生活中我意外地得到了雙重分發模式的一個演示。我的團隊剛剛轉移到一個新的辦公室中,我們一直在解決網絡上的問題。我們的網絡負責人上周給我打了個電話并且告訴我我的同事應該如何做以連接到VPN。他喋喋不休地向我講述一大堆我不懂的網絡術語,所以我最終把電話給了我的同事,讓他們直接對話。
現在我們也為程序控制器做同樣的事情。并非讓程序控制器去詢問每個提供器哪些需要被顯示在ApplicationShell中,提供器可以簡單地忽略中間人并且告訴ApplicationShell對于每一屏應該顯示些什么(查看 代碼6)。
1 public interface IPresenter { 2 void SetupView(IApplicationShell shell); 3 } 4 5 public class ApplicationController { 6 private IApplicationShell _shell; 7 8 public ApplicationController(IApplicationShell shell) { 9 _shell = shell; 10 } 11 12 public void ActivateScreen(IPresenter presenter) { 13 teardownCurrentScreen(); 14 15 // 使用雙重分發設置新屏幕 16 presenter.SetupView(_shell); 17 } 18 19 private void teardownCurrentScreen() { 20 // 移除現有屏幕 21 } 22 } View Code起初不管我如何做,我都將不得不為了新的定制菜單以及左欄面板中的控件而去修改ApplicationShell,但如果我使用雙重分發策略,對于新的變更,程序控制器和提供器都只需要做非常少的修改。創建額外的屏幕概念(screen concepts)我不再需要修改程序控制器和提供器類。對于新的Shell概念(screen concepts),這個構架是開放的可擴展的,而程序控制器和單獨的提供器類對于修改是關閉的。
Liskov 替換原則
如果我前面所說的,使用開閉原則最通常的做法就是使用多態去用一個全新的類替換程序中現存的一部分。就拿最早的例子來說,你有一個稱為BusinessProcess的類,它的工作是,嗯,執行業務處理。在這個過程中,它需要從數據源中訪問數據:
1 public class BusinessProcess { 2 private IDataSource _source; 3 4 public BusinessProcess(IDataSource source) { 5 _source = source; 6 } 7 } 8 public interface IDataSource { 9 Entity FindEntity(long key); 10 } View Code如果你可以通過實現IDataSource對這個系統進行擴展并且不對BusinessProcess類做任何的修改,那么這個設計就遵循了開閉原則。你可能起初通過一個簡單的基于XML文件的機制,然后轉而使用數據庫進行存儲,隨后添加某種類型的緩存-- 但是你還是不想修改BusinessProcess類。所有這些都是可能的,只要你能夠遵循一個相關的原則:Liskov替代原則。
粗略地說,如果你可以在任何接受抽象的地方使用那個抽象的任何實現,就是在遵循Liskov替換原則。BusinessProcess應該可以使用IDataSource的任何實現而不需要進行修改。BusinessProcess不應該知道IDataSource中除了進行通信的的公共接口以外的任何內部事務。
為了深入這個觀點,代碼7演示了一個沒有遵循Liskov替換原則的例子。這個版本的BusinessProcess類型對于獲取FileSource有著特定的邏輯,同時依賴一些針對于DatabaseSource類的特定錯誤處理邏輯。你應該創建IDataSource的實現以便他們可以處理所有特定的底層需求。通過這樣做可以使 BusinessProcess類像代碼8這樣書寫:
代碼7.沒有對IDataSource進行抽象的BusinessProcess類
1 public class BusinessProcess { 2 private IDataSource _source; 3 4 public BusinessProcess(IDataSource source) { 5 _source = source; 6 } 7 8 public void Process() { 9 long theKey = 112; 10 11 // 針對于 FileSource的特定代碼 12 if (_source is FileSource) { 13 ((FileSource)_source).LoadFile(); 14 } 15 16 try { 17 Entity entity = _source.FindEntity(theKey); 18 } 19 catch (System.Data.DataException) { 20 // 對于DatabaseSource的特定處理程序 21 // 這是 向下轉換(downcast) 的一個例子 22 ((DatabaseSource)_source).CleanUpTheConnection(); 23 } 24 } 25 } View Code代碼8更好的BusinessProcess
1 public class BusinessProcess { 2 private readonly IDataSource _source; 3 4 public BusinessProcess(IDataSource source) { 5 _source = source; 6 } 7 8 public void Process(Message message) { 9 // Process()方法的第一部分 10 11 // 這里不再有針對于某個特定IDataSource實現的代碼 12 Entity entity = _source.FindEntity(message.Key); 13 14 // Process()方法的最后部分 15 } 16 } View Code尋找閉包
記得,如果一個類僅僅依賴于它所交互的另一個類的公共契約(Contract)(譯注:其實就是公共接口),開閉原則只是通過多態來實現。如果在某一部分中,一個抽象了的類必須向下轉換為特定的子類,那么你就沒有遵循開閉原則。
如果一個使用另一個類的類嵌入了關于它所依賴的類的內部工作(比如假設一個方法的返回值總是由大到小排序),那么實際上對于這個依賴你不能替換為另一個實現。因為對于閱讀你代碼的人來說它們是不明顯的,這種類型的對于特定實現的隱式耦合特別有害。不要讓抽象的消費者依賴于除過那個抽象的公共契約的任何東西。
我建議你將開閉原則作為一個設計方向而非一個完全的目標。如果你試圖將你能想到所有可能改變的東西都變成完全可嵌入式的,你很有可能創建一個非常難于工作的過度設計的系統。你可能并非總是試圖寫一些在各個方面都滿足開閉原則的代碼,但是即使只進行到中途也是非常有益的。
轉載于:https://www.cnblogs.com/shao-shao/articles/3450759.html
總結
以上是生活随笔為你收集整理的开闭原则------(转)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java回顾之多线程同步
- 下一篇: js中文正则