如何使用設計模式

SSBun發表於2017-12-13

Iterator模式 (迭代器)

####一個一個遍歷 一個集合類可以遵守 Iterator 協議,並實現一個 Iterator,一般包含 next()方法來獲取下一個值,藉此來實現資料的遍歷。我們來實現一個簡單的陣列的迭代器例子:

    protocol IteratorProtocol {
        associatedtype Element  // 關聯型別
        func next() -> Self.Element? // 返回下一個數值
    }
    
    class Iterator: IteratorProtocol {
        var currentIndex: Int = 0
        var datas: [Element]
        
        init(datas: [Element]) {
            self.datas = datas
        }
        
        func next() -> Self.Element? {
            if self.datas.count > currentIndex {
                defer {
                    self.currentIndex += 1
                }
                return self.datas[currentIndex]
            } else {
                return nil
            }
        }
    }
    
複製程式碼

可以將遍歷和實現分離開來,如果以後集合型別發生改變,只要同樣為其實現了 Iteraltor協議的方法,就無需大批量的修改程式碼。

Adapter模式 (介面卡)

####加個介面卡以便複用 就像要將一個220v的電源,轉換為10v的輸出一樣,我們需要一個介面卡來進行輸入和輸出的轉換。如果一個類Number有createNumber()的方法,而有一個協議Print需要實現一個printNumber() 的方法,我們可以通過一箇中間類NumberPrint繼承至Number並實現Print 協議,這樣的話我們就可以使用 createNumber()的方法,來實現printNumber()方法。這無疑是提高了程式碼的複用,並且降低了程式碼的耦合度,我們不需要修改原來的型別就能夠進行新的擴充套件。這種方法叫做類介面卡模式

    class Number {
        func createNumber() -> Int { // 生成一個隨機數
            return arc4random() % 999   
        }
    }
    
    protocol Print {
        // 需要列印出一個數字,這裡舉例只是列印數字。實際專案中可能需要的是一個複雜的操作
        func printNumber()
        
    }
    
    // 一個繼承於`Number`並遵守`Print`的介面卡類,它可以藉助父類的方法來實現協議的方法
    class NumberPrint: Number, Print { 
        func printNumber() {
            print(super.createNumber()) // 藉助父類的方法實現,來實現協議的功能
        }      
    }
複製程式碼

還有一種模式是物件介面卡模式,如果需要轉換的不是協議Print,而是類Print。對於無法進行多繼承的語言來說,無法建立一箇中間類NumberPrint 同時繼承兩個類。 這是我們需要進行一些轉變,我們可以建立一個繼承於Print 的類 NumberPrint,而NumberPrint 中有一個屬性是 Number 的例項變數,我們可以通過呼叫例項變數的物件方法來實現Print 中的 printNumber()方法。

    // 此時的 Print 是一個類
    class NumberPrint: Print {
        var number: Number = Number()
        
        // 重寫父類的方法,利用屬性`number` 來實現父類的方法
        func printNumber() {
            let n = self.number.createNumber()
            print(n)
        }
    }
複製程式碼

當需要擴充套件類的功能時,無需對現有的類進行修改。有時一個類已經被多地方複用,並確認可靠,如果冒然進行修改可能會出現意想不到的錯誤,而且又要進行一輪新的測試,所以可以使用介面卡進行處理。 還有當進行版本更新的時候,可能會有版本適配的要求,這個時候如何對舊的程式碼進行適配就很重要了,我們可以通過介面卡模式,對新的功能進行轉換,而保留舊的介面。

交給子類

Template Method模式(模板模式)

將具體的實現交給子類

如果一個類的邏輯程式碼在父類中,而其具體的方法需要子類來實現,我們就可以稱之為模板模式。其實父類就是一個抽象類,比如我們定義一個類Person,它實現了eat()、run()和sleep()方法。我們可以在父類中定義它的執行順序。但是具體的方法是如何吃、如何跑、如何睡覺的我們要交給子類來實現,畢竟Child(小孩)和Adult(成人)的習慣是不同的,不是嗎?

使用模板模式,當我們遇到了執行邏輯的改變時,我們不需要去修改各個子類,我們只需要修改抽象類就行了。並且無論是任何子類都可以被父類執行。

    class Person {
    // 控制人的行為,先吃放後跑步,最後睡覺
        func action() {            eat()
            run()
            sleep()
        }
        
        // 要交給子類實現的方法
        func run(){}
        func eat(){}
        func sleep()
    }
    
    class Child: Person {
        func eat() {
            print("drink milk")
        }
        func run() {
            print("run 10m")
        }
        func sleep() {
            print("sleep for 10 hours")
        }
    }
    
    class Adult: Person {
        func eate() {
            print("eat food")
        }
        
        func run() {
            print("run 10km")
        }
        func sleep() {
            print("sleep for 6 hours")
        }
    }
複製程式碼

上面的例子中我們使用一個抽象類來描述Person。你也可以使用協議Protocol來達到同樣的目的,不同的操作實現,相同的操作步驟。

我們這裡將邏輯程式碼放到了父類中,把具體實現放到了子類中,但是實際使用時,如何分配父類和子類之間的程式碼的處理級別就需要大家們自己斟酌了,如果父類中實現的太多,就失去了模板的意義,降低了父類的靈活性。但是父類實現的太少也會導致子類中的程式碼重複,所以一切看大家的感覺了。

Factory Method模式(工廠模式)

將例項的生成交給子類

什麼是工廠模式呢?通過一個Factory 生成一個 Product, 而 Factory和 Product 的實現是由子類來實現的,使用了模板模式。所以你可以定製自己的工廠生產出你想要的例項。 舉個例子來說,我們把一種麵點機器作為Factory,而生成的麵點是類Product。麵點機器生成麵點用方法create(),而麵點可以被吃掉有方法eat()

    protocol Factory { // 定義了一個工廠
        func create() -> Product
    }
    
    protocol Product { // 定義了一個產品
        func eat()
    }
複製程式碼

但是我們並沒有定義機器如何生產麵點以及麵點該如何被吃掉,這個時候我們建立類Dumpling Machine(餃子機器)繼承於Factory 並實現方法create(). 接下來我們實現類Dumpling 繼承至Product 並實現了方法eat()。然後我們就可以通過DumplingMachinecreate() 來生成Dumpling的例項了。

    class DumplingMachine: Factory {
        func create() -> Dumpling {
            return Dumpling()
        }
    }
    
    class Dumpling: Product {
        func eat() {
            ... // 實現怎麼吃餃子
        }
    }
複製程式碼

這就是工廠模式的使用,那我們什麼時候使用工廠模式呢。它又有什麼好處呢? 工廠模式將框架具體實現分離開來了,當我們實現自己的框架(Factory,Product)時,我們直接定義相應的方法邏輯和屬性結構,抽象的描述框架的行為。而開發者可以通過框架來實現自己的工廠和產品。當我們使用工廠方法生成例項時,我們不需要考慮框架內部的實現,只需要實現預先約定的方法和屬性就OK了。

