一、什么是設計模式
"每一個模式描述了一個在我們周圍不斷重復發生的問題以及該問題的解決方案的核心.這樣,你就能一次又一次地使用該方案而不必做重復的勞動". ---Christopher Alexander
我想告訴大家的是:
能看懂設計模式的代碼,你往往只是懂了皮毛,設計模式真正教給你的是,告訴你的是什么是設計原則,針對哪種變化、哪種場景使用哪種設計模式。
現實中的場景不會讓你在程序設計之初,一上來便套用設計模式,這往往十分不靠譜,更為實際的做法是,"Refactoring to Patterns",結合你身邊的代碼,使用設計模式來重構代碼。
二.分清設計模式與架構模式
剛開始接觸編程的新人往往分不清什么是設計模式,什么是架構模式。甚至只知道架構模式,而不是設計模式,這里羅列出從低到高的三種關系。
1.設計習語Design Idioms
Design Idioms描述與特定編程語言相關的底層模式、技巧、慣用法.
(舉個栗子來說的話,就像OC中的block,Swift中的函數編程、閉包、guard,不一一列舉)
2.設計模式Design Patterns
Design Patterns主要描述的是"類與相互通信的對象之間的組織關系,包括它們的角色、職責、協作方式等方面"
(如Delegate)
3.架構模式Architectural Patterns
Architectural Patterns描述系統中與基本結構組織關系密切的高層模式,包括子系統劃分,職責,以及如何組織它們之間的關系規則
(如MVVM、Redux、VIPPER、響應式Rx等等)
三、什么是GOF
設計模式的經典名著——Design Patterns: Elements ofReusable Object-Oriented Software,中譯本名為《設計模式——可復用面向對象軟件的基礎》的四位作者Erich Gamma、Richard Helm、Ralph Johnson,以及John Vlissides,這四人常被稱為Gang of Four,即四人組,簡稱GoF。
該書描繪了23種經典的設計模式,創立了模式在軟件設計中的地位,通常所說的設計模式隱含地表示"面向對象設計模式".但不并表示就是等于"面向對象設計模式"
四、軟件設計的復雜和解決途徑
伴隨著下面4個不可避免的變化(客戶需求的變化、技術平臺的變化 、開發團隊的變化、市場環境的變化)
那么我們又該如何解決復雜性?
1.分解:人們面對復雜性有一個常見的做法:即分而治之,將大問題分解為多個小問題,將復雜的問題分解為多個簡單的問題
2.抽象:更高層次來講,人們處理復雜性有一個通用的技術,即抽象.由于不能掌握全部復雜的對象,我們選擇忽視它的非本質細節,而去處理泛化和理想化了的對象
五、面向對象設計原則
設計模式的原則才是最重要的,而不像算法,可以去套用,衡量一個程序的好壞,需要我們來對照這些原則的尺子去一一丈量。
變化是復用的天敵。而設計模式的存在是抵御變化,但并不意味沒有變化,而是將變化的范圍逐步縮小。
1、依賴倒置原則(DIP)
- 高層模塊(穩定)不應該依賴于低層模塊(變化),二者都依賴于抽象(穩定)
- 抽象(穩定)不應該依賴于實現細節(變化),實現細節應該依賴于抽象(穩定)
下面為代碼示例:
我們沒有人是生而知之的,在了解是什么是抽象類之前,我們一定都寫過這樣的代碼:
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(){ ... }
}
而這個設計原則告訴我們應該像這樣去思考:
class DrawingBoard{//繪畫板,代表高層模塊
var shapeArray:Array<Shape>?
func onPaint(){
for shape in shapeArray{
shape.Draw();
}
}
}
protocol Shape{//抽象接口,同時也是一種穩定的模塊(高層和低層都依賴抽象類)
func Draw(){ }
}
class Line:Shape{//底層模塊(代表容易變化的模塊)
override func Draw() { }//實現細節應該依賴于抽象
}
class Rect:Shape{//底層模塊(代表容易變化的模塊)
override func Draw() { }//實現細節應該依賴于抽象
}
結構就變成了這樣,看看現在是不是這樣的規則:
高層模塊(穩定)不應該依賴于低層模塊(變化),二者都依賴于抽象(穩定)
抽象(穩定)不應該依賴于實現細節(變化),實現細節應該依賴于抽象(穩定)
2.開放封閉原則(OCP)
- 對擴展開放,對更改封閉.
- 類模塊應該是可擴展的,但是不可修改.
假如我們來一個新的需求時,如果不使用設計模式,我們經常會在原有代碼結構上進行更改。根據這個原則,我們應該避免這種更改,而選擇去擴展。
因為更改的代價往往是十分大的,
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{
}
這種代碼就違反了開放封閉原則,它是在改變代碼,這就意味著這塊代碼需要重新編譯、重新測試、重新部署,改變的代價十分高昂。
我們依舊像之前那樣,重新修改代碼:
class DrawingBoard{//繪畫板,代表高層模塊
var shapeArray:Array<Shape>?
func onPaint(){
for shape in shapeArray{
shape.Draw();
}
}
}
protocol Shape{//抽象接口,同時也是一種穩定的模塊(高層和低層都依賴抽象類)
func Draw(){ }
}
class Line:Shape{//底層模塊(代表容易變化的模塊)
override func Draw() { }//實現細節應該依賴于抽象
}
class Rect:Shape{//底層模塊(代表容易變化的模塊)
override func Draw() { }//實現細節應該依賴于抽象
}
class Circle:Shape{//底層模塊(代表容易變化的模塊)
override func Draw() { }//實現細節應該依賴于抽象
}
第二種方法明顯就是一種以擴展的方式應對新的需求,這就是來自面向對象的智慧。
上圖紅色的部分代表修改&新增。
3.接口隔離原則(ISP)
- 不應該強迫客戶程序依賴它們不用的方法
- 接口應該小而完備
不要去暴露不該暴露的接口,需要我們去考慮什么使用private,internal,public。如果庫開發程序員無節制的public 方法給iOS應用開發程序員,iOS應用開發程序員就會和一些不應該public的接口產生依賴,這樣你的接口就都需要保持穩定。
所以接口應該小而完備。
4.優先使用對象組合,而不是類繼承
- 類繼承通常為"白箱復用",對象組合通常為"黑箱復用"
- 繼承在某種程度上破壞了封裝性,子類父類耦合度高。
- 而對象組合則只要求被組合的對象具有良好定義的接口,耦合度低
許多初學面向對象的程序員都非常喜歡使用繼承。因為面向對象中的繼承更符合我們直觀的世界觀。
就像相較于函數式編程,我們更加會適應命令式編程,因為函數式編程的數學思想不容易被接受,使用命令式編程更明顯地看到如何將真實世界中的對象和程序語言中的對象一一對應。
而關于組合優于繼承的例子,我在裝飾模式一文已經提及。
5.單一職責原則(SRP)
- 一個類應該僅有一個引起它變化的原因
- 變化的方向隱含著類的責任
如果我們一個類充滿了幾十個方法和成員時,這明顯是不正常的,這就代表隱含了多個責任,就像iOS開發中如果將ViewController和View混淆在一起,這明顯是不對的,當隱含多個責任時,很明顯會出問題.
之后寫的文章 橋模式和裝飾模式就會遇到類的責任問題,新手開發者如果輕視責任的問題,甚至會造成整個程序的設計出現問題。
6.Liskov替換原則(LSP)
- 子類必須能夠替換他們的基類(IS-A)
- 繼承表達類型抽象
一般而言,這個原則看起來似乎天經地義,子類替換父類似乎是理所當然的,的確如此,但是不排除有以下情況的出現:
class 樂器{
func 奏樂() -> Void {
}
func 調音() -> Void {
}
}
class 武器:樂器{
override func 奏樂() -> Void {
fatalError("無法奏樂")
}
override func 調音() -> Void {
fatalError("無法調音")
}
}
這個設計看上去似乎十分可笑,但是很多程序員在現實設計時,會發現子類有時候確實就是不應該使用父類的方法,于是直接拋出異常。例子看上去很傻瓜,但當真實投入實踐,有時候我們就會犯糊涂。
這顯然就違背了我們的原則,證明了武器這個類壓根就不應該設計為子類。
7.封裝變化點
- 使用封裝來創建對象之間的分界層,讓設計者可以在一側進行修改,而不會對另外一側產生不良的影響
這里依舊拿庫開發程序員舉例,如果庫開發程序員不封裝變化點,對外接口不是穩定的,而是變化的,那么每次修改,都會導致iOS開發程序員同時進行修改。
這里我拿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是穩定的
8.面向接口編程,而不是針對實現編程
- 客戶程序無需獲知對象的具體類型,只需知道對象所具有的接口。
- 減少系統中各部分的依賴關系,從而實現"高內聚、松耦合"的類型設計方案。