用Swift整理GOF設計模式(一)--掃盲設計模式

一、什么是設計模式

"每一個模式描述了一個在我們周圍不斷重復發(fā)生的問題以及該問題的解決方案的核心.這樣,你就能一次又一次地使用該方案而不必做重復的勞動". ---Christopher Alexander

我想告訴大家的是:
能看懂設計模式的代碼,你往往只是懂了皮毛,設計模式真正教給你的是,告訴你的是什么是設計原則,針對哪種變化、哪種場景使用哪種設計模式。
現(xiàn)實中的場景不會讓你在程序設計之初,一上來便套用設計模式,這往往十分不靠譜,更為實際的做法是,"Refactoring to Patterns",結(jié)合你身邊的代碼,使用設計模式來重構(gòu)代碼。

二.分清設計模式與架構(gòu)模式

剛開始接觸編程的新人往往分不清什么是設計模式,什么是架構(gòu)模式。甚至只知道架構(gòu)模式,而不是設計模式,這里羅列出從低到高的三種關系。
1.設計習語Design Idioms
Design Idioms描述與特定編程語言相關的底層模式、技巧、慣用法.
(舉個栗子來說的話,就像OC中的block,Swift中的函數(shù)編程、閉包、guard,不一一列舉)

2.設計模式Design Patterns
Design Patterns主要描述的是"類與相互通信的對象之間的組織關系,包括它們的角色、職責、協(xié)作方式等方面"
(如Delegate)

3.架構(gòu)模式Architectural Patterns
Architectural Patterns描述系統(tǒng)中與基本結(jié)構(gòu)組織關系密切的高層模式,包括子系統(tǒng)劃分,職責,以及如何組織它們之間的關系規(guī)則
(如MVVM、Redux、VIPPER、響應式Rx等等)

三、什么是GOF

設計模式的經(jīng)典名著——Design Patterns: Elements ofReusable Object-Oriented Software,中譯本名為《設計模式——可復用面向?qū)ο筌浖幕A》的四位作者Erich Gamma、Richard Helm、Ralph Johnson,以及John Vlissides,這四人常被稱為Gang of Four,即四人組,簡稱GoF。

Design Patterns: Elements ofReusable Object-Oriented Software

該書描繪了23種經(jīng)典的設計模式,創(chuàng)立了模式在軟件設計中的地位,通常所說的設計模式隱含地表示"面向?qū)ο笤O計模式".但不并表示就是等于"面向?qū)ο笤O計模式"

四、軟件設計的復雜和解決途徑

伴隨著下面4個不可避免的變化(客戶需求的變化、技術平臺的變化 、開發(fā)團隊的變化、市場環(huán)境的變化)
那么我們又該如何解決復雜性?
1.分解:人們面對復雜性有一個常見的做法:即分而治之,將大問題分解為多個小問題,將復雜的問題分解為多個簡單的問題
2.抽象:更高層次來講,人們處理復雜性有一個通用的技術,即抽象.由于不能掌握全部復雜的對象,我們選擇忽視它的非本質(zhì)細節(jié),而去處理泛化和理想化了的對象

五、面向?qū)ο笤O計原則

設計模式的原則才是最重要的,而不像算法,可以去套用,衡量一個程序的好壞,需要我們來對照這些原則的尺子去一一丈量。
變化是復用的天敵。而設計模式的存在是抵御變化,但并不意味沒有變化,而是將變化的范圍逐步縮小。
1、依賴倒置原則(DIP)

  • 高層模塊(穩(wěn)定)不應該依賴于低層模塊(變化),二者都依賴于抽象(穩(wěn)定)
  • 抽象(穩(wěn)定)不應該依賴于實現(xiàn)細節(jié)(變化),實現(xiàn)細節(jié)應該依賴于抽象(穩(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(){ ... }
   }      

而這個設計原則告訴我們應該像這樣去思考:

   class DrawingBoard{//繪畫板,代表高層模塊
        var shapeArray:Array<Shape>?
        func onPaint(){
             for shape in shapeArray{
                  shape.Draw();
             }
         }
   }
  protocol Shape{//抽象接口,同時也是一種穩(wěn)定的模塊(高層和低層都依賴抽象類)
        func Draw(){  }
   }
  class Line:Shape{//底層模塊(代表容易變化的模塊)
        override func Draw() {  }//實現(xiàn)細節(jié)應該依賴于抽象
   }
  class Rect:Shape{//底層模塊(代表容易變化的模塊)
        override func Draw() {  }//實現(xiàn)細節(jié)應該依賴于抽象
   }      

結(jié)構(gòu)就變成了這樣,看看現(xiàn)在是不是這樣的規(guī)則:
高層模塊(穩(wěn)定)不應該依賴于低層模塊(變化),二者都依賴于抽象(穩(wěn)定)
抽象(穩(wěn)定)不應該依賴于實現(xiàn)細節(jié)(變化),實現(xiàn)細節(jié)應該依賴于抽象(穩(wěn)定)

BB615F2C-4948-4E59-9271-4C0B51B5848E.png

2.開放封閉原則(OCP)

  • 對擴展開放,對更改封閉.
  • 類模塊應該是可擴展的,但是不可修改.

假如我們來一個新的需求時,如果不使用設計模式,我們經(jīng)常會在原有代碼結(jié)構(gòu)上進行更改。根據(jù)這個原則,我們應該避免這種更改,而選擇去擴展。
因為更改的代價往往是十分大的,

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{//抽象接口,同時也是一種穩(wěn)定的模塊(高層和低層都依賴抽象類)
        func Draw(){  }
   }
  class Line:Shape{//底層模塊(代表容易變化的模塊)
        override func Draw() {  }//實現(xiàn)細節(jié)應該依賴于抽象
   }
  class Rect:Shape{//底層模塊(代表容易變化的模塊)
        override func Draw() {  }//實現(xiàn)細節(jié)應該依賴于抽象
   }  
  class Circle:Shape{//底層模塊(代表容易變化的模塊)
        override func Draw() {  }//實現(xiàn)細節(jié)應該依賴于抽象
   }  

第二種方法明顯就是一種以擴展的方式應對新的需求,這就是來自面向?qū)ο蟮闹腔邸?/p>