例如Object-C中的NSNumber 就使用了工廠模式。 我們可以通過NSNumber 生成不同的數值型別。

使用工廠模式的時候,生成例項我們有幾個實現的方法。<1> 指定其為抽象方法,這樣如何子類不實現的話,編譯器就會報錯。如果語法不支援定義抽象方法,這種方法就無法使用了。**<2>為其實現預設的實現,當子類沒有繼承父類的方法時。我們可以預設實現此過程,不過我不推薦使用這種方法,因為往往預設的實現都是不符合實際需要的,如果是忘了子類實現也無法通過編譯器來提醒。<3>**在父類的實現裡,丟擲異常。並提示使用者必須實現子類的方法。這樣如果使用者忘記在子類中實現這個方法,就會丟擲異樣,防止進一步的錯誤方法,也能夠提示使用者的錯誤發生在哪裡。

生成例項

Singleton 模式(單例模式)

####只有一個例項 當你想要保證在任何情況下都只有一個例項,程式對外也表現出只有一個例項的時候,你就可以選擇使用單例模式來實現你的類。 單例模式,顧名思義就是這個類,只會生成一個例項變數。當你想要新例項化一個類的時候,它要不返回給你唯一的例項,要不就丟擲異常。通常一個單例模式的類,都只有一個類似於shareInstance()類方法用來獲取唯一的例項。具體的實現方法,根據各個語言的不同而做改變。 基本上,就是在類中建立一個例項變數,這個例項變數一旦被初始化就無法被改變和銷燬。而獲取例項的方法,總是返回這是例項就 ok 了。

在某些情況下,類似於程式的視窗,一定是隻有一個的。這個時候,為了方便管理這個視窗,我們就可以實現一個單例來處理具體的事物。亦或是在程式啟動以後需要,一個始終純在的類例項,來進行公共資料的處理和傳遞。 在 GUI 上也有可以用到的地方,比如一個介面上,只能同時出現一個的彈窗。你不必在顯示一個的時候,去關閉另一個,你只需要在這個地方顯示一個彈窗,另一個就會消失,畢竟只有一個例項。(你可以不必在其他的類中持有此例項,在想用的時候,直接獲取單例就 OK 了)

Prototype 模式(原型模式)

通過複製生成例項

一般情況下,我們在生成一個例項的時候。都是使用初始化方法,根據一個類來生成一個例項。當時在某些情況下,我們可能並不想根據一個類來例項化一個物件,這個時候我們可以通過一個例項來生成另一個例項,這種複製的方式,我們就稱之為 Prototype 模式。 什麼情況下我們可以選擇使用 prototype 模式呢?大概有以下三種情況。

  • **當物件功能相近而種類又太多的時候。**如果使用類的話,會建立很多的類。如果是在一個類中,建立不同功能的例項,然後通過例項複製來進行後續的物件生成。(有一種把例項當類用的感覺...)
  • **太過複雜,無法通過類來生成例項。**比如一個畫板上筆的運動軌跡,通過一個物件來記錄。在另一個地方要生成一個和這個筆的運動軌跡完全一樣的物件時,你很難通過一個類來例項化出這個物件。而對這個物件的複製就能夠很容易的做到。
  • **寫框架時,想要將類和例項解耦。**這個在實現的過程中,你就可要體會到,它的複用性很高,類之間是沒有耦合的。

實現的過程大概如此: 我們通過建立一個類Manager,並實現register()createClone()方法,用來註冊和生成例項。然後是協議Product,它定義了方法use()copySelf()。繼承此協議的類需要實現use()來實現如何使用執行,copySelf()則是複製自己以生成新的例項。 另外則是是具體的類了,我們舉個例子是類State,它用來描述一個人身體的狀況,State 需要遵守協議Product並實現方法use()copySelf()。我們生成一個一個state 例項,並使用Manager 來註冊此例項,然後可以通過 createClone()來進行復制,下面是簡化的虛擬碼

這裡我們可以把Manager想象為一個庫房,因為通過例項來生成例項,畢竟要有一個母體。而這個母體不能像是一個類一樣隨時可以呼叫,所以我們需要把它放在一個地方,在我們想用的時候,隨時可以使用,並且防止母體被意外修改,Manager 只是提供了複製的方法,你不能獲取和修改母體

protocol Product {
    func copySelf() -> Product
    func use()
}

class Manager { // 用來管理可以自我複製的例項
    private var datas: [String: Product] = []
    public func register(name: String, object: Product)
    {
        datas[string] = object
    }
    public func createClone(name: String) -> Product {
        return datas[name].copySelf
    }
}

class State: Product {
    var height: Double = 0
    var weight: Double = 0
    var age: Int = 0
    func run() {
       print("run")
    }
    func copySelf() -> State {
        return self.copy()
    }
    func use() {
        run()
    } 
}
複製程式碼

Builder 模式 (構建模式)

組裝複雜的例項

有時候,當我們在構建一個複雜的模組的時候,我們需要將其拆分出來。形成一個個小的元件,然後通過一個管理類,進行重新組合。這樣的話我們可以最大限度的提供程式碼的複用性及可替代性。 Builder 的思路十分的簡潔,主要分為Director(管理者), **Builder(構建器)**和 Buinder 的子類。Director 通過一個 Builder 的例項來生成所需要的資料,而資料的具體實現方式,則是通過子類來實現的。Builder 中應當涵蓋構建資料所需要的所有必要方法,但是不應當含有特別的方法。這樣 Director 可以通過一個 Builder 來實現功能。 而具體的實現方式,它就不知道了,它只是知道其呼叫了一個 Builder,但是 Builder 有很多,它並不知道呼叫的是哪一個 Builder。這種不知道則提高了模式的靈活,只有不知道,才能夠被替換

    
    protocol Builder {
        func playVideo()
    }

    class Director { 
        var builder: Builder? // 一個可以播放視訊的構建器
        func playVideo() {  // 管理者想要實現播放視訊的功能
            builder?.playVideo()
        }
    }
    
    class Mp4Player: Builder {  //實現一個 mp4播放器
        func playVideo() {  
            self.playWithMp4()
        }
        
        func playWithMp4() {
            ... // 特有方法,以 MP4格式播放
        }        
    }
    
    class AviPlayer: Builder { // 實現一個 Avi 播放器
         func playVideo() {  
            self.playWithAvi()
        }
        
        func playWithAvi() {
            ... // 特有方法,以 Avi格式播放
        }      
    }
    
    // 實際的使用中,你可以為 Director 提供 mp4或是 avi 播放器
    // 只要符合`Builder`標準, 你可以隨意替換 builder 及其內部的實現
    // 而不影響其他的程式碼
    main {
         let player = Director()
         player.builder = Mp4Player()
         // palyer.builder = AviPlayer()
    }
