设计模式之SOLID原则再回首
? ? 本科階段學過設計模式,那時對設計模式的五大原則——SOLID原則的概念與理解還是比較模糊,此時過去了2年時間,在學習《高級軟件工程》課程中老師又提到了設計模式,課程中還詳細討論了五大原則的過程,這次SOLID原則再回首作者提出了一些更通俗的理解吧~
一. 什么是設計模式?
? ? 那么,什么是設計模式呢?? ? 從廣義角度講設計模式是可解決一類軟件問題并能重復使用的設計方案;
? ? 從狹義角度講設計模式是對被用來在特定場景下解決一般設計問題的類和相互通信的對象的描述,是在類和對象的層次描述的可重復使用的軟件設計問題的解決方案.
? ? 模式體現的是程序整體的構思,也會出現在分析或者是概要設計階段,包括創建型模式、結構型模式和行為型模式.
? ? 模式的核心思想是通過增加抽象層,把變化部分從那些不變部分里分離出來.
? ? 模式的四大基本要素包括:
? ? 1.模式名稱(Pattern Name)
? ? 2.問題(Problem):描述應該在何時使用模式,解釋了設計問題和問題存在的前因后果,可能還描述模式必須滿足的先決條件
? ? 3.解決方案(Solution):描述了設計的組成成分、相互關系及各自的職責和協作方式.模式就像一個模板,可應用于多種場合,所以解決方案并不描述一個具體的設計或實現,而是提供設計問題的抽象描述和解決問題所采用的元素組合(類和對象).
? ? 4.效果(Consequences):描述模式的應用效果及使用模式應權衡的問題.
? ? 其中設計模式 的SOLID原則(Principles)如下:
? ? ? ? 單一職責原則(Single Responsibility)
? ? ? ? 開閉原則(Open Closed)
? ? ? ? 里氏代換原則(Liskov Substitution)
? ? ? ? 接口隔離原原則(Interface Segregation)
? ? ? ? 依賴倒置原則(Dependency Inversion)
? ? 設計模式就是實現了上述原則,從而達到代碼復用、增加可維護性的目的.
二. 單一職責原則
? ? 單一職責原則(Single Responsibility Principle,SRP)思想:就一個類而言,應僅有一個引起它變化的原因(一個類只有一個職責);每一個引起類變化的原因就是一個職責,當類具有多職責時,應該把多余職責分離出去,分別創建類來完成.
? ? 每一個職責都是一個變化的軸線,當需求變化時會反映為類的職責的變化.
? ? 例如Modem可以鏈接dial(撥號連接)\hangup(掛斷撥號)和send(發送數據)\recv(接收數據).
? ? ? ? interface Modem {
? ? ? ? ? ? public void dial(String pno);
? ? ? ? ? ? public void hangup();
? ? ? ? ? ? public void send(char c);
? ? ? ? ? ? public char recv();
? ? ? ? }
? ? Modem類有兩個職責,鏈接管理和數據通信,應該將它們分離.一個類中可以有多個方法,但是一個類只干一件事.
? ? 正如《大話設計模式》中所說:“如果一個類承擔的職責過多,就等于把這些職責耦合在一起,一個職責的變化可能會削弱或抑制這個類完成其他職責的能力.這種耦合會導致脆弱的設計,當變化發生時,設計會遭受到意想不到的破壞.”而且在軟件設計過程中需要完成的內容非常多,發現職責并把那些職責相互分離還是有難度的.
? ? 例子理解:
? ? 比如C#Winform設計俄羅斯方塊游戲時,就需要把界面變化(方塊繪制\擦除)和游戲邏輯(方塊下落\旋轉\碰撞\移動等)分離,至少應該講程序分為兩個類——游戲邏輯類和WinForm窗體界面類.當需要改變界面時,只需修改窗體類,和游戲邏輯無關,而且游戲邏輯是不太容易變化的,將它們分離有利于界面的修改,從而達到代碼復用的目的.
? ??編程過程中我們要在類的職責分離上多思考,做到單一職責,這樣你的代碼才是真正的易維護、易擴展、易復用、靈活多樣.
三. 開閉原則
? ?開閉原則(Open Closed Principle,OCP)思想:對功能擴展是開放的(增加新功能),但對修改原功能代碼是關閉的.在進行擴展(類\模塊\函數都可以擴展)時,不需要對原來的程序進行修改,即已有東西不變,可添加新功能.
? ? 它的好處是靈活可用,可加入新功能,新模塊滿足不斷變化的新需求,由于不修改軟件原來的模塊,不用擔心軟件的穩定性.主要原則包括:
? ? 1.抽象原則
? ? 把系統的所有可能的行為抽象成一個底層,由于可從抽象層導出多個具體類來改變系統行為,因此對于可變部分系統設計對擴展是開放的.
? ? 2.可變性封裝原則
? ? 對系統所有可能發生變化的部分進行評估和分類,每個可變的因素都單獨進行封裝.
? ?正如《大話設計模式》中所說:“我們在做任何系統時,需求都會存在一定的變化,那么如何面對需求變化時,設計軟件可以相對容易修改,而不是把整個項目推倒重來.開閉原則可以讓設計面對需求變化時保持相對穩定,從而使得系統在第一個版本后不斷推出新的版本.”
? ? 但是無論模塊怎么封裝,都會存在一些無法對之封裝的變化,既然不可能完全封裝,設計人員必須對設計的模塊應該對哪種變化封裝做出選擇,他需要通過猜測最有可能發生的變化種類,然后構造抽象來隔離那些變化.通常設計人員會在發生小變化時,就及早去想辦法應對發生更大變化的可能,即等到變化時立即采取行動.當變化發生時就創建抽象來隔離以后發生同類變化.
? ? 例子理解:(源自<<大話設計模式>>)
? ? 香港回歸采用"一國兩制"的體制就是開閉原則的代表例子,社會主義制度是不可更改,但可以增加新的制度實現共和.再如一個計算器的例子,原來客戶端只有加法運算,現在需要添加新的功能減法,此時如果你去修改原來類就會違背"開放-封閉原則",而且當添加很多新的運算時,這樣的代碼很難維護.
? ? 此時你需要重構程序,增加一個抽象類的運算類,通過繼承或多態等隔離具體加法、減法與client耦合,面對需求變化,對程序的改動是通過增加新代碼進行的,而不是更改現有的代碼,這就是開閉原則的精髓.
? ? PS:想到以前圖像處理軟件,每學習一章新處理如圖像灰度、增強、采樣、二值化等,就在原來的基礎上不斷增加新的函數實現,而且最后一個.cpp文件4000多行,簡直無法直視啊!可能還是我實際項目做得比較少的原因,拿到一個項目如何去規劃、設計的理解依然不夠,等工作幾年后我再回來寫一些實際經驗的設計思想文章吧,拭目以待~
? ? 切記:開閉原則是面向對象設計的核心所在,這個原則可以帶來面向對象技術所聲稱的巨大好處,即可維護、可擴展、可服用、靈活性好,開發人員對程序中呈現出頻繁變化的那些部分作出抽象,然而對于應用程序中的每個部分都刻意地進行抽象同樣不是一個好主意,拒絕不成熟的抽象和抽象本身一樣重要.
四. 里氏替換原則
? ?里氏替換原則(Liskov Substitution Principle,LSP)思想:繼承必須確保父類所擁有的性質在子類中仍然成立,當一個子類的實例能夠替換任何其父類的實例時,它們之間才具有is-a-kind-of-A關系.只有當一個子類(派生類)可以替換掉其父類(基類)而軟件功能不受影響時,基類才能被復用,派生類也才能在基類的基礎上增加新的行為.
? ? 其本質是在同一個繼承體系中的對象應該有共同的行為特征.
? ? 換句話說,一個軟件實體如果使用的是一個父類的話,那么一定適用于其子類,而且覺察不出父類對象和子類對象的區別,在軟件中把父類都替換成它的子類,程序的行為沒有變化,子類必須能夠替換掉它們的父類型.
? ? 例子理解:
? ? 父類為貓,子類有黑貓和白貓,如果一個方法使用于貓如捉老鼠,則必然使用于黑貓和白貓.再如<<大話設計模式>>中的問題“企鵝是鳥嗎?”
? ? 生物學:企鵝屬于鳥類
? ? LSP原則:企鵝不屬于鳥類,因為企鵝不會"飛",所以企鵝不能繼承鳥類
? ? 再如動物的例子,動物具有吃、喝、跑、叫等行為,如果需要其他動物也有類似行為,由于它們都繼承于動物,只需要更改實例化的地方,程序其他不許改變.如下圖所示:
五.?依賴倒置原則
? ?依賴倒置原則(Dependence Inversion Principle)思想:高層模塊不應該依賴低層模塊,二者都應該依賴于抽象.? ? ? ? 1.高層模塊只應該包含重要的業務模型和策略選擇
? ? ? ? 2.低層模塊則是不同業務和策略的實現
? ? ? ? 3.高層抽象不依賴高層和底層模塊的具體實現,最多依賴低層的抽象
? ? ? ? 4.低層抽象和實現也只依賴于高層抽象
? ? 換句話說:要依賴于抽象,而不要依賴于具體實現;高層是功能和策略,低層是具體實現;編程程序語言就是需要針對接口編程而不要針對實現編程.
? ? 順便說說與設計模式相關的兩個知識點:
? ? 1.面向對象的四個優點:可維護、可擴展、可服用、靈活性好
? ? 2.高內聚低耦合:高內聚就是一個模塊內各個元素彼此結合緊密程度高,一個軟件模塊只負責一個任務,也就是單一職責原則,提高模塊的獨立性;低耦合就是軟件結構內不同模塊之間相互連接的程度低,模塊與模塊之間盡可能獨立存在,模塊與模塊之間的接口盡量少而且簡單,具有更好的復用性和擴展性.
? ? 例子理解:(強推&參考博客園cbf4life的文章——依賴倒置原則)
? ? 下圖表示司機駕駛奔馳車的類圖.
? ?代碼如下所示,司機通過調用奔馳車run方法開動汽車,通過client場景描述eastmount開奔馳車. //司機類 public class Driver {//司機主要職責駕駛汽車public void drive(Benz benz) {benz.run();} } //奔馳類 public class Benz {public void run() {System.out.printIn("奔馳汽車跑");} } //場景類 public class Client {public static void main(String[] args) {//eastmount開奔馳車Driver eastmount = new Driver();Benz benz = new Benz();eastmount.drive(benz);} }? ? 現在司機eastmount不僅要開奔馳車,還要開寶馬車,又該怎么實現呢?自定義寶馬汽車類BMW,但是不能開,因為eastmount沒有開寶馬車的方法,出現的問題就是:
? ? 司機類和奔馳車類之間是一個緊耦合關系,這導致了系統的可維護性大大降低,增加新的車類如寶馬BWM汽車就需要修改司機類drive()方法,這不是穩定性而是易變的.被依賴者的變化竟然讓依賴者來承擔修改的代價,所以可以通過依賴倒置原則實現.
? ? 如下圖所示,通過建立兩個接口IDriver和ICar,分別定義了司機的職能就是駕駛汽車,通過drive()方法實現;汽車的職能就是運行,通過run()方法實現.
? ? 代碼如下,接口只是一個抽象化的概念,是對一類事物的抽象描述,具體的實現代碼由相應的類來完成,所以還需要定義Driver實現類;同時在IDriver接口中通過傳入ICar接口實現抽象之間的依賴關系,Driver實現類也傳入了ICar接口,具體開的是哪種車需要在高層模型中聲明,具體開發方法在BMW和Benz類中定義. //司機接口 駕駛汽車 抽象 public interface IDriver {public void drive(ICar car); } //司機類 具體實現 public class Driver implements IDriver{//司機的主要職責就是駕駛汽車public void drive(ICar car){car.run();} } //汽車接口 public interface ICar {public void run(); } //奔馳車類 public class Benz implements ICar{public void run(){System.out.println("奔馳汽車跑");} } //寶馬車類 public class BMW implements ICar{public void run(){System.out.println("寶馬汽車跑");} } //實現eastmount開寶馬車 public class Client {public static void main(String[] args) {IDriver eastmount = new Driver();ICar bmw = new BMW();eastmount.drive(bmw);} }? ?由于"抽象不應該依賴細節",所以我們認為抽象(ICar接口)不依賴BMW和Benz兩個實現類(細節),在高層次的模塊中應用都是抽象,Client就屬于高層次業務邏輯,它對于低層次模塊的依賴都是建立在抽象上,eastmount都是以IDriver接口類型進行操作,屏蔽了細節對抽象的影響,現在開不同類型的車,只需要修改業務場景類即可實現:
? ? ICar benz = new Benz();
? ? eastmount.drive(benz);
? ? 依賴倒置其實可以說是面向對象是設計的標志,用哪種語言來編寫程序不重要,如果編寫時考慮的都是如何針對抽象編程而不是針對細節編程,即程序中所有的依賴關系都是終止與抽象類或接口,那就是面向對象的設計,反之則是過程化的設計.
六.?接口隔離原則
? ? 接口隔離原則(Interface Segregation Principle,ISP)思想:多個和客戶相關的接口要好于一個通用接口.如果一個類有幾個使用者,與其讓這個類再如所有使用這需要使用的所有方法,還不如為每個使用者創建一個特定接口,并讓該類分別實現這些接口.
? ? 例子理解:(推薦&引用zhengzhb博客——接口隔離原則)
? ? 如下圖所示,類A依賴接口I中方法1\方法2\方法3,類B是對類A依賴的實現,類C依賴于接口I中的方法方法1\方法4\方法5,類D是對類C依賴的實現.對于類B和類D中都存在用不到的方法,但是由于實現了接口I,所以需要實現這些不用的方法.(代碼見原文鏈接)
? ? 可以發現接口過于臃腫,只要接口中出現的方法,不管對依賴于它的類有沒有用,實現類中都必須去實現這些方法,如類B中方法4和方法5,類D中方法2和方法3,顯然這樣的設計不適用.
? ? 設計如下修改為符合接口隔離原則,必須對接口I進行拆分,拆分為三個接口I1\I2\I3.此時實現類B和類D中方法都是需要使用的方法,這就體現了建立單一接口而不是龐大接口的好處.(源碼見上面的鏈接)
? ?接口隔離原則即盡量細化接口,接口中的方法盡量少,為各個類建立專用的接口,而不是試圖去建立一個龐大而臃腫的接口提供所有依賴于他的類去調用.說到這里,很多人會覺的接口隔離原則跟之前的單一職責原則很相似,其實不然。
? ? 其一,單一職責原則原注重的是職責,而接口隔離原則注重對接口依賴的隔離.
? ? 其二,單一職責原則主要是約束類,其次才是接口和方法,它針對的是程序中的實現和細節;而接口隔離原則主要約束接口,主要針對抽象,針對程序整體框架的構建. ? ???總結:這是一篇關于回顧設計模式SOLID五大原則的文章,我非常喜歡文章中的例子,每個例子都是我精選了描述模式的,通過Modom講述了單一職責原則、加減法計算器講述了開閉原則、企鵝動物講述了里氏替換原則、通過Driver和Car實現了依賴倒置原則,最后講述了接口隔離原則.希望文章對大家有所幫助,尤其是學習設計模式的同學和代碼寫得不太規范或需要重構的同學,如果有錯誤或不足之處,還請海涵~
? ? (By:Eastmount 2014-11-29 晚上8點 http://blog.csdn.net/eastmount/)
? ? 文章參考:
? ? 1.<<高級軟件工程>>設計模式部分課件及老師講述內容
? ? 2.<<大話設計模式>> 程杰著
? ? 3.博客園cbf4life的文章——依賴倒置原則
? ? 4.zhengzhb博客——接口隔離原則 與50位技術專家面對面20年技術見證,附贈技術全景圖
總結
以上是生活随笔為你收集整理的设计模式之SOLID原则再回首的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [Android] SQLite数据库之
- 下一篇: asp.net ajax控件工具集 Au