版權(quán)說明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請進行舉報或認(rèn)領(lǐng)
文檔簡介
程序設(shè)計開閉原則出處:/en-us/magazine/cc546578.aspx引言本文是新開設(shè)的MSDN軟件設(shè)計基礎(chǔ)專欄的第一篇文章。我的目的是以不局限于某種特定工具或者某個(軟件工程)周期方法(lifecyclemethodology)的方式來討論設(shè)計的模式和原則。換言之,我計劃討論一些可以引導(dǎo)你使用任何技術(shù),或者在任何項目中更好地進行設(shè)計的基礎(chǔ)知識。我喜歡以討論開閉原則和其他由RobertC.Martin在其著作《敏捷軟件開發(fā),原則,模式和實踐》中所倡導(dǎo)的相關(guān)主題作為開始。不要因為在標(biāo)題中出現(xiàn)“敏捷”一詞就把書合上了,因為這本書實際上完全是關(guān)于如何竭力進行優(yōu)良軟件設(shè)計的。問下你自己:有多少次你是從零開始去寫一個全新的應(yīng)用程序?又有多少次你是通過將新功能添加到現(xiàn)有代碼庫(codebase)中來作為開始?恐怕大多數(shù)的情況下,你是花費了更多的時間將新功能添加到現(xiàn)有代碼庫中吧。然后再問自己另一個問題:寫全新的代碼容易還是對現(xiàn)有代碼進行修改容易?通常對我來說寫全新的方法和類要比深入舊代碼中,找出我想要修改的部分容易得多。修改舊有代碼增添了破壞已有功能的風(fēng)險。對于新代碼來說,你通常只需要測試下新實現(xiàn)的功能就可以了。而當(dāng)你修改舊有代碼時,你不得不既要測試你更改的部分,還要進行一系列的兼容測試,以保證你沒有破壞任何的舊有代碼。所以,你通?;诂F(xiàn)有的代碼庫進行工作,可是寫全新的代碼又比修改舊的代碼容易得多。你難道不想像寫全新代碼一樣多產(chǎn)、輕松地去對現(xiàn)有的代碼庫進行擴展么?這就是開閉原則一展身手的地方了。我來解釋一下開閉原則,它的意思是:軟件實體應(yīng)該對于擴展是開放的,而對于修改是關(guān)閉的。從字面上看這好像是矛盾的,實際并非如此。它的全部含義就是你應(yīng)該這樣去構(gòu)建一個應(yīng)用程序:可以在對現(xiàn)有代碼做最小修改的同時添加新的功能。我曾經(jīng)認(rèn)為開閉原則僅僅是意味著使用插件(plugins),但并不是這么簡單。你應(yīng)該避免一個小小的改動就波及了你應(yīng)用程序中的多個類。這樣會使程序更加脆弱,更傾向于產(chǎn)生向下兼容的問題,并使擴展付出更高的代價。為了隔離變化,你會想要以一種一旦寫好了就再也不需要修改的方式去寫類和方法。然而你如何構(gòu)建代碼以實現(xiàn)隔離變化呢?我想說的第一步就是遵循單一責(zé)任原則。單一責(zé)任原則在遵循開閉原則的過程中,我期望能夠?qū)懗鲆粋€類或者方法,在以后我回過頭讀它的時候,會很舒服地看到它能完成它的工作并且我也不需要再修改它。你永遠(yuǎn)也達(dá)不到真正的開閉天堂,但是通過嚴(yán)格地遵循與之相關(guān)的單一責(zé)任原則:一個類應(yīng)該有并且只有一個更改的理由,你可以非??拷亟咏?。寫那些永遠(yuǎn)也不需要進行修改的類的最簡單方法就是寫一些只能做一件事情的類。通過這種方式,一個類只有在它所確切負(fù)責(zé)的那件事更改時它才需要更改。代碼1演示了沒有遵循單一責(zé)任原則的一個例子。我真的懷疑你正在像這樣設(shè)計一個系統(tǒng),但是最好記得為什么我們不應(yīng)該這樣去構(gòu)建代碼。代碼1.這個類負(fù)責(zé)了太多的事publicclassOrderProcessingModule{publicvoidProcess(OrderStatusMessageorderStatusMessage){//從配置文件中讀取連接字符串stringconnectionString=ConfigurationManager.ConnectionStrings["Main"].ConnectionString;
Orderorder=null;using(SqlConnectionconnection=newSqlConnection(connectionString)){//從數(shù)據(jù)庫中獲取一些數(shù)據(jù)order=fetchData(orderStatusMessage,connection);}//向來自于OrderStatusMessage的訂單提交變更updateTheOrder(order);//國際訂單有一些特定的規(guī)則if(order.IsInternational){processInternationalOrder(order);}//對于大批量訂單我們需要特別處理elseif(order.LineItems.Count>10){processLargeDomesticOrder(order);}//小的國內(nèi)訂單也需要區(qū)別處理else{processRegularDomesticOrder(order);}//如果訂單準(zhǔn)備好了就發(fā)貨if(order.IsReadyToShip()){ShippingGatewaygateway=new
ShippingGateway();//將訂單對象提交運送ShipmentMessagemessage=createShipmentMessageForOrder(order);gateway.SendShipment(message);}}OrderProcessingModule真是太忙了。它要進行數(shù)據(jù)訪問、獲取配置文件信息、為訂單處理執(zhí)行業(yè)務(wù)規(guī)則(可能本身就非常復(fù)雜),并且將完成的訂單轉(zhuǎn)移出貨。通常的情況是,如果你通過這種方式創(chuàng)建了OrderProcessingModule,你將會經(jīng)常深入到這段代碼中進行修改。而許多系統(tǒng)需求的變化也會造成OrderProcessingModule的代碼產(chǎn)生非常多的變更,讓系統(tǒng)變得岌岌可危并使變更花費很大代價。除了這種一大塊代碼的方式,你應(yīng)該遵循單一責(zé)任原則,將整個OrderProcessingModule分成一系列相關(guān)類的子系統(tǒng),每一個類完成它自己特定的職責(zé)。舉個例子,你可以將所有數(shù)據(jù)訪問的功能放到一個新類中,管它叫OrderDataService,而把Order的業(yè)務(wù)邏輯放到另一個類中(我會在下一節(jié)進行更詳細(xì)的講述)。根據(jù)開閉原則,通過將業(yè)務(wù)邏輯和數(shù)據(jù)訪問的職責(zé)劃分到不同的類中,你將可以獨立地改變它們中的一個而不會影響到另一個。數(shù)據(jù)庫物理部署的變化可能將使你把數(shù)據(jù)訪問部分完全更換掉(對擴展開放),然而訂單邏輯類依然沒有任何改動(對變更關(guān)閉)。單一責(zé)任原則的要點不僅僅是寫一些更小的類和方法。它的要點是每一個類應(yīng)該實現(xiàn)一系列緊密相關(guān)的功能。遵循單一責(zé)任原則的最簡單辦法就是不斷地問自己是不是這個類的每一個方法和操作都與這個類的名稱直接相關(guān)。如果你找到了一些方法與這個類的名稱不相稱,你可以考慮將這些方法移到另一個類中。責(zé)任鏈模式
業(yè)務(wù)規(guī)則在代碼庫(Codebase)的生命周期中相對于系統(tǒng)的任何其他部分可能面臨更多的變化。在OrderProcessingModule類中,基于接收的訂單的類型,對于訂單的處理有不少的分支邏輯:if(order.IsInternational){processInternationalOrder(order);}elseif(order.LineItems.Count>10){processLargeDomesticOrder(order);}else{processRegularDomesticOrder(order);}一個真正的訂單處理系統(tǒng)很有可能在業(yè)務(wù)增長的時候包含更多類型的訂單一并且要考慮很多的特殊情況,比如對于政府或者受到優(yōu)待的客戶,以及每周一次的特別供應(yīng)。對你而言,如果能夠書寫并且測試一些新的訂單處理邏輯而不用冒著破壞現(xiàn)有業(yè)務(wù)規(guī)則的風(fēng)險將會是一件非常有利的事情。最后,通過代碼2所示的責(zé)任鏈模式,對于這個訂單處理的例子你可以更進一步地運用開閉原則。我所做的第一件事就是把所有的分支判斷由OrderProcessingModule中轉(zhuǎn)移到一個獨立的類中,這個類實現(xiàn)IOrderHandler接口:publicinterfaceIOrderHandler{voidProcessOrder(Orderorder);boolCanProcess(Orderorder);}代碼2.引入責(zé)任鏈publicclassOrderProcessingModule{privateIOrderHandler]]_handlers;publicOrderProcessingModule(){
_handlers=newlOrderHandler[]{newInternationalOrderHandler(),newSmallDomesticOrderHandler(),newLargeDomesticOrderHandler(),};}publicvoidProcess(OrderStatusMessageorderStatusMessage,Orderorder){//對來自O(shè)rderStatusMessage的訂單提交變更updateTheOrder(order);//找出知道如何處理這個訂單的第一個IOrderHandlerIOrderHandlerhandler=Array.Find(_handlers,h=>h.CanProcess(order));handler.ProcessOrder(order);}privatevoidupdateTheOrder(Orderorder){}}
然后我可以對于每種類型的訂單寫一個獨立的lOrderHandler實現(xiàn),包含著像這樣的基本邏輯,“我知道如何處理這個訂單,讓我來處理它”。現(xiàn)在對于每種類型的訂單處理邏輯都分隔到了獨立的處理類中(HandlerClass),對于某種類型的訂單你可以更改業(yè)務(wù)規(guī)則而不用擔(dān)心會破化其他類型訂單的規(guī)則。更好的是,你可以添加全新類型的訂單處理程序而只需要對現(xiàn)有代碼做細(xì)小的改動。舉個例子,比如說,以后某個時候,我需要在系統(tǒng)中為政府的訂單添加支持。通過責(zé)任鏈模式,我可以添加一個全新的類,叫做GovernmentOrderHandler,這個類實現(xiàn)lOrderHandler接口。一旦我對GovernmentOrderHanlder按期望的方式所進行的工作感到滿意,通過修改OrderProcessingModule類構(gòu)造函數(shù)的一行代碼,我就可以添加這個新的政府訂單處理規(guī)則:publicOrderProcessingModule(){_handlers=newIOrderHandler[]{newInternationalOrderHandler(),newSmallDomesticOrderHandler(),newLargeDomesticOrderHandler(),newGovernmentOrderHandler。, //新添加的處理規(guī)則};}通過在訂單處理規(guī)則上遵循開閉原則,我使得在系統(tǒng)中添加新類型的訂單處理邏輯容易得多。我能夠用比在一個類中實現(xiàn)各種類型訂單處理所要面臨的小得多的影響其它類型訂單的風(fēng)險來完成政府訂單規(guī)則的添加。雙重分發(fā)如果以后上面的步驟變得更加復(fù)雜該怎么辦呢?如果僅僅依靠多態(tài)無法滿足未來可能出現(xiàn)的所有變化呢?我們可以使用稱為雙重分發(fā)的模式將變化推入子類中,通過這種方式,我們不需要破壞現(xiàn)有的接口定義。舉個例子,比如說我們正在構(gòu)建一個復(fù)雜的桌面應(yīng)用程序,它能一次顯示某種主面板中的一屏(screen)。每次我在程序中打開一個新屏,我需要做很多的事情。我可能需要更改可用的菜單,檢查那些已經(jīng)打開的屏幕的狀態(tài),做一些定制整個屏幕顯示的事,并且,yeah,以某種方式顯示新屏。典型地,我會使用某種ModelViewPresenter(MVP)模式的變體作為我的桌面應(yīng)用程序的構(gòu)架,并且我通常會使用程序控制器(ApplicationController)模式去協(xié)調(diào)應(yīng)用程序中各種不同MVP組(譯注:因為MVP由三個部分組成,所以將每三個部件分為一細(xì)。通過在MVP中使用一個程序控制器(了解MVP的更多信息,可以參考Jean-PaulBoodhoo在MSDN雜志設(shè)計模式專欄中關(guān)于MVP模式的文章,/en-us/magazine/cc188690.aspx),激活屏幕可能會包含下面三個基本的部分:每一屏(Screen)都有一個提供器(Presenter),每個提供器知道關(guān)于一個特定屏幕的所有事情。應(yīng)用程序的主窗體有一個ApplicationShell。ApplicationShell負(fù)責(zé)以其自己的某種方式顯示位于面板(Panel)或者Tab控件(TabControl)中的獨立視圖(view)。ApplilcationShell也將包含所有的菜單。應(yīng)用程序控制器(ApplicationController)在程序
中扮演交警的角色。它知道ApplicationShell以及在應(yīng)用程序中傳輸?shù)拿恳粋€提供器。應(yīng)用程序控制器控制屏幕激活和反激活的生命周期。如果我所需要做得只不過簡單地在激活時顯示ApplicationShell中的視圖,代碼可能如同代碼3所示。對于簡單的應(yīng)用程序來說這完全是可行的,但是如果程序變得更加復(fù)雜會怎樣呢?如果在下一個發(fā)布版本中,我有新的需求,在某些屏幕激活的時候向主Shell中添加菜單項?如果對于某些而非全部的視圖,我想要在靠著主屏幕左邊際的新面板中顯示額外的控件?代碼3.一個簡單的基于視圖的應(yīng)用程序publicinterfacelApplicationShell{voidDisplayMainView(objectview);}publicinterfaceIPresenter{//僅僅提供對于內(nèi)部Windows窗體用戶控件或者窗體的訪問objectView{get;}}publicclassApplicationController{privateIApplicationShell_shell;publicApplicationController(IApplicationShellshell){_shell=shell;}
publicvoidActivateScreen(IPresenterpresenter){teardownCurrentScreen();//設(shè)置新屏幕_shell.DisplayMainView(presenter.View);}privatevoidteardownCurrentScreen(){//移除現(xiàn)存屏幕}}我還想讓構(gòu)架支持嵌入(pluggable),以便于通過簡單的嵌入新的提供器就可以在程序中添加新屏幕,所以現(xiàn)有提供器的抽象應(yīng)該對于這些新菜單以及左邊面板的構(gòu)造函數(shù)有所了解。然后我還必須更改ApplicationShell或者程序控制器,以對新菜單項以及左邊面板中額外的控件做出響應(yīng)。代碼4顯示了一種可能的解決方案。我向IPrensenter接口中添加了新的屬性用于對新的菜單項以及任何有可能添加到新的左側(cè)面板中的控件進行建模。我同樣為這些新的概念向IApplicationShell添加了一些新的成員。然后我在ApplicationController.ActivateScreen(IPresenter)方法中添加了些新代碼代碼4.試圖擴展IPresenterpublicclassMenuCommand{//...}publicinterfaceIApplicationShell{voidDisplayMainView(objectview);
//新行為voidAddMenuCommands(MenuCommand[]commands);voidDisplayInExplorerPane(objectpaneView);}publicinterfaceIPresenter{objectView{get;}//新屬性MenuCommand[]Commands{get;}object[]ExplorerViews{get;}}publicclassApplicationController{privateIApplicationShell_shell;publicApplicationController(IApplicationShellshell){_shell=shell;}publicvoidActivateScreen(IPresenterpresenter){teardownCurrentScreen();//設(shè)置新屏幕
_shell.DisplayMainView(presenter.View);//新代碼_shell.AddMenuCommands(presenter.Commands);foreach(varexplorerViewinpresenter.ExplorerViews){_shell.DisplayInExplorerPane(explorerView);}}privatevoidteardownCurrentScreen(){//移除現(xiàn)有屏幕}}那么,這個解決方案遵守了開閉原則么?一點也沒有。首先,我必須修改IPresenter接口。因為它是一個接口,我必須在代碼庫中修改IPresenter接口的每一個實現(xiàn),并且為這些新的方法添加一些空的實現(xiàn),僅僅為了我的代碼可以再一次編譯通過。這通常是一個無法忍受的改變,尤其是當(dāng)你不能直接控制這些IPresenter實現(xiàn)中的任何一個的時候。關(guān)于這部分我們后面再說。我同樣需要修改ApplicationController類,以使得它知道主ApplicationShell中的屏幕所可能需要的所有新的定制化類型。最后,我需要修改ApplicationShell以使它支持這些新的Shell定制。變化很小,但是同樣,我很有可能不久以后想要再次添加更多的屏幕定制。
在一個真正的應(yīng)用程序中,ApplicationControll類可能會變得足夠復(fù)雜,而不必承擔(dān)額外配置Applicationshell的責(zé)任。我們將這些職責(zé)置于每個提供器中可能會更好一些。通過使用一個名為Presenter的抽象類,而不是使用一個接口將會減少修改每個IPresenter接口的實現(xiàn)的痛苦。像代碼5這樣,我可以僅僅向抽象類中添加一些默認(rèn)的實現(xiàn)。并且在添加新的行為時我不需要修改任何現(xiàn)有的Presenter實現(xiàn)。代碼5.使用抽象的PresenterpublicabstractclassBasePresenter{publicabstractobjectView{get;}//Commands的默認(rèn)實現(xiàn)publicvirtualMenuCommand[]Commands{get{returnnewMenuCommand[0];}}//默認(rèn)的ExplorerViewspublicvirtualobject[]ExplorerViews{get{returnnewobject[0];}}}最后,還有一種更靠近開閉原則的方式需要說明。除了在IPresenter和BasePresenter中添加Get選擇器,我可以使用雙重分發(fā)模式。
幾天前在實際生活中我意外地得到了雙重分發(fā)模式的一個演示。我的團隊剛剛轉(zhuǎn)移到一個新的辦公室中,我們一直在解決網(wǎng)絡(luò)上的問題。我們的網(wǎng)絡(luò)負(fù)責(zé)人上周給我打了個電話并且告訴我我的同事應(yīng)該如何做以連接到VPN。他喋喋不休地向我講述一大堆我不懂的網(wǎng)絡(luò)術(shù)語,所以我最終把電話給了我的同事,讓他們直接對話?,F(xiàn)在我們也為程序控制器做同樣的事情。并非讓程序控制器去詢問每個提供器哪些需要被顯示在ApplicationShell中,提供器可以簡單地忽略中間人并且告訴ApplicationShell對于每一屏應(yīng)該顯示些什么(查看代碼6)。publicinterfaceIPresenter{voidSetupView(IApplicationShellshell);}publicclassApplicationController{privateIApplicationShell_shell;publicApplicationController(IApplicationShellshell){_shell=shell;}publicvoidActivateScreen(IPresenterpresenter){teardownCurrentScreen();//使用雙重分發(fā)設(shè)置新屏幕presenter.SetupView(_shell);}privatevoidteardownCurrentScreen(){
//移除現(xiàn)有屏幕起初不管我如何做,我都將不得不為了新的定制菜單以及左欄面板中的控件而去修改Applicationshell,但如果我使用雙重分發(fā)策略,對于新的變更,程序控制器和提供器都只需要做非常少的修改。創(chuàng)建額外的屏幕概念(screenconcepts)我不再需要修改程序控制器和提供器類。對于新的Shell概念(screenconcepts),這個構(gòu)架是開放的可擴展的,而程序控制器和單獨的提供器類對于修改是關(guān)閉的。Liskov替換原則如果我前面所說的,使用開閉原則最通常的做法就是使用多態(tài)去用一個全新的類替換程序中現(xiàn)存的一部分。就拿最早的例子來說,你有一個稱為BusinessProcess的類,它的工作是,嗯,執(zhí)行業(yè)務(wù)處理。在這個過程中,它需要從數(shù)據(jù)源中訪問數(shù)據(jù):publicclassBusinessProcess{privateIDataSource_source;publicBusinessProcess(IDataSourcesource){_source=source;}}publicinterfaceIDataSource{EntityFindEntity(longkey);}如果你可以通過實現(xiàn)IDataSource對這個系統(tǒng)進行擴展并且不對BusinessProcess類做任何的修改,那么這個設(shè)計就遵循了開閉原則。你可能起初通過一個簡單的基于XML文件的機制,然后轉(zhuǎn)而使用數(shù)據(jù)庫進行存儲,隨后添加某種類型的緩存--但是你還是不想修改
BusinessProcess類。所有這些都是可能的,只要你能夠遵循一個相關(guān)的原則:Liskov替代原則。粗略地說,如果你可以在任何接受抽象的地方使用那個抽象的任何實現(xiàn),就是在遵循Liskov替換原則°BusinessProcess應(yīng)該可以使用IDataSource的任何實現(xiàn)而不需要進行修改。BusinessProcess不應(yīng)該知道IDataSource中除了進行通信的的公共接口以外的任何內(nèi)部事務(wù)。為了深入這個觀點,代碼7演示了一個沒有遵循Liskov替換原則的例子。這個版本的BusinessProcess類型對于獲取FileSource有著特定的邏輯,同時依賴一些針對于DatabaseSource類的特定錯誤處理邏輯。你應(yīng)該創(chuàng)建IDataSource的實現(xiàn)以便他們可以處理所有特定的底層需求。通過這樣做可以使BusinessProcess類像代碼8這樣書寫:代碼7,沒有對IDataSource進行抽象的BusinessProcess類publicclassBusinessProcess{p
溫馨提示
- 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
- 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
- 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁內(nèi)容里面會有圖紙預(yù)覽,若沒有圖紙預(yù)覽就沒有圖紙。
- 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
- 5. 人人文庫網(wǎng)僅提供信息存儲空間,僅對用戶上傳內(nèi)容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對任何下載內(nèi)容負(fù)責(zé)。
- 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請與我們聯(lián)系,我們立即糾正。
- 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時也不承擔(dān)用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。
最新文檔
- 2024年貴黃高速沿線居民搬遷補償合同
- 2024年鍋爐設(shè)備全面維護保養(yǎng)承包合同書版B版
- 2024年生態(tài)環(huán)保主題廣告制作安裝合同范本2篇
- 2024年版城市供水系統(tǒng)升級改造合同
- 2024標(biāo)準(zhǔn)房屋承租再出租協(xié)議樣本
- 2024年版許可使用合同中的許可范圍與使用期限
- 2024新型環(huán)保材料研發(fā)與應(yīng)用合同
- 2024年甲乙雙方關(guān)于影視作品制作的合同
- 2024年石油化工設(shè)備安裝協(xié)議
- 2024戊己雙方關(guān)于合作開展旅游業(yè)務(wù)的戰(zhàn)略合作協(xié)議
- 供應(yīng)鏈年終工作總結(jié)報告
- 2024年香港中旅國際投資有限公司招聘筆試參考題庫含答案解析
- 醬油培訓(xùn)課件
- Zippo-2024年美版年冊集合
- 重大隱患判定標(biāo)準(zhǔn)培訓(xùn)課件
- 畫法幾何及機械制圖課件
- 棋牌游戲自審自查報告
- 地質(zhì)災(zāi)害風(fēng)險調(diào)查評價項目招標(biāo)文件
- JJF 2088-2023大型蒸汽滅菌器溫度、壓力、時間參數(shù)校準(zhǔn)規(guī)范
- 幼兒園食堂食品安全主體責(zé)任風(fēng)險管控清單(日管控)
- 陜西華縣皮影戲調(diào)研報告
評論
0/150
提交評論