複製程式碼

到現在為止,大家們對抽象這個概念應該都很瞭解了,抽象可以是抽象類也可以是介面,抽象的目的就是隱藏實現而突出邏輯。將邏輯和實現分開是實現程式碼複用和提高維護性減少耦合常用的方法。以後如果提到抽象,希望大家都能理解它的含義。

Abstract Factory 模式 (抽象工廠模式)

將關聯的零件組裝成產品

抽象工廠的作用就是將抽象零件加工成抽象產品。直接理解的話可能不是特別容易懂,我們直接舉一個例子,就大概明白它的意思了。 我們的抽象工廠就是一個生產抽象產品電子板的機器,這個電子板上有很多的電容、電阻和各種各樣的元件(抽象元件)。這裡我們並不知道電子板的大小和所需元件的引數和數量。那就意味著我們可以通過實現不同的電子板子類**(抽象產品的子類)來生產不同的產品。而抽象元件 只要符合對應的引數,我們可以使用任意廠商的元件(抽象零件的子類)**來使用。產品模型有了,元件也有了,那麼實現一個具體的工廠來生產特定的產品是很重要的,不可能一個工廠可以生產任何產品吧,我們也可以通過修改工廠例項來優化生產的工藝和流程。 這樣我們就實現了一個可以生產各式各樣產品的生產線。當需要修改的時候,我們不用替換很多的資料,只要將特定的子類替換掉就可以實現產品線的跟新,是不是和現在的代工廠一模一樣。

你會發現大部分的設計模式都要牽扯到抽象概念(介面)。這是很多模式優化的基礎。如果你知道面對物件程式設計和函式響應式程式設計等等,那你肯定對面對介面程式設計也有所耳聞,Swfit 相比 OC 就大量的使用了面向介面程式設計。這種程式設計方式的靈活性很高,如果大家感興趣,可以去多瞭解一下

分開考慮

Bridge 模式 (橋接模式)

將類的功能層次結構和實現層次結構分類開來

為了瞭解我們是為了橋接誰和誰,我們需要先來了解一下什麼是類的功能層次類的實現層次

  • 類的功能層次: 功能層次其實就是實現一個類的子類,當你需要給一個類新增新的功能的時候,我們可以通過實現一個子類來完成。隨著功能的增多,我們可以實現一個又一個子類,並不斷的加深這個結構,這就是類的功能層次。**(類的功能層次過多是不好的設計)**就行下面這樣:
  • Person - Men - Boy
  • 類的實現層次: 類的實現層次則是抽象類的實現,當我們需要改變一個類的方法實現方式的時候,我們只需一個繼承抽象類的子類就行了,我們並不是為了給父類中新增其沒有的新功能,我們只是為原功能提供了不同的實現而已。
  • Display - StringDisplay - HtmlDisplay

在實際的使用中,我們往往要根據實際的需求,靈活的運用這兩種結構。如果僅僅是將它們混合在一起使用的話,當應用變得更為複雜的時候,你就很難清楚的認識到,你到底應該繼承哪個類。 所以我們需要將功能層次實現層次分離開來。但前面說了,我們要靈活的運用兩種層次,那就要讓他們之間有聯絡,這時我們就需要在它們之間建一條橋樑。 大概的實現就是下圖這樣,Display 類是功能層次的最高層級,它持有一個 DisplayImp1的例項,這個例項中有與 Display 相對應的功能。DisplayImp1是一個抽象類, 而 StringDisplayImp1 繼承了 DisplayImp1,實現了所有的方法。

如何使用設計模式

這時我們可以建立一個 stringDisplayImp1的例項,通過這個例項來建立一個 Display 或是 PhotoDisplay 來使用。

Strategy 模式 (策略模式)

整體地替換演算法

**策略在程式中也可以被稱作演算法。**我們在處理程式中一些複雜的關係時,所使用的演算法可能會根據軟體的系統、時間的需求、錯誤率及系統硬體機能等進行相應的調整。這是我們要同時完備幾種演算法以便在系統中進行替換,不加設計的話,替換演算法本身也將是一個麻煩的事情。 Strategy 模式就可以方便的完整替換整個演算法。

例如我們想要實現一個棋類應用,單機模式下我們將會有一個AI來和玩家對戰。我們定義一個 Player 類作為AI的類。建立一個Player 需要提供一個策略,而這個策略Strategy是一個抽象類,它定義了一系列的方法,可以通過現在棋局的資料推算出下一步該往哪走。我們根據遊戲的演算法來制定演算法,這個時候我們就可以通過不同的子類策略實現裝置的適配和 AI 難度的調節。 無論策略發生了什麼改變,我們無需修改任何的介面,我們只需要替換一個策略的類,就可以完成整個演算法的替換。

Strategy 模式常用在棋牌類遊戲中,而且確實很實用。我感覺 Strategy 模式不太像一個正經的設計模式,它的概念很簡單,甚至就是抽象類或介面模式的基礎應用而已。我們平時寫程式碼的時候,多多少少會用過或見過這類用法。

一致性

Composite 模式 (複合模式)

容器與內容一致性

我們平時使用的電腦、ipad和手機等電子裝置都有自己的檔案管理系統。他們的基本結構就是有一個根目錄,下屬很多的資料夾和檔案。資料夾下面又是資料夾或是檔案。我們所看的這種樹狀結構看起來是由兩種資料型別組成的。其實我們完全可以把它們統一看做為一種目錄條目。 這種目錄條目擁有通用的屬性和方法,它們擁有一致的行為。能夠使容器和內容具有一致性,創造出遞迴的結構的模式就是 Composite 模式。

如何使用設計模式

Entry 是一個抽象類,它定義了一個條目。它通過getName()來獲取條目名字、getSize()獲取條目大小、printList()是用來列印內容列表,add()則是提供給子類Directory 來新增新的FileDirectoryFile 是檔案類,它可以返回檔名、大小和報告自己的目錄。Directory 是資料夾類,它有名字name, 還有directories 用來儲存自己內部的檔案和資料夾列表,但是它沒有自己的大小,它的大小是通過內容的getSize()方法相加獲取。 通過這樣的方式,我們就構建了一個遞迴的檔案結構,這種結構將內部的內容和外部的容器統一起來,使物件的呼叫變得更易理解和簡潔。

Decorator 模式 (裝飾器模式)

裝飾邊框與被裝飾物的一致性

一提到裝飾器,大家肯定都知道裝飾的概念。裝飾器就像你照片的相框,水果蛋糕上的點心一樣,通過裝飾物使主體**[被裝飾物]**(相片和蛋糕)變得與眾不同。這個模式的作用也是如此,但是如果只是這樣的話,你很容易把裝飾器看做和主體不同的東西。你的想法大概是這樣的:

如何使用設計模式

