詳解設(shè)計模式六大原則
設(shè)計模式(Design pattern)是一套被反復使用比藻、多數(shù)人知曉的铝量、經(jīng)過分類編目的、代碼設(shè)計經(jīng)驗的總結(jié)银亲。使用設(shè)計模式是為了可重用代碼款违、讓代碼更容易被他人理解型奥、保證代碼可靠性铃绒。 毫無疑問,設(shè)計模式于己于他人于系統(tǒng)都是多贏的吐根;設(shè)計模式使代碼編制真正工程化请梢;設(shè)計模式是軟件工程的基石脈絡赠尾,如同大廈的結(jié)構(gòu)一樣。
借用并改編一下魯迅老師《故鄉(xiāng)》中的一句話毅弧,一句話概括設(shè)計模式: 希望本無所謂有气嫁,無所謂無.這正如coding的設(shè)計模式,其實coding本沒有設(shè)計模式够坐,用的人多了寸宵,也便成了設(shè)計模式
六大原則
設(shè)計模式(面向?qū)ο?有六大原則:
開閉原則(Open Closed Principle,OCP)
里氏代換原則(Liskov Substitution Principle元咙,LSP)
依賴倒轉(zhuǎn)原則(Dependency Inversion Principle梯影,DIP)
接口隔離原則(Interface Segregation Principle,ISP)
合成/聚合復用原則(Composite/Aggregate Reuse Principle庶香,CARP)
最小知識原則(Principle of Least Knowledge甲棍,PLK,也叫迪米特法則)
開閉原則具有理想主義的色彩赶掖,它是面向?qū)ο笤O(shè)計的終極目標感猛。其他幾條七扰,則可以看做是開閉原則的實現(xiàn)方法。 設(shè)計模式就是實現(xiàn)了這些原則陪白,從而達到了代碼復用颈走、增加可維護性的目的。
C# 開閉原則
1.概念:
一個軟件實體如類咱士、模塊和函數(shù)應該對擴展開放疫鹊,對修改關(guān)閉。模塊應盡量在不修改原(是“原”司致,指原來的代碼)代碼的情況下進行擴展拆吆。
2.模擬場景:
在軟件的生命周期內(nèi),因為變化脂矫、升級和維護等原因需要對軟件原有代碼進行修改時枣耀,可能會給舊代碼中引入錯誤,也可能會使我們不得不對整個功能進行重構(gòu)庭再,并且需要原有代碼經(jīng)過重新測試捞奕。
3.Solution:
當軟件需要變化時,盡量通過擴展軟件實體的行為來實現(xiàn)變化拄轻,而不是通過修改已有的代碼來實現(xiàn)變化颅围。
4.注意事項:
通過接口或者抽象類約束擴展,對擴展進行邊界限定恨搓,不允許出現(xiàn)在接口或抽象類中不存在的public方法
參數(shù)類型院促、引用對象盡量使用接口或者抽象類,而不是實現(xiàn)類
抽象層盡量保持穩(wěn)定斧抱,一旦確定即不允許修改
5.開閉原則的優(yōu)點:
可復用性
可維護性
6.開閉原則圖解:
C# 里氏代換原則
1.概述: 派生類(子類)對象能夠替換其基類(父類)對象被調(diào)用
2.概念:
里氏代換原則(Liskov Substitution Principle LSP)面向?qū)ο笤O(shè)計的基本原則之一常拓。 里氏代換原則中說,任何基類可以出現(xiàn)的地方辉浦,子類一定可以出現(xiàn)弄抬。 LSP是繼承復用的基石,只有當衍生類可以替換掉基類宪郊,軟件單位的功能不受到影響時掂恕,基類才能真正被復用,而衍生類也能夠在基類的基礎(chǔ)上增加新的行為弛槐。里氏代換原則是對“開-閉”原則的補充懊亡。實現(xiàn)“開-閉”原則的關(guān)鍵步驟就是抽象化。而基類與子類的繼承關(guān)系就是抽象化的具體實現(xiàn)丐黄,所以里氏代換原則是對實現(xiàn)抽象化的具體步驟的規(guī)范斋配。(源自百度百科)
3.子類為什么可以替換父類的位置孔飒?:
當滿足繼承的時候灌闺,父類肯定存在非私有成員艰争,子類肯定是得到了父類的這些非私有成員(假設(shè),父類的的成員全部是私有的桂对,那么子類沒辦法從父類繼承任何成員甩卓,也就不存在繼承的概念了)。既然子類繼承了父類的這些非私有成員蕉斜,那么父類對象也就可以在子類對象中調(diào)用這些非私有成員逾柿。所以,子類對象可以替換父類對象的位置宅此。
4.C# 里氏代換原則優(yōu)點:
需求變化時机错,只須繼承,而別的東西不會改變父腕。由于里氏代換原則才使得開放封閉成為可能弱匪。這樣使得子類在父類無需修改的話就可以擴展。
5.C# 里氏代換原則Demo:
代碼正文:
namespaceTestApp
{
using System;
classProgram
{
staticvoidMain(string[] args)
{
Transportation transportation =newTransportation();
transportation.Say();
Transportation sedan =newSedan();
sedan.Say();
Console.ReadKey();
}
}
classTransportation
{
publicTransportation()
{
Console.WriteLine("Transportation?");
}
publicvirtualvoidSay()
{
Console.WriteLine("121");
}
}
classSedan:Transportation
{
publicSedan()
{
Console.WriteLine("Transportation:Sedan");
}
publicoverridevoidSay()
{
Console.WriteLine("Sedan");
}
}
classBicycles : Transportation
{
publicBicycles()
{
Console.WriteLine("Transportation:Bicycles");
}
publicoverridevoidSay()
{
Console.WriteLine("Bicycles");
}
}
}
代碼效果:
6.里氏代換原則圖解:
C# 依賴倒轉(zhuǎn)原則
1.概念:
依賴倒置原則(Dependence Inversion Principle)是程序要依賴于抽象接口璧亮,不要依賴于具體實現(xiàn)萧诫。簡單的說就是要求對抽象進行編程,不要對實現(xiàn)進行編程枝嘶,這樣就降低了客戶與實現(xiàn)模塊間的耦合帘饶。
2.C# 依賴倒轉(zhuǎn)原則用處:
有些時候為了代碼復用,一般會把常用的代碼寫成函數(shù)或類庫群扶。這樣開發(fā)新項目時及刻,直接用就行了。比如做項目時大多要訪問數(shù)據(jù)庫竞阐,所以我們就把訪問數(shù)據(jù)庫的代碼寫成了函數(shù)提茁。每次做項目去調(diào)用這些函數(shù)。那么我們的問題來了馁菜。我們要做新項目時茴扁,發(fā)現(xiàn)業(yè)務邏輯的高層模塊都是一樣的,但客戶卻希望使用不同的數(shù)據(jù)庫或存儲住處方式汪疮,這時就出現(xiàn)麻煩了峭火。我們希望能再次利用這些高層模塊,但高層模塊都是與低層的訪問數(shù)據(jù)庫綁定在一起智嚷,沒辦法復用這些高層模塊卖丸。所以不管是高層模塊和低層模塊都應該依賴于抽象,具體一點就是接口或抽象類盏道,只要接口是穩(wěn)定的稍浆,那么任何一個更改都不用擔心了。
3.注意事項:
高層模塊不應該依賴低層模塊。兩個都應該依賴抽象衅枫。
抽象不應該依賴結(jié)節(jié)嫁艇。細節(jié)應該依賴抽象。
4.模擬場景:
場景:
假設(shè)現(xiàn)在需要一個Monitor工具弦撩,去運行一些已有的APP步咪,自動化來完成我們的工作。Monitor工具需要啟動這些已有的APP益楼,并且寫下Log猾漫。
代碼實現(xiàn)1:
namespaceTestLibrary.ExtensionsClass
{
usingSystem;
publicclassAppOne
{
publicboolStart()
{
Console.WriteLine("1號APP開始啟動");
returntrue;
}
publicboolExportLog()
{
Console.WriteLine("1號APP輸出日志");
returntrue;
}
}
publicclassAppTwo
{
publicboolStart()
{
Console.WriteLine("2號APP開始啟動");
returntrue;
}
publicboolExportLog()
{
Console.WriteLine("2號APP輸出日志");
returntrue;
}
}
publicclassMonitor
{
publicenumAppNumber
{
AppOne=1,
AppTwo=2
}
privateAppOne appOne =newAppOne();
privateAppTwo appTwo =newAppTwo();
privateAppNumber number;
publicMonitor(AppNumber number)
{
this.number = number;
}
publicboolStartApp()
{
returnnumber == AppNumber.AppOne ? appOne.Start() : appTwo.Start();
}
publicboolExportAppLog()
{
returnnumber == AppNumber.AppOne ? appOne.ExportLog() : appTwo.ExportLog();
}
}
}
代碼解析1:
在代碼實現(xiàn)1中我們已經(jīng)輕松實現(xiàn)了Monitor去運行已有APP并且寫下LOG的需求。并且代碼已經(jīng)上線了.
春…夏…秋…冬…
春…夏…秋…冬…
春…夏…秋…冬…
就這樣感凤,三年過去了悯周。
一天客戶找上門了,公司業(yè)務擴展了陪竿,現(xiàn)在需要新加3個APP用Monitor自動化队橙。這樣我們就必須得改Monitor。
代碼實現(xiàn)2:
namespaceTestLibrary.ExtensionsClass
{
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Text;
usingSystem.Threading.Tasks;
publicclassMonitor
{
publicenumAppNumber
{
AppOne = 1,
AppTwo = 2,
AppThree = 3,
AppFour = 4,
AppFive = 5
}
privateAppOne appOne =newAppOne();
privateAppTwo appTwo =newAppTwo();
privateAppThree appThree =newAppThree();
privateAppFour appFour =newAppFour();
privateAppFive appFive =newAppFive();
privateAppNumber number;
publicMonitor(AppNumber number)
{
this.number = number;
}
publicboolStartApp()
{
boolresult =false;
if(number == AppNumber.AppOne)
{
result = appOne.Start();
}
elseif(number == AppNumber.AppTwo)
{
result = appTwo.Start();
}
elseif(number == AppNumber.AppThree)
{
result = appThree.Start();
}
elseif(number == AppNumber.AppFour)
{
result = appFour.Start();
}
elseif(number == AppNumber.AppFive)
{
result = appFive.Start();
}
returnresult;
}
publicboolExportAppLog()
{
boolresult =false;
if(number == AppNumber.AppOne)
{
result = appOne.ExportLog();
}
elseif(number == AppNumber.AppTwo)
{
result = appTwo.ExportLog();
}
elseif(number == AppNumber.AppThree)
{
result = appThree.ExportLog();
}
elseif(number == AppNumber.AppFour)
{
result = appFour.ExportLog();
}
elseif(number == AppNumber.AppFive)
{
result = appFive.ExportLog();
}
returnresult;
}
}
}
代碼解析2:
這樣會給系統(tǒng)添加新的相互依賴萨惑。并且隨著時間和需求的推移捐康,會有更多的APP需要用Monitor來監(jiān)測,這個Monitor工具也會被越來越對的if…else撐爆炸庸蔼,而且代碼隨著APP越多解总,越難維護。最終會導致Monitor走向滅亡(下線)姐仅。
介于這種情況花枫,可以用Monitor這個模塊來生成其它的程序,使得系統(tǒng)能夠用在需要的APP上掏膏。OOD給我們提供了一種機制來實現(xiàn)這種“依賴倒置”劳翰。
代碼實現(xiàn)3:
namespaceTestLibrary.ExtensionsClass
{
usingSystem;
publicinterfaceIApp
{
boolStart();
boolExportLog();
}
publicclassAppOne : IApp
{
publicboolStart()
{
Console.WriteLine("1號APP開始啟動");
returntrue;
}
publicboolExportLog()
{
Console.WriteLine("1號APP輸出日志");
returntrue;
}
}
publicclassAppTwo : IApp
{
publicboolStart()
{
Console.WriteLine("2號APP開始啟動");
returntrue;
}
publicboolExportLog()
{
Console.WriteLine("2號APP輸出日志");
returntrue;
}
}
publicclassMonitor
{
privateIApp iapp;
publicMonitor(IApp iapp)
{
this.iapp = iapp;
}
publicboolStartApp()
{
returniapp.Start();
}
publicboolExportAppLog()
{
returniapp.ExportLog();
}
}
}
代碼解析3:
現(xiàn)在Monitor依賴于IApp這個接口,而與具體實現(xiàn)的APP類沒有關(guān)系馒疹,所以無論再怎么添加APP都不會影響到Monitor本身佳簸,只需要去添加一個實現(xiàn)IApp接口的APP類就可以了。
C# 接口隔離原則
1.概念:
客戶端不應該依賴它不需要的接口颖变,類間的依賴關(guān)系應該建立在最小的接口上
2.含義:
接口隔離原則的核心定義生均,不出現(xiàn)臃腫的接口(Fat Interface),但是“小”是有限度的腥刹,首先就是不能違反單一職責原則马胧。
3.模擬場景:
一個OA系統(tǒng),外部只負責提交和撤回工作流衔峰,內(nèi)部負責審核和駁回工作流佩脊。
4.代碼演示:
namespaceTestLibrary.ExtensionsClass
{
usingSystem;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Text;
usingSystem.Threading.Tasks;
publicinterfaceIReview
{
voidReviewWorkFlow();
voidRejectWorkFlow();
}
publicclassReview : IReview
{
publicvoidReviewWorkFlow()
{
Console.WriteLine("開始審核工作流");
}
publicvoidRejectWorkFlow()
{
Console.WriteLine("已經(jīng)駁回工作流");
}
}
publicinterfaceISubmit
{
voidSubmitWorkFlow();
voidCancelWorkFlow();
}
publicclassSubmit : ISubmit
{
publicvoidSubmitWorkFlow()
{
Console.WriteLine("開始提交工作流");
}
publicvoidCancelWorkFlow()
{
Console.WriteLine("已經(jīng)撤銷工作流");
}
}
}
5.代碼解析:
其實接口隔離原則很好理解蛙粘,在上面的例子里可以看出來,如果把OA的外部和內(nèi)部都定義一個接口的話威彰,那這個接口會很大出牧,而且實現(xiàn)接口的類也會變得臃腫。
C# 合成/聚合復用原則
1.概念:
合成/聚合復用原則(Composite/Aggregate Reuse Principle抱冷,CARP)經(jīng)常又叫做合成復用原則崔列。合成/聚合復用原則就是在一個新的對象里面使用一些已有的對象梢褐,使之成為新對象的一部分旺遮;新的對象通過向這些對象的委派達到復用已有功能的目的。它的設(shè)計原則是:要盡量使用合成/聚合盈咳,盡量不要使用繼承耿眉。
2.合成/聚合解析:
聚合概念:
聚合用來表示“擁有”關(guān)系或者整體與部分的關(guān)系。代表部分的對象有可能會被多個代表整體的對象所共享鱼响,而且不一定會隨著某個代表整體的對象被銷毀或破壞而被銷毀或破壞鸣剪,部分的生命周期可以超越整體。例如丈积,Iphone5和IOS筐骇,當Iphone5刪除后,IOS還能存在江滨,IOS可以被Iphone6引用铛纬。
聚合關(guān)系UML類圖:
C# 合成/聚合復用原則
代碼演示:
namespaceTestLibrary.ExtensionsClass
{
classIOS
{
}
classIphone5
{
privateIOS ios;
publicIphone5(IOS ios)
{
this.ios = ios;
}
}
}
合成概念:
合成用來表示一種強得多的“擁有”關(guān)系。在一個合成關(guān)系里唬滑,部分和整體的生命周期是一樣的告唆。一個合成的新對象完全擁有對其組成部分的支配權(quán),包括它們的創(chuàng)建和湮滅等晶密。使用程序語言的術(shù)語來說擒悬,合成而成的新對象對組成部分的內(nèi)存分配、內(nèi)存釋放有絕對的責任稻艰。一個合成關(guān)系中的成分對象是不能與另一個合成關(guān)系共享的懂牧。一個成分對象在同一個時間內(nèi)只能屬于一個合成關(guān)系。如果一個合成關(guān)系湮滅了尊勿,那么所有的成分對象要么自己湮滅所有的成分對象(這種情況較為普遍)要么就得將這一責任交給別人(較為罕見)归苍。例如:水和魚的關(guān)系,當水沒了运怖,魚也不可能獨立存在拼弃。
合成關(guān)系UML類圖:
代碼演示:
namespaceTestLibrary.ExtensionsClass
{
usingSystem;
classFish
{
publicFish CreateFish()
{
Console.WriteLine("一條小魚兒");
returnnewFish();
}
}
classWater
{
privateFish fish;
publicWater()
{
fish =newFish();
}
publicvoidCreateWater()
{
// 當創(chuàng)建了一個水的地方,那這個地方也得放點魚進去
fish.CreateFish();
}
}
}
3.模擬場景:
比如說我們先搖到號(這個比較困難)了摇展,需要為自己買一輛車吻氧,如果4S店里的車默認的配置都是一樣的。那么我們只要買車就會有這些配置,這時使用了繼承關(guān)系:
不可能所有汽車的配置都是一樣的盯孙,所以就有SUV和小轎車兩種(只列舉兩種比較熱門的車型)鲁森,并且使用機動車對它們進行聚合使用。這時采用了合成/聚合的原則:
C# 迪米特法則
1.概念:
一個軟件實體應當盡可能少的與其他實體發(fā)生相互作用振惰。每一個軟件單位對其他的單位都只有最少的知識歌溉,而且局限于那些與本單位密切相關(guān)的軟件單位。迪米特法則的初衷在于降低類之間的耦合骑晶。由于每個類盡量減少對其他類的依賴痛垛,因此,很容易使得系統(tǒng)的功能模塊功能獨立桶蛔,相互之間不存在(或很少有)依賴關(guān)系匙头。迪米特法則不希望類之間建立直接的聯(lián)系。如果真的有需要建立聯(lián)系仔雷,也希望能通過它的友元類來轉(zhuǎn)達蹂析。因此,應用迪米特法則有可能造成的一個后果就是:系統(tǒng)中存在大量的中介類碟婆,這些類之所以存在完全是為了傳遞類之間的相互調(diào)用關(guān)系——這在一定程度上增加了系統(tǒng)的復雜度电抚。
2.模擬場景:
場景:公司財務總監(jiān)發(fā)出指令,讓財務部門的人去統(tǒng)計公司已發(fā)公司的人數(shù)竖共。
一個常態(tài)的編程:(肯定是不符LoD的反例)
UML類圖:
代碼演示:
namespaceTestLibrary.ExtensionsClass
{
usingSystem;
usingSystem.Collections.Generic;
///
/// 財務總監(jiān)
///
publicclassCFO
{
///
/// 財務總監(jiān)發(fā)出指令,讓財務部門統(tǒng)計已發(fā)工資人數(shù)
///
publicvoidDirective(Finance finance)
{
List employeeList =newList();
// 初始化已發(fā)工資人數(shù)
for(inti = 0; i < 500; i++)
{
employeeList.Add(newEmployee());
}
// 轉(zhuǎn)告財務部門開始統(tǒng)計已結(jié)算公司的員工
finance.SettlementSalary(employeeList);
}
}
///
/// 財務部
///
publicclassFinance
{
///
/// 統(tǒng)計已結(jié)算公司的員工
///
publicvoidSettlementSalary(List employeeList)
{
Console.WriteLine(string.Format("已結(jié)算工資人數(shù):{0}", employeeList.Count));
}
}
///
/// 員工
///
publicclassEmployee
{
}
///
/// 主程序
///
publicclassRunner
{
publicstaticvoidmain(String[] args)
{
CFO cfo =newCFO();
// 財務總監(jiān)發(fā)出指令
cfo.Directive(newFinance());
}
}
}
根據(jù)模擬的場景:財務總監(jiān)讓財務部門總結(jié)已發(fā)工資的人數(shù)蝙叛。 財務總監(jiān)和員工是陌生關(guān)系(即總監(jiān)不需要對員工執(zhí)行任何操作)。根據(jù)上述UML圖和代碼解決辦法顯然可以看出肘迎,上述做法違背了LoD法則甥温。
依據(jù)LoD法則解耦:(符合LoD的例子)
UML類圖:
代碼演示:
namespaceTestLibrary.ExtensionsClass
{
usingSystem;
usingSystem.Collections.Generic;
///
/// 財務總監(jiān)
///
publicclassCFO
{
///
/// 財務總監(jiān)發(fā)出指令,讓財務部門統(tǒng)計已發(fā)工資人數(shù)
///
publicvoidDirective(Finance finance)
{
// 通知財務部門開始統(tǒng)計已結(jié)算公司的員工
finance.SettlementSalary();
}
}
///
/// 財務部
///
publicclassFinance
{
privateList employeeList;
//傳遞公司已工資的人
publicFinance(List _employeeList)
{
this.employeeList = _employeeList;
}
///
/// 統(tǒng)計已結(jié)算公司的員工
///
publicvoidSettlementSalary()
{
Console.WriteLine(string.Format("已結(jié)算工資人數(shù):{0}", employeeList.Count));
}
}
///
/// 員工
///
publicclassEmployee
{
}
///
/// 主程序
///
publicclassRunner
{
publicstaticvoidmain(String[] args)
{
List employeeList =newList();
// 初始化已發(fā)工資人數(shù)
for(inti = 0; i < 500; i++)
{
employeeList.Add(newEmployee());
}
CFO cfo =newCFO();
// 財務總監(jiān)發(fā)出指令
cfo.Directive(newFinance(employeeList));
}
}
}
根據(jù)LoD原則我們需要讓財務總監(jiān)和員工之間沒有之間的聯(lián)系。這樣才是遵守了迪米特法則妓布。
博客總結(jié)
想搞懂設(shè)計模式姻蚓,必須先知道設(shè)計模式遵循的六大原則,無論是哪種設(shè)計模式都會遵循一種或者多種原則匣沼。這是面向?qū)ο蟛蛔兊姆▌t狰挡。本文針對的是設(shè)計模式(面向?qū)ο?主要的六大原則展開的講解,并盡量做到結(jié)合實例和UML類圖释涛,幫助大家理解加叁。在后續(xù)的博文中還會跟進一些設(shè)計模式的實例。