一崭放、什么是設(shè)計(jì)模式
"每一個(gè)模式描述了一個(gè)在我們周圍不斷重復(fù)發(fā)生的問題以及該問題的解決方案的核心.這樣,你就能一次又一次地使用該方案而不必做重復(fù)的勞動(dòng)". ---Christopher Alexander
我想告訴大家的是:
能看懂設(shè)計(jì)模式的代碼,你往往只是懂了皮毛,設(shè)計(jì)模式真正教給你的是,告訴你的是什么是設(shè)計(jì)原則,針對哪種變化、哪種場景使用哪種設(shè)計(jì)模式。
現(xiàn)實(shí)中的場景不會(huì)讓你在程序設(shè)計(jì)之初,一上來便套用設(shè)計(jì)模式,這往往十分不靠譜,更為實(shí)際的做法是,"Refactoring to Patterns",結(jié)合你身邊的代碼,使用設(shè)計(jì)模式來重構(gòu)代碼。
二.分清設(shè)計(jì)模式與架構(gòu)模式
剛開始接觸編程的新人往往分不清什么是設(shè)計(jì)模式,什么是架構(gòu)模式斑胜。甚至只知道架構(gòu)模式,而不是設(shè)計(jì)模式,這里羅列出從低到高的三種關(guān)系绷落。
1.設(shè)計(jì)習(xí)語Design Idioms
Design Idioms描述與特定編程語言相關(guān)的底層模式、技巧蛔溃、慣用法.
(舉個(gè)栗子來說的話,就像OC中的block,Swift中的函數(shù)編程、閉包篱蝇、guard,不一一列舉)
2.設(shè)計(jì)模式Design Patterns
Design Patterns主要描述的是"類與相互通信的對象之間的組織關(guān)系,包括它們的角色贺待、職責(zé)、協(xié)作方式等方面"
(如Delegate)
3.架構(gòu)模式Architectural Patterns
Architectural Patterns描述系統(tǒng)中與基本結(jié)構(gòu)組織關(guān)系密切的高層模式,包括子系統(tǒng)劃分,職責(zé),以及如何組織它們之間的關(guān)系規(guī)則
(如MVVM零截、Redux麸塞、VIPPER、響應(yīng)式Rx等等)
三涧衙、什么是GOF
設(shè)計(jì)模式的經(jīng)典名著——Design Patterns: Elements ofReusable Object-Oriented Software哪工,中譯本名為《設(shè)計(jì)模式——可復(fù)用面向?qū)ο筌浖幕A(chǔ)》的四位作者Erich Gamma、Richard Helm弧哎、Ralph Johnson雁比,以及John Vlissides,這四人常被稱為Gang of Four撤嫩,即四人組偎捎,簡稱GoF。
該書描繪了23種經(jīng)典的設(shè)計(jì)模式,創(chuàng)立了模式在軟件設(shè)計(jì)中的地位,通常所說的設(shè)計(jì)模式隱含地表示"面向?qū)ο笤O(shè)計(jì)模式".但不并表示就是等于"面向?qū)ο笤O(shè)計(jì)模式"
四序攘、軟件設(shè)計(jì)的復(fù)雜和解決途徑
伴隨著下面4個(gè)不可避免的變化(客戶需求的變化茴她、技術(shù)平臺(tái)的變化 、開發(fā)團(tuán)隊(duì)的變化程奠、市場環(huán)境的變化)
那么我們又該如何解決復(fù)雜性丈牢?
1.分解:人們面對復(fù)雜性有一個(gè)常見的做法:即分而治之,將大問題分解為多個(gè)小問題,將復(fù)雜的問題分解為多個(gè)簡單的問題
2.抽象:更高層次來講,人們處理復(fù)雜性有一個(gè)通用的技術(shù),即抽象.由于不能掌握全部復(fù)雜的對象,我們選擇忽視它的非本質(zhì)細(xì)節(jié),而去處理泛化和理想化了的對象
五、面向?qū)ο笤O(shè)計(jì)原則
設(shè)計(jì)模式的原則才是最重要的,而不像算法,可以去套用,衡量一個(gè)程序的好壞,需要我們來對照這些原則的尺子去一一丈量梦染。
變化是復(fù)用的天敵赡麦。而設(shè)計(jì)模式的存在是抵御變化,但并不意味沒有變化,而是將變化的范圍逐步縮小。
1帕识、依賴倒置原則(DIP)
- 高層模塊(穩(wěn)定)不應(yīng)該依賴于低層模塊(變化),二者都依賴于抽象(穩(wěn)定)
- 抽象(穩(wěn)定)不應(yīng)該依賴于實(shí)現(xiàn)細(xì)節(jié)(變化),實(shí)現(xiàn)細(xì)節(jié)應(yīng)該依賴于抽象(穩(wěn)定)
下面為代碼示例:
我們沒有人是生而知之的,在了解是什么是抽象類之前,我們一定都寫過這樣的代碼:
class DrawingBoard{//繪畫板,代表高層模塊
var lineArray:Array<Line>泛粹?
var rectArray:Array<Rect>?
func onPaint(){
for lineInstance in lineArray{
event.Graphics.DrawLine(Pens.Red,
lineInstance.leftUp,
lineInstance.width,
lineInstance.height)
}
for rectInstance in rectArray{
event.Graphics.DrawRect(Pens.Red,
rectInstance.leftUp,
rectInstance.width,
rectInstance.height)
}
}
}
class Line{//底層模塊(代表容易變化的模塊)
func Draw(){ ... }
}
class Rect{//底層模塊(代表容易變化的模塊)
func Draw(){ ... }
}
而這個(gè)設(shè)計(jì)原則告訴我們應(yīng)該像這樣去思考:
class DrawingBoard{//繪畫板,代表高層模塊
var shapeArray:Array<Shape>肮疗?
func onPaint(){
for shape in shapeArray{
shape.Draw();
}
}
}
protocol Shape{//抽象接口,同時(shí)也是一種穩(wěn)定的模塊(高層和低層都依賴抽象類)
func Draw(){ }
}
class Line:Shape{//底層模塊(代表容易變化的模塊)
override func Draw() { }//實(shí)現(xiàn)細(xì)節(jié)應(yīng)該依賴于抽象
}
class Rect:Shape{//底層模塊(代表容易變化的模塊)
override func Draw() { }//實(shí)現(xiàn)細(xì)節(jié)應(yīng)該依賴于抽象
}
結(jié)構(gòu)就變成了這樣,看看現(xiàn)在是不是這樣的規(guī)則:
高層模塊(穩(wěn)定)不應(yīng)該依賴于低層模塊(變化),二者都依賴于抽象(穩(wěn)定)
抽象(穩(wěn)定)不應(yīng)該依賴于實(shí)現(xiàn)細(xì)節(jié)(變化),實(shí)現(xiàn)細(xì)節(jié)應(yīng)該依賴于抽象(穩(wěn)定)
2.開放封閉原則(OCP)
- 對擴(kuò)展開放,對更改封閉.
- 類模塊應(yīng)該是可擴(kuò)展的,但是不可修改.
假如我們來一個(gè)新的需求時(shí),如果不使用設(shè)計(jì)模式,我們經(jīng)常會(huì)在原有代碼結(jié)構(gòu)上進(jìn)行更改晶姊。根據(jù)這個(gè)原則,我們應(yīng)該避免這種更改,而選擇去擴(kuò)展。
因?yàn)楦牡拇鷥r(jià)往往是十分大的,
class DrawingBoard{
var lineArray:Array<Line>?
var rectArray:Array<Rect>?
//新的改變需求
var circleArray:Array<Circle>?
func onPaint(event:PaintEventArgs){
//舊代碼
for lineInstance in lineArray{
//同下
}
for rectInstance in rectArray{
//同下
}
//新代碼
for circleInstance in circleArray{
event.Graphics.DrawCircle(Pens.Red,
circleInstance.leftUp,
circleInstance.width,
circleInstance.height)
}
}
}
class Line{//底層模塊(代表容易變化的模塊)
//...
}
class Rect{//底層模塊(代表容易變化的模塊)
//...
}
class Circle{
}
這種代碼就違反了開放封閉原則,它是在改變代碼,這就意味著這塊代碼需要重新編譯伪货、重新測試们衙、重新部署,改變的代價(jià)十分高昂钾怔。
我們依舊像之前那樣,重新修改代碼:
class DrawingBoard{//繪畫板,代表高層模塊
var shapeArray:Array<Shape>?
func onPaint(){
for shape in shapeArray{
shape.Draw();
}
}
}
protocol Shape{//抽象接口,同時(shí)也是一種穩(wěn)定的模塊(高層和低層都依賴抽象類)
func Draw(){ }
}
class Line:Shape{//底層模塊(代表容易變化的模塊)
override func Draw() { }//實(shí)現(xiàn)細(xì)節(jié)應(yīng)該依賴于抽象
}
class Rect:Shape{//底層模塊(代表容易變化的模塊)
override func Draw() { }//實(shí)現(xiàn)細(xì)節(jié)應(yīng)該依賴于抽象
}
class Circle:Shape{//底層模塊(代表容易變化的模塊)
override func Draw() { }//實(shí)現(xiàn)細(xì)節(jié)應(yīng)該依賴于抽象
}
第二種方法明顯就是一種以擴(kuò)展的方式應(yīng)對新的需求,這就是來自面向?qū)ο蟮闹腔邸?/p>
上圖紅色的部分代表修改&新增蒙挑。
3.接口隔離原則(ISP)
- 不應(yīng)該強(qiáng)迫客戶程序依賴它們不用的方法
- 接口應(yīng)該小而完備
不要去暴露不該暴露的接口,需要我們?nèi)タ紤]什么使用private,internal,public宗侦。如果庫開發(fā)程序員無節(jié)制的public 方法給iOS應(yīng)用開發(fā)程序員,iOS應(yīng)用開發(fā)程序員就會(huì)和一些不應(yīng)該public的接口產(chǎn)生依賴,這樣你的接口就都需要保持穩(wěn)定。
所以接口應(yīng)該小而完備忆蚀。
4.優(yōu)先使用對象組合,而不是類繼承
- 類繼承通常為"白箱復(fù)用",對象組合通常為"黑箱復(fù)用"
- 繼承在某種程度上破壞了封裝性,子類父類耦合度高矾利。
- 而對象組合則只要求被組合的對象具有良好定義的接口,耦合度低
許多初學(xué)面向?qū)ο蟮某绦騿T都非常喜歡使用繼承。因?yàn)槊嫦驅(qū)ο笾械睦^承更符合我們直觀的世界觀馋袜。
就像相較于函數(shù)式編程,我們更加會(huì)適應(yīng)命令式編程,因?yàn)楹瘮?shù)式編程的數(shù)學(xué)思想不容易被接受,使用命令式編程更明顯地看到如何將真實(shí)世界中的對象和程序語言中的對象一一對應(yīng)男旗。
而關(guān)于組合優(yōu)于繼承的例子,我在裝飾模式一文已經(jīng)提及。
5.單一職責(zé)原則(SRP)
- 一個(gè)類應(yīng)該僅有一個(gè)引起它變化的原因
- 變化的方向隱含著類的責(zé)任
如果我們一個(gè)類充滿了幾十個(gè)方法和成員時(shí),這明顯是不正常的,這就代表隱含了多個(gè)責(zé)任,就像iOS開發(fā)中如果將ViewController和View混淆在一起,這明顯是不對的,當(dāng)隱含多個(gè)責(zé)任時(shí),很明顯會(huì)出問題.
之后寫的文章 橋模式和裝飾模式就會(huì)遇到類的責(zé)任問題,新手開發(fā)者如果輕視責(zé)任的問題,甚至?xí)斐烧麄€(gè)程序的設(shè)計(jì)出現(xiàn)問題欣鳖。
6.Liskov替換原則(LSP)
- 子類必須能夠替換他們的基類(IS-A)
- 繼承表達(dá)類型抽象
一般而言,這個(gè)原則看起來似乎天經(jīng)地義,子類替換父類似乎是理所當(dāng)然的,的確如此,但是不排除有以下情況的出現(xiàn):
class 樂器{
func 奏樂() -> Void {
}
func 調(diào)音() -> Void {
}
}
class 武器:樂器{
override func 奏樂() -> Void {
fatalError("無法奏樂")
}
override func 調(diào)音() -> Void {
fatalError("無法調(diào)音")
}
}
這個(gè)設(shè)計(jì)看上去似乎十分可笑,但是很多程序員在現(xiàn)實(shí)設(shè)計(jì)時(shí),會(huì)發(fā)現(xiàn)子類有時(shí)候確實(shí)就是不應(yīng)該使用父類的方法,于是直接拋出異常察皇。例子看上去很傻瓜,但當(dāng)真實(shí)投入實(shí)踐,有時(shí)候我們就會(huì)犯糊涂。
這顯然就違背了我們的原則,證明了武器這個(gè)類壓根就不應(yīng)該設(shè)計(jì)為子類泽台。
7.封裝變化點(diǎn)
- 使用封裝來創(chuàng)建對象之間的分界層,讓設(shè)計(jì)者可以在一側(cè)進(jìn)行修改,而不會(huì)對另外一側(cè)產(chǎn)生不良的影響
這里依舊拿庫開發(fā)程序員舉例,如果庫開發(fā)程序員不封裝變化點(diǎn),對外接口不是穩(wěn)定的,而是變化的,那么每次修改,都會(huì)導(dǎo)致iOS開發(fā)程序員同時(shí)進(jìn)行修改什荣。
這里我拿Swift中的官方代碼舉例:
public func assert(
_ condition: @autoclosure () -> Bool,
_ message: @autoclosure () -> String = String(),
file: StaticString = #file, line: UInt = #line
) {
_assertionFailed("assertion failed", message(), file, line,
flags: _fatalErrorFlags())
}
}
_assertionFailed可以是變化的,而assert是穩(wěn)定的
8.面向接口編程,而不是針對實(shí)現(xiàn)編程
- 客戶程序無需獲知對象的具體類型,只需知道對象所具有的接口。
- 減少系統(tǒng)中各部分的依賴關(guān)系,從而實(shí)現(xiàn)"高內(nèi)聚师痕、松耦合"的類型設(shè)計(jì)方案溃睹。