你可能以為它們是一被一個一個放到被裝飾物上的。這樣的話,你就無法裝飾裝飾物本身了,整個模式的擴充套件性就被降低了。我們需要裝飾物被裝飾物具備一致性,這樣的話介面就變得透明瞭起來,無論我們如何對被裝飾物進行裝飾,我們最後所看到的被裝飾物所體現的介面和行為還是和最初是一樣的。而這樣的形式才是真正的裝飾器模式,它就像一個俄羅斯套娃,一層巢狀一層,每層都可以看著一個包裝或裝飾,直到最後一個套娃出現。

如何使用設計模式

那我們如何實現這個結構呢,它看起來和 Composite 模式有些像。我們舉一個顯示程式的例子,它可以為顯示內容新增+、-、* 等字元邊框。 我們需要一個抽象類 Dispaly來描述顯示的流程,通過一個子類 StringDisplay 可以顯示一行字串。接下來我們定義一個裝飾器的抽象類Decorator,它是繼承於Display 的子抽象類Decorator 中有一個型別為Display的成員變數display,它表示被裝飾物。 這樣我們就將裝飾物和被裝飾物統一起來了,使他們滿足一致性。它的優缺點大概有以下幾點

  • 介面的透明性 無論我們進行多少次裝飾,被裝飾物的介面都沒有被隱藏起來,還是和當初一樣。
  • 可以在不改變裝飾物的前提下去新增功能 當我們新增功能的時候,只需要實現不同的裝飾器就 OK 了。
  • 需要實現太多的小類 我們需要些很多不同功能的裝飾器,這些類的功能通常不多,但是卻數量巨大。

訪問資料結構

Visitor 模式 (訪問者模式)

訪問資料結構並處理資料

我們大家都學過資料結構,資料結構的重要性就不言而喻了。但是此設計模式的重點不是如何設計資料結構,而是如何訪問資料結構。 一般我們在實現了一個資料結構後,都會將資料的操作方法和資料結構本身繫結在同一個類中。使它們成為一個整體。這樣做在以後使用的時候會方便很多,也不用那麼多的類進行操作。 但是當你想要擴張資料結構的處理方法的時候,你就需要直接修改資料結構的類。這樣做很不方便,既麻煩又不利於專案的穩定。 如何解決呢?以我們學習了以上那麼多設計模式的經驗,當然是將資料結構和訪問操作分離開來嘍。 在 Visitor 模式中,Visitor 是一個訪問者的形象。它是一個抽象類,內部定義了一個 visit()方法。,以我們在 Composite 模式中使用的檔案系統為例,這裡的Entry類同樣代表了資料結構的抽象形式,它的具體實現是由FileDirecotry 實現的。 不一樣的地方是,我們這裡要定義一個新的介面 Element,這個介面定義了一個accpet(Visiter)方法,Entry 遵守這個介面,而它的子類需要實現這個介面的方法。 accept()方法接受一個Visitor 例項,並在 accept()的方法內部呼叫Visitorvisit()方法,同時把自己【也就是Entry 本身】傳給這個方法。這樣在visit()的內部就可以進行資料的處理了,而細節則是由Visitor 的子類所實現的。 此時,我們就可以通過實現不同的Visitor 子類進行資料訪問方式的擴充套件。

整個模式的結構就是如此,但是你可能會為裡面 visit()和 accpet()的呼叫感到困惑在處理 Directory 的過程中,我們需要遍歷裡面的所以物件,並一個個呼叫他們的 accpet()方法,同時把Visitor 自己也給傳過去,然後在各個Entry 中再次呼叫 visit()方法,進行同樣的操作,直到最後一個檔案是File,遞迴就結束了。 這裡用到了兩個類的兩個方法來進行巢狀遞迴,著實很難讓人理解。一個遞迴就已經讓人頭痛了,這樣的遞迴也主要是為了實現資料處理資料結構的分離,並簡化資料的處理過程。 當你實現了一個新的Visitor 並通過一句呼叫就可以直接處理一個資料型別,而不用關心具體的型別時,你就會感受到它的好處了。 一切的辛苦都是有價值的。

Chain of Responsibility 模式 (責任鏈模式)

推卸責任

看到這個模式的描述推卸責任,是不是感覺有些奇葩。我們生活中,推卸責任看起來像是一個低效的處理問題的方式,那是如何在程式中發生正向的作用呢? 我們先看一下責任鏈模式的結構:

  • 問題: 既然要處理問題,那問題本身就很重要了。我們需要一個抽象類或是介面來定義問題。
  • 處理問題的抽象類: 問題需要交給一個類來處理,而這個類定義了處理問題的流程,比如判斷自己能否解決,如果能解決就返回結果。如果不能解決,就自動交給下一個類來處理,要是沒有下一個類就返回錯誤。
  • 具體處理問題的類: 抽象類我們已經定義了,但是具體的解決問題的方法,需要不同的子類來實現。

比如我們有多種不同等級的預警處理方案來處理一個警報。警報分為藍、黃、橙和紅四個級別。我們定義一個警報類Alert,它通過初始化方法init(int)傳入一個等級來建立。 接下來我們定義一個抽象類Handle 來實現處理警報的流程,它有屬性 next(Handle 的例項)表示要將責任推給那個物件。以及一個讓子類實現的抽象方法resolve(),用來表示解決問題的具體實現。 它通過方法targetr(Alert) 來處理警報, target 內會呼叫resolve()方法來處理問題,如果能處理就返回成功,如果無法處理,就將問題交給next,接下來會呼叫nexttarget()方法。直到有方案能處理警報,或是沒有辦法處理,報告錯誤。

現在想一想我們開頭的問題,責任鏈模式有什麼好處呢。

  • 我們可以簡化處理問題的流程,如果不踢皮球的話,我們需要為每個問題指明對應的處理方法。那Alert本身就需要知道自己能被哪個類處理,這就像你想要解決一個問題,你未必就一定能找到一個對的人。你只是純粹的想解決問題而已。讓問題知道自己該被誰解決,就會讓問題本身變得更加複雜。
  • 可以動態的修改流程,我們的處理順序是鏈式的,上一個類決定下一個要處理的類。我們只需要需改一下next就能夠輕鬆的改變處理問題的順序。

使用責任鏈模式的一個問題是,會增加處理問題的時間,因為是一個一個去判斷能不能解決的。如果問題沒有固定的解決方案,使用責任鏈模式是沒有任何問題的。如果能夠確定問題的處理方式就沒必要這樣了。

簡單化

Facade 模式 (視窗模式)

簡單視窗