9155AFEB-E0C8-4CC0-B11A-225AA1B5D46A.png

上圖紅色的部分代表修改&新增。

3.接口隔離原則(ISP)

  • 不應該強迫客戶程序依賴它們不用的方法
  • 接口應該小而完備

不要去暴露不該暴露的接口,需要我們?nèi)タ紤]什么使用private,internal,public。如果庫開發(fā)程序員無節(jié)制的public 方法給iOS應用開發(fā)程序員,iOS應用開發(fā)程序員就會和一些不應該public的接口產(chǎn)生依賴,這樣你的接口就都需要保持穩(wěn)定。
所以接口應該小而完備。

4.優(yōu)先使用對象組合,而不是類繼承

  • 類繼承通常為"白箱復用",對象組合通常為"黑箱復用"
  • 繼承在某種程度上破壞了封裝性,子類父類耦合度高。
  • 而對象組合則只要求被組合的對象具有良好定義的接口,耦合度低

許多初學面向?qū)ο蟮某绦騿T都非常喜歡使用繼承。因為面向?qū)ο笾械睦^承更符合我們直觀的世界觀。
就像相較于函數(shù)式編程,我們更加會適應命令式編程,因為函數(shù)式編程的數(shù)學思想不容易被接受,使用命令式編程更明顯地看到如何將真實世界中的對象和程序語言中的對象一一對應。
而關于組合優(yōu)于繼承的例子,我在裝飾模式一文已經(jīng)提及。

5.單一職責原則(SRP)

  • 一個類應該僅有一個引起它變化的原因
  • 變化的方向隱含著類的責任

如果我們一個類充滿了幾十個方法和成員時,這明顯是不正常的,這就代表隱含了多個責任,就像iOS開發(fā)中如果將ViewController和View混淆在一起,這明顯是不對的,當隱含多個責任時,很明顯會出問題.
之后寫的文章 橋模式裝飾模式就會遇到類的責任問題,新手開發(fā)者如果輕視責任的問題,甚至會造成整個程序的設計出現(xiàn)問題。

6.Liskov替換原則(LSP)

  • 子類必須能夠替換他們的基類(IS-A)
  • 繼承表達類型抽象

一般而言,這個原則看起來似乎天經(jīng)地義,子類替換父類似乎是理所當然的,的確如此,但是不排除有以下情況的出現(xiàn):

class 樂器{
    func 奏樂() -> Void {     
    }
    func 調(diào)音() -> Void {       
    }
}
class 武器:樂器{
    override func 奏樂() -> Void {
        fatalError("無法奏樂")
    }
    override func 調(diào)音() -> Void {
        fatalError("無法調(diào)音")
    }
}

這個設計看上去似乎十分可笑,但是很多程序員在現(xiàn)實設計時,會發(fā)現(xiàn)子類有時候確實就是不應該使用父類的方法,于是直接拋出異常。例子看上去很傻瓜,但當真實投入實踐,有時候我們就會犯糊涂。
這顯然就違背了我們的原則,證明了武器這個類壓根就不應該設計為子類。

7.封裝變化點

  • 使用封裝來創(chuàng)建對象之間的分界層,讓設計者可以在一側(cè)進行修改,而不會對另外一側(cè)產(chǎn)生不良的影響

這里依舊拿庫開發(fā)程序員舉例,如果庫開發(fā)程序員不封裝變化點,對外接口不是穩(wěn)定的,而是變化的,那么每次修改,都會導致iOS開發(fā)程序員同時進行修改。
這里我拿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.面向接口編程,而不是針對實現(xiàn)編程

  • 客戶程序無需獲知對象的具體類型,只需知道對象所具有的接口。
  • 減少系統(tǒng)中各部分的依賴關系,從而實現(xiàn)"高內(nèi)聚、松耦合"的類型設計方案。
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 設計模式匯總 一、基礎知識 1. 設計模式概述 定義:設計模式(Design Pattern)是一套被反復使用、多...
    MinoyJet閱讀 4,073評論 1 15
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,502評論 19 139
  • 設計模式基本原則 開放-封閉原則(OCP),是說軟件實體(類、模塊、函數(shù)等等)應該可以拓展,但是不可修改。開-閉原...
    西山薄涼閱讀 4,071評論 3 14
  • 小時候的冬至是一碗濃濃的疙瘩湯,放學以后,踏著厚厚的雪地往家趕,或鵝毛大雪,或窸窣小雪,總之,像約定好似的,冬至這...
    Sophie朵兒閱讀 1,108評論 2 7
  • 昨夜飲醉亭風, 解衣欲睡還濃。 本以潮聲漸起, 卻是玉嶺橫城。
    亂棋閱讀 176評論 0 0

友情鏈接更多精彩內(nèi)容