隨著時間的腳步,我們的程式會越來越完善,同時也會變得更加複雜和冗餘。當我們實現新的功能時,我們需要在眾多的類中,找到需要的類,並組織邏輯和順序。特別是在大型程式中,每次呼叫都要注意眾多的類之間錯中複雜的關係。難道我們就不能使用一個統一的視窗,只需要呼叫這個視窗的方法,我們就可以實現這個操作,這個思想就是Facade 模式。 例如要實現一個釋出模組,要釋出的內容有文字、視訊和圖片。原來的操作是我們要分別上傳圖片 >> 上傳視訊 >> 處理文字 >> 整理json 資料 >> 上傳伺服器。 這樣的操作做一次還好,如果有多個地方需要使用到釋出的功能,這樣就顯得太過複雜了,也不利於整合模組的功能。 我們現在使用 Facade 模式進行改進,實現一個PublishManager 類就是Facade模式 中的視窗,它有一個方法publish(text, image, video) 可以直接接受文字、圖片和視訊,在PublishManager 內部,它可以把文字交給TextHandle[處理文字的類] 來處理,把圖片和視訊的上傳交給UploadManager[進行上傳的類],拿到 url 後通過JSONSerialization進行 JSON 處理。最後通過HTTPManager 將資料傳遞給後臺。

這樣以後,我們無論在任何地方需要使用到釋出功能的時候,我們只需要呼叫PublishManager 的釋出方法,就可以直接進行釋出,這裡我們就實現了一個視窗,進行釋出的視窗,而複雜的內部呼叫,就被我們隱藏起來了,我們無需關心它的內部呼叫,如果以後需要進行修改我們可以直接修改PublishManager 而不用再調整其他的地方,使得釋出的功能變得更加純粹。

Mediator 模式 (中介模式、仲裁者模式)

只有一個仲裁者

如果你要編寫一個聯機的棋類遊戲,同時有4名玩家進行對戰,每人一步,通過某個規則可以吃掉別人棋子。我們該如何同步各個玩家的棋盤和管理各個玩家的狀態呢。 如果我們每個玩家的終端,各自控制自己的狀態而後將資料傳送到其他的終端。那每個終端都要處理其他終端傳送過來的資料,而後同步自己的狀態。 這時每個終端都有一份自己的資料,處理的邏輯隨著玩家的個數增加也會變得更加複雜。並且一旦一個玩家的資料出錯,他會把錯誤的資料傳送給其他的終端,這時雙方的資料會發生衝突而產生致命錯誤。 而今天我們將通過 Mediator 模式來解決這個問題。我們通過一個仲裁者,你可以把它作為遊戲的一箇中間伺服器。玩家的每個終端都只是接收仲裁者發來的屬於自己的資料並進行狀態的更新,而自己的每一步操作就只是傳遞給仲裁者。仲裁者進行資料的處理後,再通知所有的終端分別更新狀態,這樣一來各個終端的操作實時彙集到仲裁者,而仲裁者再實時進行資料分發。 這樣做就不會出現資料不同步的狀況了,而資料的處理集中到了一點,降低了出現 bug 的概率。即使出現了問題也容易排查 bug 發生在哪裡。

除了上述的使用情景以外,我們在專案當中處理 GUI 的點選、介面和操作邏輯管理時,也可以使用Mediator 模式。 我們建立一個抽象類類Manager作為 Mediator,再建立一個介面Colleague, 表示和Manager 連線的各個控制元件。Manager 定義了各種各樣設定Colleague 的方法和方法didChange( Colleague )來告知Manager哪個控制元件發生了改變。我們例項化Manager 的一個子類,將其傳遞給各個控制元件[實現介面 Colleague],當控制元件發生狀態變更時就傳遞給這個仲裁者,而後仲裁者進行處理後,通過各個設定Colleague 的方法進行控制元件狀態的更新。

開到這裡我們就能發現,Mediator 模式是一種雙向繫結機制。只不過是各個物件都繫結同一個仲裁者,而後通過與它進行通訊藉以實現與其他的物件進行通訊的目的。

管理狀態

Observer 模式 [觀察者模式]

傳送狀態變化通知

說到觀察者模式,我想大家都應該有所瞭解。很多語言中都有Observer 模式的設計,雖然各種各樣的實現各有區別,但都是以Observer 觀察被觀察者,當被觀察者發生改變時,通知 Observer 發生了什麼改變為目的。 我們現在來實現一個簡單化的觀察者模式,我們建立一個抽象類NumberGenerator, 再建立一個RandomNumberGenerator 繼承自NumberGenerator

class NumberGenerator {
   var value: Int = 0 // 在這裡簡單的表示為自己的值
   public var observers: [Observer] = [] // 儲存所有的觀察者
   
   
   func addObserver(ob: Observer) {...} // 新增觀察者
   func deleteObserver(ob: Observer) {...} // 刪除觀察者
   func notifyObserver() {...} // 通過所有的觀察者,資料發生改變
   func excute() {...} // 執行資料跟新
   
   func getNumber() { FatalError() }// 交給子類實現,實現數值如何生成
} 

class RandomNumberGenerator: NumberGenerator {
   func getNumber() {...} // 返回一個隨機數
}
複製程式碼

接下來就是建立一個介面Observer,只有實現了此介面的類才能成為 NumberGenerator 的觀察者。它只有一個 update 方法 [在 swfit 中介面相當於協議]


protocol Observer {
    func update(obj: NumberGenerator)
}

class Display: Observer {
    func update(obj: NumberGenerator) {
        print(obj.value)
    }
}
複製程式碼

我們通過Dispaly的實現,將每次訂閱到的值顯示出來。下面是一個簡單的使用

    let generator = RandomNumberGenerator()
    let observer = Display()
    generator.addObserver(observer)
    generator.excute()
    
    // print: 2   列印出一個隨機數
複製程式碼

以上就是一個簡單化的Observer 模式的使用,如果細心的話你會看到,我們直接將被觀察者本身返回給了觀察者。一個物件可以同時被很多觀察者觀察,但是觀察者想要獲取的資訊可能各有不同,所以直接將自身傳遞,讓觀察者自己去查詢。 當然了,這是由於我們的設計過於簡陋。在 Objective-C 中,我們可以直接監聽各個物件的屬性。 其實,觀察者模式,我們也可以稱為訂閱模式。觀察者並不是去主動觀察,而是被觀察者通知觀察者的。如果理解為釋出和訂閱就更加契合了,你可以訂閱一個物件,如果他釋出了新的內容,你就會得到通知。

到這裡,如果你上面的各種模式都瞭解了一遍的話,你就會發現,在很多模式中已經出現了很多的這種可替換性設計了。通常進行替換性設計,可以提高系統的靈活性和降低耦合性。一般我們通過以下兩者方式進行替換性設計。

  • 利用抽象類和介面從具體類中提取出抽象方法
  • 在將例項作為引數傳遞至類中,或是在類的欄位中儲存例項時,不使用具體的型別,而是使用抽象類和介面

使用這種設計我們可以輕鬆的替換專案中的具體類。

Momento 模式

儲存物件狀態

我們平時使用的文字編輯器、PS 等等,都有一系列十分重要的功能,就是撤銷(undo)、 重做(redo) 和歷史快照(history)。像是撤銷這樣的操作我每天要使用幾百次,那如何記錄每個操作節點的狀態就十分重要了。 而 Momento 模式就十分善於處理這種情況,Momento 有紀念品的意思,我們也可以想象著把一個物件每個時間點的狀態拍上一張照片作為紀念品。 當我們需要的時候,我們可以通過每個時間點的快照來恢復物件的狀態。比如我們要記錄一個棋局,類ChessGame表示一局正在進行的棋盤。裡面有方法createMomento()通過當前棋子的資料儲存快照。我們是建立一個類Momento 來儲存棋局資料的。生成的快照被存入棋局的陣列history 中,當呼叫undo()方法時,我們就取出最後一個棋局狀態進行棋局的復原,這就是Momento 模式

class ChessGame {
    private var chessmanLocations: [Any]! // 這裡面是此次雙方旗子的位置資訊
    private var history: [Momento]? // 所以得快照陣列
    
    func undo() {
        let state = self.history.pop()
        ... 根據資訊恢復所有的棋子資料
    } // 撤銷
    
    func createMomento() {
        let mom = Momento(self.chessmanLocations)
        self.history.append(mom)
    } // 生成一個快照,並存入陣列中
    
}

class Momento {
    var chessmanLocations: [Any]!
}
複製程式碼

這裡的 Momento 模式和以前的 Prototype 模式在儲存狀態上也一點點相似,但是這裡的Momento 只是儲存恢復狀態所需要的必要資料,而Prototype 模式中,例項複製成的則是完完全全相同的另一個例項,所以它們的區別還是很明顯的。

State 模式 (狀態模式)

用類表示狀態

有些時候我們在專案當中會遇到各種各樣的狀態,比如應用的夜間模式和白天模式,再或者是一個警報系統的各個預警狀態。使用夜間、白天模式是一些閱讀軟體常備的功能,切換不同的模式,整個應用的介面會發生色調的轉變。而警報系統在不同的預警狀態下,對同一事件的處理方式也是不同的。

針對這種需要根據狀態判斷的例子,我們通常使用的方法,就是通過 if或是switch 來判斷不同的狀態,而執行不同的實現方法。比如應用的夜間和白天模式:


class Manager {
    public var isNight: Bool
    
    func navBarColor() -> UIColor {
        if self.isNight {
            return UIColor.black
        } else {
            return UIColor.white
        }
    }
    
    func bgColor() -> UIColor {
        if self.isNight {
            return ...
        } else {
            return ...
        }
    }    
    ...
}
複製程式碼

這個就是我們一般的實現方式,這樣的實現方式在簡單的狀態切換時到沒有什麼。但是像是以上這樣的白天和黑夜模式的介面顏色獲取,可能有幾十個方法,一個類中滿滿的都是if 看起來就眼花。如果這個時候你需要新增另一個模式,你就需要在每個方法下面新增一個 else if重要的是,編譯器並不會因為你忘記寫一個,而通知你, 所以,在新增新的模式時,我們很容易出錯,接下來就是用到 State 模式的時候了。

通過一個類來表示一個狀態,就是狀態模式。State 模式中我們通過建立一個類來表示一個新狀態。像以前一樣,我們需要建立一個抽象類State 來定義狀態中需要實現的方法。接下來我們分別定義NightStateDayState來表示白天和黑夜的狀態,通過以下的程式碼我們來看看有什麼區別。


public class State {
    
    public func navBarColor() -> UIColor {
        FatalError("no implementation")
    }
    
    public func bgColor() -> UIColor {
        FatalError("no implementation")
    }
}

class DayState: State {

    static let instance = DayState() // 狀態不需要重複建立,使用單例模式

    class func shared() -> DayState {
        return self;
    }

    override public func navBarColor() -> UIColor {
        return UIColor.white
    }
    override public func bgColor() -> UIColor {
        return UIColor.white
    }
}

class DayState: State {

static let instance = DayState() // 狀態不需要重複建立,使用單例模式

    override public func navBarColor() -> UIColor {
        return UIColor.black
    }
    override public func bgColor() -> UIColor {
        return UIColor.black
    }
}

class UIManager {
    var currentState: State;
    
    func resetState() {
        let date = NSDate()
        if (date => 9am && date <= 7pm) {
            self.currentState =  DayState.instance
        } else {
            self.currentState = NightState.instance
        }
    }
    
    func showNavBarColor() {
        setupNavBarColor(self.currentState.navBarColor)
    }
    
    func showBgColor() {
        setupBgColor(self.currentState.bgColor)
    }
}

複製程式碼

通過上面的例子,我想你一定明白了它們的區別。在這樣的 State 模式下,UIManager 是用來控制介面的顏色顯示的。它負責切換和控制狀態,所以它需要知道所有狀態的條件。 除了讓UIManager 控制狀態的切換外,我們還可以讓每個狀態本身去控制現在的狀態,這裡就像是 Chain of Responsibility 模式(責任模式)。我們擴充套件一下這個協議:

    
    extension State {
        func setTime(manager: UIManager, time: Date) {
            
        }
    }
    
    // DaySate 和 NightState 需要將上對應的方法
    
    class DayState: State {
        ..... 
        
        func setTime(manager: UIManager, time: Date) {
            if (date < 9am && date > 7pm) {
                manager.currentState = NightState.instance
            }
        }        
    }
    
    class NightState: State {
        .....         func setTime(manager: UIManager, time: Date) {
            if (date >= 9am && date <= 7pm) {
                manager.currentState = DayState.instance
            }
        }  
    }
複製程式碼

可以看出來,UIManager 只需要預設一個狀態,然後再呼叫方法前,告知當前模式時間,它就可以通過自己的判斷來尋找正確的狀態。這裡的狀態只有兩種,如果有很多種的話,自己不是此狀態,就傳遞給下一個狀態,直到找到一個正確的狀態。 使用第一種方法,manager 就需要知道所有的狀態關係。但是耦合度很低,各個狀態不需要知道其他的狀態。 而第二種方法,每個狀態或多或少的需要知道其他的狀態,這樣增加了耦合度。不過 Manager 不用再管理所有的狀態了,它只需要處理方法就行了。

  • 我們可以方便的新增各種各樣的狀態我們只需要實現State的方法就行了,可能還需要處理一下切換到其他狀態的情況,不過這是你使用第二種Manager 管理的時候。
  • 新增依賴於狀態的處理十分的麻煩當我們對狀態新增一個新的處理方法的時候,我們需要修改每一個狀態,這十分的麻煩。所幸的是,我們不會忘了給其中的一個狀態新增新的處理方法,因為編譯器會提示我們,如果我們忘記了給任意一個狀態新增方法。如果不使用State 模式就不會得到編譯器的幫助,可想而知,一旦大意,就會引發不可知的 bug。

避免浪費

Flyweight 模式 (輕量級模式)

共享物件,避免浪費

我們都知道在應用當中使用的物件都佔用了一定的系統記憶體,當我們的物件佔用記憶體過大時,就會降低系統的執行速度和穩定性,甚至引發崩潰。 如果有些物件可以被共同使用,就可以減少建立新物件的開銷,也可以降低記憶體的佔用。所以 Flyweight 模式就是 通過儘量共享例項來避免 new 出新的例項來大大降低系統的記憶體消耗。

這裡我們舉一個例子,比如我們要列印一張圖片,而這張圖片是又幾種不同的素材圖片拼出來的。當我們在在列印圖片的時候,我們需要先將對應的素材按照順序排列好,才能進行列印。

class Image {
    let id: Int
    let data: Data?
    init(id: Int) {
        self.id = id
        self.data = createData(id)
    }
    
    func createData(id: Int) -> Data {
        ... // 根據id 生成圖片的資料
        return data
    }
}


class ImageManager {
    var imageIds: [Int] = [] // 需要排列的圖片 id 陣列
    var imageCache: [Int : Image] = [:] // 每個 id 對應一個它的圖片快取
    
    // 通過 id 獲取圖片,如果快取中有的話就直接使用,如果沒有的話,就建立一個放入到快取中
    func getImage(id: Int) -> Image {
        if let image = imageCache[id] {
            return image
        } else {
            let image = Image(id)
            imageCache[id]= image
            return image
        }
    }
    
    // 列印圖片,根據 id 陣列的順序進行排序
    func printImage(imageIds: [Int]) {
        self.imageIds = imageIds
        var images = imageIds.map { return getImage($0) }
        printWithImageData(images)
    }
    
    func printWithImageData(imageDatas: [Image]) {
        ... //根據圖片的資料進行列印
    }

    
}
複製程式碼

我們建立Image 當做是素材,ImageManager是用來排版素材的類,它通過傳入一個包含素材 id 的陣列來列印出對應的圖片。 在排列過程中,我們每種素材的資訊其實是不變的,所以它是可以共享的,我們使用一個字典把 id 當做 key來 實現快取素材資料。當通過 id 排列素材時,我們直接獲取快取中的素材資料,如果重複使用了一個素材,也不會再次建立,而是共享一個物件。通過這樣的方式,我們就能夠減少一大部分的記憶體消耗。 不過共享同一個物件也有問題,就是改變了這個物件,那麼所有共享它的也會發生改變。這有時候是好事,有時候是壞事,具體要看應用的場景。 但是大概可以這樣判斷是否該共享該物件。

  • 代表本質的,不依賴於狀態和位置的物件可以共享它是一個intrinsic 資訊
  • 外在的,依賴於狀態和位置的物件不能共享 它是一個extrinsic 資訊

一般的物件都適用於這兩個規則。根據專案的實現目的,靈活的運用Flyweight 模式可以優化你的應用記憶體佔用。

Proxy 模式 (代理人模式)

只在必要時生成例項

當讀到代理人模式的時候,希望你不會把它和OC 中的delegate 弄混淆了。OC中的 delegate 其實是介面interface或者說是protocol 的使用,而我們今天要了解的Proxy 模式中的代理人指的是替原本的物件來執行操作。 在哪些情況下,我們需要使用Proxy 模式呢? 通常是當一個物件的建立需要消耗大量的效能,而它的重要操作又可以延後的時候。在這種情況下,如果需要使用此物件,就立刻建立,可能會佔用過高的效能,而後又沒有使用到這個物件的重要功能,那豈不是白白浪費了大量的系統算力。

所以我們需要使用一個代理人來替代這個本人。它實現這個本人的基本屬性和方法,而將耗時的工作交給真正的本人去做,那樣只有在真正需要本人去做得事情才會去建立本人,而其他的不耗時操作將交給代理人去做。

這裡我們舉一個例子,比如有一個列印圖片的類ImagePrint,它通過一個url 來初始化例項,呼叫Print 方法就可以列印出這張圖片,這就是本人。又有一個類ImagePrintProxy 表示它的代理人。介面ImagePrintable 規定了本人代理人都應該具備的方法和屬性。下面我們通過虛擬碼來具體瞭解一下整個過程:


// 首先是介面 ImagePrintable,它定義了一個能列印圖片的類,都需要實現什麼方法
protocol ImagePrintable {    func setUrl(urlStr: String) // 設定圖片地址
    func getUrl() -> String // 獲取圖片地址
    func print() // 根據地址,列印圖片
}

// ImagePrint 本人,它是列印圖片的實際操作者,列印圖片是一個耗時的操作,
class ImagePrint: ImagePrintable {
    var url: String
    var printer: PhotoPrinter? // 這是一個圖片印表機,初始化它需要耗費大量的時間
    init(url: String) {        self.url = url
        self.printer = PhotoPrinter() // 這是一個耗時操作
    }
    func getUrl() -> String {
        return self.url
    }
    func setUrl(urlStr: String) {        self.url = urlStr
    }
    func print() {
        self.printer.printImage()
        ... // 根據 url 下載圖片然後使用 printer 再列印出來
    }
}


// 最後就是 ImagePrintProxy 代理人,通過代理人我們可以在不列印圖片時
// 設定和獲取圖片的地址,而不用初始化 ImagePrint。因為初始化`ImagePrint` 時
// 會建立`printer`,這會耗費大量的時間。而是在呼叫 print 的時候,在初始化它。

class ImagePrintProxy: ImagePrintable {
    var url: String
    var real: ImagePrint? // 這是真正執行列印操作的物件
    
    init(url: String) {
        self.url = url
    }
    
    func getUrl() -> String {
        return self.url
    }
    
    func setUrl(urlStr: String) {
        if self.real != nil {  //當存在本人時,就設定本人的值
            self.real.url = urlStr
        }
        self.url = urlStr
    }
    
    func print() {
        self.release()
        self.real.print() // 呼叫本人來實現列印圖片的方法
    }
    
    func release() { // 生成原始物件的方法
         if self.real == nil {
            self.real = ImagePrint(self.url)
         }
    }
    
}
複製程式碼

看過這個例子以後,就很容易理解什麼是Proxy 模式了,使用 Proxy 模式的時候,呼叫者並不關心是誰實現了裡面的方法,它只是呼叫了符合ImagePrintable 的類。而實際的執行者ImagePrint 也不關心自己是被直接呼叫還是間接呼叫。對問題的處理就交給了ImagePrintProxy 這個代理人身上。這樣的話,代理人就可以根據實際的情況來替本人完成一些簡單的工作,而儘量將本人的建立延後,只在真正需要使用的時候,才會建立本人

這樣的設計,對外顯示出了一致性,在不影響呼叫關係的情況下。節省了系統的效能消耗,能提高應用的流暢性。

用類來表示

Command 模式 (命令模式)

命令也是類

通常我們所說的命令都是例項的方法,雖然呼叫的結果會在例項的狀態中得到反饋,但是卻無法留下呼叫的歷史記錄。當我們想要把每一次呼叫都記錄下來時,我們可以把類當作命令來看待,使用類來表示要做的操作。這樣我們管理一系列操作時就是直接管理這些命令類的例項,而不是通過方法進行動態操作了。

那我們該如何進行設計以實現Command 模式呢,一樣,我們舉一個例子。比如我們實現一個和Flyweight 模式(上上個模式)一樣的功能,通過素材列印圖片,這裡我們再為它新增一些新的功能,並進行優化。

  • 如果素材進行排列的時候,不是按照順序,而是有各自的座標
  • 並且每新增一個素材我們就立即列印出來。

現在我們把每次新增一個素材的操作不在看做是一個方法裡面的迴圈執行,而是一個個命令。我們需要一個介面Command (interface)表示什麼是命令,命令很簡單,只需要能執行就 OK 了。 每次繪製素材的操作用DrawCommand來表示,它繼承於Command

有時我們可能需要執行一系列的操作,所以我們需要一個表示操作集合的類MacroCommand,它同樣也繼承於Command,在MacroCommand 中有新增和移除Command 的命令,同樣有儲存所有操作的屬性commands

有了命令,但是命令本身不執行具體的繪製操作,它僅僅是提供操作的具體資料。我們還需要一個繪製類,這個繪製類我們不具體建立,而是通過一個介面Drawable 來表示, Drawable 需要實現繪製方法draw()。為什麼這樣設計,如果你看了以上的設計模式,我想你應該已經很清楚了。使用介面,能方便的替換繪製實現,也為你要繪製不同的東西提供了擴充套件的可能性,並且不影響其他程式碼的結構,這就是程式碼的可替換性。 這裡我們用ImageDrawCanvas 來表示一個簡單的繪製圖片的圖層。下面是虛擬碼的實現


// 命令介面,只定義了 excute
protocol Command {
    open func excute()
}

// DrawCommand 表示繪製命令的類
class DrawCommand: Command {
    var url: String        // 圖片地址
    var position: Point    // 圖片位置
    var drawable: Drawable // 執行繪製操作的圖層,並未指定具體的型別,而是介面 Drawable
    
    // 初始化一個命令
    init(url: String, position: Point, drawable: Drawable) { 
        self.url = url
        self.position = position
        self.drawable = drawable
    }
    
    func excute() { // 執行繪製命令
        self.drawable.draw(url: self.url, position: self.position)
    }
}

// MacroCommand 一個命令集合

class MacroCommand: Command {
    var commands: [Command] // 所有的命令,只要是`Command` 就可以,這意味著不但可以新增`DrawCommand`還可以新增`MacroCommand`,命令集合在本質上還是命令。
    
    func addCommand(command: Command) {  // 新增一個命令
        if command != self { // 不能新增新增自己,防止死迴圈
             self.commands.append(command)
        }
    }
    
    func undo() { // 移除最後一個命令
         self.commands.removeLast();
    }
    
    func clear() { // 移除所有的命令
        self.commands.removeAll()
    }
    
    func excute() { // 執行命令
        for command in self.commands { // 遍歷執行所有的命令
            command.excute() 
        }
    }
}

// 繪製介面
protocol Drawable {
    func draw(url:String, position: Point)
}

// 圖片繪製類
class ImageDrawCanvas: Drawable {
    var history: MacroCommand // 繪製的命令歷史,當你需要重新繪製的時候,可以直接呼叫
    var size: Size // 畫布大小
    
    init(size: Size, history: MacroCommand) {
        self.size = size
        self.history = history
    }
    
    func draw(url: String, position: Point) {
        ... // 根據圖片的地址和座標,進行圖片的繪製
    }
    
    func redo() { //重新繪製
        self.history.excute()
    }
}

// 所有的類都準備好了,我們來看一下如何操作

func main {    var history: MacroCommand = MacroCommand() 
    lazy var imageCanvas: ImageDrawCanvas {
        return ImageDrawCanvas(Size(width: 1000, height: 1000), self.history)c
    }

    func viewDidload() {
        super.viewDidLoad()
        
        for i in 0...100 { // 迴圈新增100個素材
            let command = DrawCommand(url: "http://www.ssbun.com/12.png", position: Point(x: i, y: i), self.imageCanvas)
            command.excute() // 執行繪製
            self.history.addCommand(command) // 加入到歷史記錄中
        }
    }    
}
複製程式碼

以上偽實現了一個Commnad 模式的圖片繪製功能,不過Command 模式的主要實現就是這樣的。通過具象一個操作為一個例項,我們能精準的操控每一個操作,並重復任意的步驟。我們還可以將這些例項進行歸檔處理,永久儲存我們的操作記錄。在我們瞭解的以上所有的設計模式,除了本文的把類作為命令,還有State 模式中的把類作為狀態。 以後再遇到操作是需要在例項的方法內進行很多的判斷和選擇,你可以試著將不同的情況拆分為不同的類來實現,或許會豁然開朗。

Interpreter 模式 (翻譯模式)

語法規則也是類

又多了一個用類來替換某些東西的類,而這次,我們這模擬的是語法。在某些特殊的情況下,我們可能想要設計一種新的迷你語言來方便的編寫繁瑣的操作。例如正規表示式就可以通過簡短的語法來描述複雜的篩選條件。我們也可以設計一款小語言來這樣做,再編寫一個翻譯程式將它翻譯成你所使用的語言。而其中的各種語法可以被翻譯為不同的類,比如Add 作為 +, CommandList 作為 repeat 等等。但是,這個過程還是很麻煩的,這裡的篇幅已經很長了。而敘述一個迷你語言,或許需要更大的篇幅才能講明白,而這篇文章只是想要使用簡單的文字來幫助你瞭解所謂的23設計模式。

結語

終於看完了所有的23種設計模式,其實很多的設計模式已經不知不覺中被我們使用了無數次了。對於經驗豐富的程式設計師而言,設計模式中的方法在他們看來是理所應當的。畢竟,設計模式本身就是對前輩們經驗的總結,本身並沒有什麼突出的特點。它也不能幫你解決所有的問題,但是通過了解設計模式,我們可以更快的學習到前輩的經驗。在實際的使用中,對我們的幫助是顯而易見的。 設計模式雖然很重要,但是你卻不用想著把它們都記在自己的腦海中。死記硬背從來都不是好方法,你只要有些許的印象,知道遇見這樣的問題時該使用什麼樣的模式,隨後再去查詢具體的資料就是行了,善用搜尋引擎可是程式設計師最重要的一項技能。 說了那麼多,最後再說點我的感悟。

語言技巧很多,黑魔法很多,設計思想也很多,學完所有為大家所稱讚的思想和技巧,也並不能讓你的專案看起來更完美。遇見問題時,越是簡單的實現就越有可能解決問題,也更容易被人看懂。讓程式看起來簡單,而不是讓它看起來 NB。有一句話說的好 “要讓程式看起來明顯沒有問題,而不是沒有明顯的問題。”

相關文章