iOS設計模式詳解

jackyshan_發表於2018-08-26

iOS設計模式詳解

在軟體工程中,(引自維基百科)設計模式(design pattern)是對軟體設計中普遍存在(反覆出現)的各種問題,所提出的解決方案。這個術語是由埃裡希·伽瑪(Erich Gamma)等人在1990年代從建築設計領域引入到電腦科學的。 設計模式並不直接用來完成程式碼的編寫,而是描述在各種不同情況下,要怎麼解決問題的一種方案。物件導向設計模式通常以類別或物件來描述其中的關係和相互作用,但不涉及用來完成應用程式的特定類別或物件。設計模式能使不穩定依賴於相對穩定、具體依賴於相對抽象,避免會引起麻煩的緊耦合,以增強軟體設計面對並適應變化的能力。

使用設計模式的目的:為了程式碼可重用性、讓程式碼更容易被他人理解、保證程式碼可靠性。 設計模式使程式碼編寫真正工程化;設計模式是軟體工程的基石脈絡,如同大廈的結構一樣。

講到設計模式,必然要提設計原則,同樣為了實現程式碼複用,要遵循軟體設計原則。設計模式就是實現了這些原則,達到了程式碼的複用、可維護的目的。下面從7種設計原則,23種設計模式,來講解軟體編寫過程中用到的這些知識點

設計原則

編寫 全稱 中文
S Single Responsibility Principle 單一職責原則
O Open Close Principle 開閉原則
L Liskov Substitution Principle 里氏替換原則
I Interface Segregation Principle 介面隔離原則
D Dependence Inversion Principle 依賴倒置原則
L Law Of Demeter 迪米特法則
C Composite/Aggregate Reuse Principle CARP 組合/聚合複用原則

前面五種被稱為物件導向設計中常用的SOLID原則。

單一職責原則

理解:不同的類具備不同的職責,各司其職。做系統設計是,如果發現有一個類擁有了兩種職責,那麼就要問一個問題:可以將這個類分成兩個類嗎?如果真的有必要,那就分開,千萬不要讓一個類乾的事情太多。

總結:一個類只承擔一個職責

例子:我們設計一個訂單列表,列表分為待支付、待收貨、已收貨等列表,那我們是寫一個類,使用if判斷是哪個型別,然後請求相應的資料,還是寫多個類,分別執行各自的功能呢。很多人會覺的寫一個類比較省事,但是過多的判斷條件,各種職責冗餘到一個類中真的好嗎,如果待支付列表需要加一些特殊的功能呢,待收貨也需要加一些功能呢,那這個類是不是變得條件判斷異常的多。所以還是寫成多個類,實現各自的邏輯比較好。其實另外我們寫列表的Cell,也是一個道理,分成幾種型別的Cell去寫,而不是一個Cell實現幾種型別。

import Foundation

class OrderList: NSObject {//訂單列表
    var waitPayList: WaitPayList?//待支付
    var waitGoodsList: WaitGoodsList?//待收貨
    var receivedGoodsList: ReceivedGoodsList?//已收貨
}

class WaitPayList: NSObject {
    
}

class WaitGoodsList: NSObject {
    
}

class ReceivedGoodsList: NSObject {
    
}
複製程式碼

開閉原則

理解:類、模組、函式,可以去擴充套件,但不要去修改。如果要修改程式碼,儘量用繼承或組合的方式來擴充套件類的功能,而不是直接修改類的程式碼。當然,如果能保證對整個架構不會產生任何影響,那就沒必要搞的那麼複雜,直接改這個類吧。

總結:對軟體實體的改動,最好用擴充套件而非修改的方式。

例子:我們設計支付功能的時候,會用到不同的支付方式,我們可以選擇在支付的時候使用判斷支付條件然後使用不同的支付方式,然而這種設計真的好嗎。如果我們新增了一個支付方法或者刪除了一個支付方法是不是要改動pay方法的邏輯,那每一次的調整都要改動pay方法的邏輯是不是不合理了,依據開閉原則具體做法應該是設計擴充套件支付方式來實現不同的支付。

修改之前程式碼

import Foundation

class PayHelper {
    func pay(send: PaySendModel) -> Void {
        if send.type == 0 {
            //支付寶支付
        }
        else if send.type == 1 {
            //微信支付
        }
    }
}

class PaySendModel {
    var type: Int = 0
    var info: [String: AnyHashable]?
}
複製程式碼

修改之後

import Foundation

class PayHelper {
    var processors: [Int: PayProcessor]?
    
    func pay(send: PaySendModel) -> Void {
        guard let processors = processors else {return}
        guard let payProcessor: PayProcessor = processors[send.type] else {return}
        
        payProcessor.handle(send: send)//支付
    }
}

class PaySendModel {
    var type: Int = 0
    var info: [String: AnyHashable]?
}

protocol PayProcessor {
    func handle(send: PaySendModel)
}

class AliPayProcessor: PayProcessor {
    func handle(send: PaySendModel) {
        
    }
}

class WeChatPayProcessor: PayProcessor {
    func handle(send: PaySendModel) {
        
    }
}
複製程式碼

可以看到修改之後的支付,擴充套件起來是不是很方便,增加支付方式只需要繼承PayProcessor就行了,不需要更改pay方法了。

里氏替換原則

理解:一個物件在其出現的任何地方,都可以用子類例項做替換,並且不會導致程式的錯誤。換句話說,當子類可以在任意地方替換基類且軟體功能不受影響時,這種繼承關係的建模才是合理的。

總結:子類可以擴充套件父類的方法,但不應該複寫父類的方法。

例子:我們定義汽車的基類,基類裡面有行駛的方法,現在我們有個寶馬車,寶馬車繼承汽車基類,也有行駛方法。現在我們想知道寶馬車的行駛速度是多少,該怎麼設計呢。

修改之前

import Foundation

class Car {
    func run() {
        print("汽車跑起來了")
    }
}

class BaoMaCar: Car {
    override func run() {
        super.run()
        
        print("當前行駛速度是80Km/h")
    }
}
複製程式碼

可以看到我們重寫了run方法,增加了汽車行駛速度的邏輯,這樣是不滿足的里氏替換原則的。因為所有基類Car替換成子類BaoMaCar,run方法的行為跟以前不是一模一樣了。

修改之後

import Foundation

class Car {
    func run() {
        print("汽車跑起來了")
    }
}

class BaoMaCar: Car {
    func showSpeed() {
        print("當前行駛速度是80Km/h")
    }
}
複製程式碼

介面隔離原則

理解:一個類實現的介面中,包含了它不需要的方法。將介面拆分成更小和更具體的介面,有助於解耦,從而更容易重構、更改。

總結:物件不應被強迫依賴它不使用的方法。

例子:我們定義一個汽車介面,要求實現run等方法。

修改之前

import Foundation

protocol ICar {
    func run()
    func showSpeed()
    func playMusic()
}

class Car: ICar {
    func run() {
        print("汽車跑起來了")
    }
    
    func showSpeed() {
        print("當前行駛速度是80Km/h")
    }
    
    func playMusic() {
        print("播放音樂")
    }
}
複製程式碼

可以看到我們定義Car實現了ICar的介面,但是並不是每個車都有播放音樂的功能的,這樣對於一般的低端車沒有這個功能,對於他們來說,這個介面的設計就是冗餘的。

修改之後

import Foundation

protocol IProfessionalCar {//具備一般功能的車
    func run()
    func showSpeed()
}

protocol IEntertainingCar {//具備娛樂功能的車
    func run()
    func showSpeed()
    func playMusic()
}

class SangTaNaCar: IProfessionalCar {//桑塔納轎車
    func run() {
        print("汽車跑起來了")
    }
    
    func showSpeed() {
        print("當前行駛速度是80Km/h")
    }
}

class BaoMaCar: IEntertainingCar {//寶馬轎車
    func run() {
        print("汽車跑起來了")
    }
    
    func showSpeed() {
        print("當前行駛速度是80Km/h")
    }
    
    func playMusic() {
        print("播放音樂")
    }
}
複製程式碼

介面隔離原則的要求我們,建立單一介面,不要建立龐大臃腫的介面,儘量細化介面,介面中的方法儘量少。這通過分散定義多個介面,可以預防外來變更的擴散,提高系統的靈活性和可維護性。

依賴倒置原則

理解:高層模組不應該依賴低層模組,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。

總結:面向介面程式設計,提取出事務的本質和共性。

例子:我們給汽車加油,實現能夠加90號的汽油,如果沒有,就加93號的汽油。

修改之前

import Foundation

class Car {
    func refuel(_ gaso: Gasoline90) {
        print("加90號汽油")
    }
    
    func refuel(_ gaso: Gasoline93) {
        print("加93號汽油")
    }
}

class Gasoline90 {
    
}

class Gasoline93 {
    
}
複製程式碼

上面這段程式碼有什麼問題,可以看到Car高層模組依賴了底層模組Gasoline90和Gasoline93,這樣寫是不符合依賴倒置原則的。

修改之後

import Foundation

class Car {
    func refuel(_ gaso: IGasoline) {
        print("加\(gaso.name)汽油")
    }
}

protocol IGasoline {
    var name: String { get }
}

class Gasoline90: IGasoline {
    var name: String = "90號"
}

class Gasoline93: IGasoline {
    var name: String = "93號"
}
複製程式碼

修改之後我們高層模組Car依賴了抽象IGasoline,底層模組Gasoline90和Gasoline93也依賴了抽象IGasoline,這種設計是符合依賴倒置原則的。

迪米特法則

理解:一個物件對另一個物件瞭解得越多,那麼,它們之間的耦合性也就越強,當修改其中一個物件時,對另一個物件造成的影響也就越大。

總結:一個物件應該對其他物件保持最少的瞭解,實現低耦合、高內聚。

例子:實現一個給汽車加油的設計,使的我們可以隨時保證加油的質量過關。

修改之前

import Foundation

class Person {
    var car: Car?
    
    func refuel(_ gaso: IGasoline) {
        if gaso.isQuality == true {//如果汽油質量過關,我們就給汽車加油
            car?.refuel(gaso)
        }
    }
}

class Car {
    func refuel(_ gaso: IGasoline) {
        print("加\(gaso.name)汽油")
    }
}

protocol IGasoline {
    var name: String { get }
    var isQuality: Bool { get }
}

class Gasoline90: IGasoline {
    var name: String = "90號"
    var isQuality: Bool = false
}

class Gasoline93: IGasoline {
    var name: String = "93號"
    var isQuality: Bool = true
}
複製程式碼

可以看到上面有個問題,我們怎麼知道汽油的質量是否過關呢,即時我們知道,加油判斷油的質量這個事情也不應該由我們來做。

修改之後

import Foundation

class Person {//給車加油的人
    var car: Car?
    
    func refuel(_ worker: WorkerInPetrolStation, _ gaso: IGasoline) {
        guard let car = car else {return}
        
        worker.refuel(car, gaso)
    }
}

class WorkerInPetrolStation {//加油站工作人員
    func refuel(_ car: Car, _ gaso: IGasoline) {
        if gaso.isQuality == true {//如果汽油質量過關,我們就給汽車加油
            car.refuel(gaso)
        }
    }
}

class Car {
    func refuel(_ gaso: IGasoline) {
        print("加\(gaso.name)汽油")
    }
}

protocol IGasoline {
    var name: String { get }
    var isQuality: Bool { get }
}

class Gasoline90: IGasoline {
    var name: String = "90號"
    var isQuality: Bool = false
}

class Gasoline93: IGasoline {
    var name: String = "93號"
    var isQuality: Bool = true
}
複製程式碼

可以看到這樣我們就實現了低耦合,我們只需要知道有加油站工作人員和要加的汽油就行了,不需要知道太多汽油相關的知識,以及加油相關的操作流程,這些都交給了工作人員,這樣是符合我們的迪米特原則的。

組合/聚合複用原則

理解:合成/聚合複用原則就是在一個新的物件裡面使用一些已有的物件,使之成為新物件的一部分;新的物件通過向這些物件的委派達到複用已有功能的目的。它的設計原則是:要儘量使用合成/聚合,儘量不要使用繼承。

總結:就是說要少用繼承,多用合成關係來實現。

繼承複用有一定的缺點:比如如果基類發生了改變,那麼派生類的的實現就不得不發生改變;而且從超類繼承而來的實現是靜態的,不可能在執行時發生改變,因此它的靈活性差並最終會限制複用性。

使用組合/聚合複用原則就解決了繼承複用的缺點。

例子:我們實現一個角色,可以是僱員、經理等。

iOS設計模式詳解

人被繼承到僱員,學生,經理子類。而實際上,僱員、學生和經理分別描述一種角色,而人可以同時有幾種不同的角色。比如,一個人既然是經理了就一定是僱員,使用繼承來實現角色,則只能使用每一個人具有一種角色,這顯然是不合理的。錯誤的原因就是把角色的等級結構和人的等級結構混淆起來,把Has-A的關係誤認為是Is-A的關係,通過下面的改正就可以正確的做到這一點。

iOS設計模式詳解

從上圖可以看出,每一個人都可以有一個以上的角色,所以一個人可以同時是僱員又是經理。從這個例子可以看出,當一個類是另一個類的角色時,不應該使用繼承描述這種關係。

有了以上設計原則,我們可以遵循設計原則實現很多好的設計模式,下面講講設計模式。

建立型模式

在軟體工程中,引自維基百科建立型模式是處理物件建立的設計模式,試圖根據實際情況使用合適的方式建立物件。基本的物件建立方式可能會導致設計上的問題,或增加設計的複雜度。建立型模式通過以某種方式控制物件的建立來解決問題。 建立型模式由兩個主導思想構成。一是將系統使用的具體類封裝起來,二是隱藏這些具體類的例項建立和結合的方式。

主要用於建立物件。

抽象工廠模式

提供一個介面,用於建立與某些物件相關或依賴於某些物件的類家族,而又不需要指定它們的具體類。通過這種模式可以去除客戶程式碼和來自工廠的具體物件細節之間的耦合關係。

類簇是一種把一個公共的抽象超類下的一些私有的具體子類組合在一起的架構。抽象超類負責宣告建立私有子類例項的方法,會根據被呼叫方法的不同分配恰當的具體子類,每個返回的物件都可能屬於不同的私有子類。

Cocoa將類簇限制在資料儲存可能因環境而變的物件生成上。Foundation框架為NSStringNSDataNSDictionaryNSSet、和NSArray物件定義了類簇。公共超類包括上述的不可變類和與其相互補充的可變類NSMutableStringNSMutableDataNSMutableDictionaryNSMutableSet、和NSMutableArray

iOS設計模式詳解

import Foundation

class GzCity {//廣州市有兩個啤酒廠
    var abstractFactory1: IAbstractFactory?
    var abstractFactory2: IAbstractFactory?
}

protocol IAbstractFactory {//抽象工廠
    func createProductA() -> IProduct
    func createProductB() -> IProduct
}

protocol IProduct {
    var name: String { get }
}

class BearProduct: IProduct {//啤酒產品
    var name: String = "啤酒"
}

class ConcreteFactory1: IAbstractFactory {//啤酒工廠1
    func createProductA() -> IProduct {
        return BearProduct()
    }
    
    func createProductB() -> IProduct {
        return BearProduct()
    }
}

class ConcreteFactory2: IAbstractFactory {//啤酒工廠2
    func createProductA() -> IProduct {
        return BearProduct()
    }
    
    func createProductB() -> IProduct {
        return BearProduct()
    }
}
複製程式碼

建造者模式

將一個複雜物件的構建與它的表示分離,使得同樣的構建過程可以建立不同的表示。

  • 當建立複雜物件的演算法應該獨立於該物件的組成部分以及它們的裝配方式時。

  • 當構造過程必須允許被構造的物件有不同的表示時。

iOS設計模式詳解

import Foundation

protocol IBuilder {
    func createProduct() -> IProduct
}

protocol IProduct {
    var name: String { get }
}

class BeerProduct: IProduct {
    var name: String = "啤酒"
}

class Director {//領導
    var builder: ConcreteBuilder?
    
    func construct() {//指導生產
        guard let product = builder?.createProduct() else {return}
        print("生產產品" + product.name)
    }
}

class ConcreteBuilder: IBuilder {//生產者
    func createProduct() -> IProduct {
        return BeerProduct()
    }
}
複製程式碼

工廠方法模式

定義一個用於建立物件的介面,讓子類決定例項化哪一個類。Factory Method 使一個類的例項化延遲到其子類。

  • 當一個類不知道它所必須建立的物件的類的時候。

  • 當一個類希望由它的子類來指定它所建立的物件的時候。

  • 當類將建立物件的職責委託給多個幫助子類中的某一個,並且你希望將哪一個幫助子類是代理者這一資訊區域性化的時候。

iOS設計模式詳解

import Foundation

class ConcreteCreator: ICreator {//生產者
    func factoryMethod() -> IProduct {
        return ConcreteProduct()
    }
}

protocol ICreator {
    func factoryMethod() -> IProduct
}

protocol IProduct {
    var name: String { get }
}

class ConcreteProduct: IProduct {
    var name: String = "啤酒"
}
複製程式碼

原型模式

用原型例項指定建立物件的種類,並且通過拷貝這些原型建立新的物件。

原型模式是非常簡單的一種設計模式, 在多數情況下可被理解為一種深複製的行為。在Objective-C中使用原型模式, 首先要遵循NSCoping協議(OC中一些內建類遵循該協議, 例如NSArray, NSMutableArray等)。剛才我們提到了深複製, 一圖以蔽之:

iOS設計模式詳解

下面為UML圖

iOS設計模式詳解

import Foundation

class Client {
    var prototype: IPrototype!
    
    func operation() -> IProduct {
        return prototype.clone()
    }
}

protocol IPrototype {
    func clone() -> IProduct
}

protocol IProduct {
    var name: String { get }
}

class ConcreteProduct: IProduct, IPrototype {
    var name: String = "啤酒"
    
    func clone() -> IProduct {
        let p = ConcreteProduct()
        p.name = name
        return p
    }
}
複製程式碼

單例模式

保證一個類僅有一個例項,並提供一個訪問它的全域性訪問點。該類需要跟蹤單一的例項,並確保沒有其它例項被建立。單例類適合於需要通過單個物件訪問全域性資源的場合。

有幾個Cocoa框架類採用單例模式,包括NSFileManagerNSWorkspace、和NSApplication類。這些類在一個程式中只能有一個例項。當客戶程式碼向該類請求一個例項時,得到的是一個共享的例項,該例項在首次請求的時候被建立。

iOS設計模式詳解

import Foundation

class Singleton {
    static let instance: Singleton = Singleton()
    
    init() {
        
    }
}
複製程式碼

結構型模式

在軟體工程中結構型模式是設計模式,藉由一以貫之的方式來了解元件間的關係,以簡化設計。

主要用於處理類或物件的組合。

介面卡模式

將一個類的介面轉換成另外一個客戶希望的介面。Adapter 模式使得原本由於介面不相容而不能一起工作的那些類可以一起工作。

iOS設計模式詳解

我們可以使用介面卡模式,把UITableView的介面進行二次封裝,統一對外回撥我們關心的介面。比如點選cell的事件回撥等。

import Foundation

class ListAdaper<T>: UITableViewDelegate, UITableViewDataSource {
    
    var cellClick: ((_ obj: T) -> Void)?
    
    init(_ tableView: UITableView) {
        tableView.delegate = self
    }
    
    ...
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        cellClick?(datas[indexPath.row])
    }
    
}
複製程式碼

可以看到以上程式碼,大大簡化了介面的複雜度,適配回撥給我們的介面是我們關心和使用的到的就行了。節約了很多的程式碼成本,增加了維護性。

橋接模式

將抽象部分與它的實現部分分離,使它們都可以獨立地變化。

iOS設計模式詳解

  • Abstraction是一個虛基類,Operation()呼叫Implemetor的OperationImp()方法
  • Implemetor父類有兩個子類A和B。
  • 兩個基類間的關係是聚合關係,Abstrction是整體,Implementor是部分,說白了就是Implementor是Abstraction的成員變數。

好了,下面用橋接模式來實現下面這個比較簡單的場景: 花園裡有一朵牽牛花和一朵牡丹花,牽牛花開會吸引蜜蜂來採蜜,牡丹花開會吸引蝴蝶來採蜜。

import Foundation

protocol IAbstractInsect {
    func bloomImp()
}

protocol IAbstractFlower {
    var insect: IAbstractInsect? { get }
    
    func bloom()
}

class QianniuHua: IAbstractFlower {
    var insect: IAbstractInsect?
    
    func bloom() {
        print("牽牛花開了")
        insect?.bloomImp()
    }
}

class MudanHua: IAbstractFlower {
    var insect: IAbstractInsect?
    
    func bloom() {
        print("牡丹花開了")
        insect?.bloomImp()
    }
}

class Butterfly: IAbstractInsect {
    func bloomImp() {
        print("蝴蝶來了")
    }
}

class Bee: IAbstractInsect {
    func bloomImp() {
        print("蜜蜂來了")
    }
}

let qianniu = QianniuHua.init()
qianniu.insect = Bee.init()
qianniu.bloom()

let mudan = MudanHua.init()
mudan.insect = Butterfly.init()
mudan.bloom()
複製程式碼

列印如下: 牽牛花開了 蜜蜂來了 牡丹花開了 蝴蝶來了

組合模式

這種模式將互相關聯的物件合成為樹結構,以表現部分-全部的層次結構。它使客戶程式碼可以統一地處理單獨的物件和多個物件的合成結果。

合成物件是模型-檢視-控制器聚集模式的一部分。

iOS設計模式詳解

組合模式最大的優點是他的節點可以自由增加,且呼叫節點方便。

import UIKit

class Composite: NSObject {
    var subComposites: NSMutableArray = {NSMutableArray()}()
    var parentComposite: Composite?
    
    func addComposite(comp: Composite) {
        subComposites.add(comp)
        comp.parentComposite = self
    }

    func removeCompositeAtIndex(index:Int)  {
        subComposites.remove(at: index)
    }

    func removeComposite(comp: Composite)  {
        subComposites.remove(comp)
    }

    func removeFromParent()  {
        if (self.parentComposite != nil) {
            self.parentComposite?.removeComposite(comp: self)
        }
    }
}
複製程式碼

裝飾模式

這種模式動態地將額外的責任附加到一個物件上。在進行功能擴充套件時,裝飾是子類化之外的一種靈活的備選方法。和子類化一樣,採納裝飾模式可以加入新的行為,而又不必修改已有的程式碼。裝飾將需要擴充套件的類的物件進行包裝,實現與該物件相同的介面,並在將任務傳遞給被包裝物件之前或之後加入自己的行為。裝飾模式表達了這樣的設計原則:類應該接納擴充套件,但避免修改。

裝飾是用於物件合成的模式,在您自己的程式碼中應該鼓勵使用物件的合成。然而,Cocoa自己提供了一些基於這種模式的類和機制。在這些實現中,擴充套件物件並不完全複製它所包裝的物件的介面,雖然具體實現中可以使用不同的技術來進行介面共享。

Cocoa在實現某些類時用到了裝飾模式,包括NSAttributedStringNSScrollView、和NSTableView。後面兩個類是複合檢視的例子,它們將其它一些檢視類的物件組合在一起,然後協調它們之間的互動。

動態地給一個物件新增一些額外的職責。就增加功能來說,Decorator模式相比生成子類更為靈活。

iOS設計模式詳解

通過類別實現裝飾模式,如果你是一名iOS開發者,你可能立即會想到用類別來實現。沒錯,用類別實現可以達到同樣的效果,而且會更簡單。類別是Objective-C的特性,它可以新增類的行為,而不用進行子類化,通過類別新增的方法不會影響類原來的方法,類別也成為類的一部分,並可由其子類繼承。

  雖然通過類別可以實現裝飾模式,但是這並不是一種嚴格的實現,由類別新增的方法是編譯時繫結的,而裝飾模式是動態繫結的,另外類別也沒有封裝被擴充套件類的例項。類別適合裝飾器不多的時候,上面的例子只有一個NetData裝飾器,用類別實現會更輕量,更容易。

外觀模式

這種模式為子系統中的一組介面提供統一的介面。表觀模式定義一個更高階別的介面,通過減少複雜度和隱藏子系統之間的通訊和依賴性,使子系統更加易於使用。

iOS設計模式詳解

NSImage類為裝載和使用基於點陣圖(比如JPEG、PNG、或者TIFF格式)或向量(EPS或PDF格式)的影象提供統一的介面。NSImage可以為同一個影象保持多個表示,不同的表示對應於不同型別的NSImageRep物件。NSImage可以自動選擇適合於特定資料型別和顯示裝置的表示。同時,它隱藏了影象操作和選擇的細節,使客戶程式碼可以交替使用很多不同的表示。

  • 實現了子系統與客戶端之間的鬆耦合關係。
  • 客戶端遮蔽了子系統元件,減少了客戶端所需處理的物件數目,並使得子系統使用起來更加容易。

在生活中很多地方也用到外觀模式,比如購買基金,我們從基金機構那裡購買基金,然後他們幫我們管理我們的基金,去操作和執行,我們只管購買和賣出就行了,而不用去管他們內部的操作,下面是UML圖和具體實現:

iOS設計模式詳解

/// 基金類
class Fund {
    var gu1 = Stock1()
    var gu2 = Stock2()
    var gu3 = Stock3()
    var nd  = NationalDebt()
    var rt  = Realty()

    public func buyFund() {
        print("買入基金")
        gu1.buy()
        gu2.buy()
        gu3.buy()
        nd.buy()
        rt.buy()
    }

    public func shellFund() {
        print("\n賣出基金")
        gu1.shell()
        gu2.shell()
        gu3.shell()
        nd.shell()
        rt.shell()
    }
}

//股票類
class Stock1: Deal {
    var dealName: String {
        return "股票一"
    }
}

class Stock2: Deal {
    var dealName: String {
        return "股票二"
    }
}

class Stock3: Deal {
    var dealName: String {
        return "股票三"
    }
}

class NationalDebt: Deal {
    var dealName: String {
        return "國債"
    }
}

class Realty: Deal {
    var dealName: String {
        return "房地產"
    }
}

protocol Deal {
    var dealName: String {get}
    mutating func shell()
    mutating func buy()
}

extension Deal {
    mutating func shell() {
        print("\(dealName)賣出")
    }

    mutating func buy() {
        print("\(dealName)買入")
    }
}

let jijin = Fund()
// 基金購買
jijin.buyFund()
// 基金贖回
jijin.shellFund()
複製程式碼

列印如下: 買入基金 股票一買入 股票二買入 股票三買入 國債買入 房地產買入

賣出基金 股票一賣出 股票二賣出 股票三賣出 國債賣出 房地產賣出

以上就是簡單的外觀模式的實現,定義高層介面,基金的買入和賣出,去使用整個基金系統,而不用去管內部是怎麼操作的。

享元模式

運用共享技術有效地支援大量細粒度的物件。

iOS設計模式詳解

適用性

  • 一個應用程式使用了大量的物件。

  • 完全由於使用大量的物件,造成很大的儲存開銷。

  • 物件的大多數狀態都可變為外部狀態。

  • 如果刪除物件的外部狀態,那麼可以用相對較少的共享物件取代很多組物件。

  • 應用程式不依賴於物件標識。由於Flyweight物件可以被共享,對於概念上明顯有別的物件,標識測試將返回真值。

例子:我們在一個介面上生成1000個花,共有5種花,實現方式如下。

iOS設計模式詳解

修改前

- (void)viewDidLoad
{
    [super viewDidLoad];

//使用普通模式
    for (int i = 0; i < 100000; i++) {
        @autoreleasepool {
            CGRect screenBounds = [[UIScreen mainScreen] bounds];
            CGFloat x = (arc4random() % (NSInteger)screenBounds.size.width);
            CGFloat y = (arc4random() % (NSInteger)screenBounds.size.height);
            NSInteger minSize = 10;
            NSInteger maxSize = 50;
            CGFloat size = (arc4random() % (maxSize - minSize + 1)) + minSize;
            CGRect area = CGRectMake(x, y, size, size);

            FlowerType flowerType = arc4random() % kTotalNumberOfFlowerTypes;
            //新建物件
            UIImageView *imageview = [self flowerViewWithType:flowerType];
            imageview.frame = area;
            [self.view addSubview:imageview];
        }
    }
}

- (UIImageView *)flowerViewWithType:(FlowerType)type
{
    UIImageView *flowerView = nil;
    UIImage *flowerImage;

    switch (type)
    {
        case kAnemone:
            flowerImage = [UIImage imageNamed:@"anemone.png"];
            break;
        case kCosmos:
            flowerImage = [UIImage imageNamed:@"cosmos.png"];
            break;
        case kGerberas:
            flowerImage = [UIImage imageNamed:@"gerberas.png"];
            break;
        case kHollyhock:
            flowerImage = [UIImage imageNamed:@"hollyhock.png"];
            break;
        case kJasmine:
            flowerImage = [UIImage imageNamed:@"jasmine.png"];
            break;
        case kZinnia:
            flowerImage = [UIImage imageNamed:@"zinnia.png"];
            break;
        default:
            break;
    }

    flowerView = [[UIImageView alloc]initWithImage:flowerImage];

    return flowerView;
}
複製程式碼

上面的程式碼可以看到每次要新建一個flowerView新增到self.view檢視上,這樣做的後果是什麼呢,特別是生成大量的物件的時候。

iOS設計模式詳解

可以看到記憶體暫用很大,程式碼上看,其實只有6種花放在不同的位置而已,那我們可以利用享元模式的思想,複用這6種花,然後繪製到不同位置,而不是增加物件新增到檢視上。

修改之後

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>


@interface FlowerView : UIImageView
{

}

- (void) drawRect:(CGRect)rect;

@end

==================

#import "FlowerView.h"
#import <UIKit/UIKit.h>

@implementation FlowerView

- (void) drawRect:(CGRect)rect
{
  [self.image drawInRect:rect];
}

@end
複製程式碼
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

typedef enum  
{
  kAnemone,
  kCosmos,
  kGerberas,
  kHollyhock,
  kJasmine,
  kZinnia,
  kTotalNumberOfFlowerTypes
} FlowerType;

@interface FlowerFactory : NSObject 
{
  @private
  NSMutableDictionary *flowerPool_;
}

- (UIImageView *) flowerViewWithType:(FlowerType)type;

@end

======================

#import "FlowerFactory.h"
#import "FlowerView.h"

@implementation FlowerFactory


- (UIImageView *)flowerViewWithType:(FlowerType)type
{
  if (flowerPool_ == nil)
  {
    flowerPool_ = [[NSMutableDictionary alloc] 
                   initWithCapacity:kTotalNumberOfFlowerTypes];
  }

  UIImageView *flowerView = [flowerPool_ objectForKey:[NSNumber
                                                  numberWithInt:type]];

  if (flowerView == nil)
  {
    UIImage *flowerImage;

    switch (type) 
    {
      case kAnemone:
        flowerImage = [UIImage imageNamed:@"anemone.png"];
        break;
      case kCosmos:
        flowerImage = [UIImage imageNamed:@"cosmos.png"];
        break;
      case kGerberas:
        flowerImage = [UIImage imageNamed:@"gerberas.png"];
        break;
      case kHollyhock:
        flowerImage = [UIImage imageNamed:@"hollyhock.png"];
        break;
      case kJasmine:
        flowerImage = [UIImage imageNamed:@"jasmine.png"];
        break;
      case kZinnia:
        flowerImage = [UIImage imageNamed:@"zinnia.png"];
        break;
      default:
        break;
    } 

    flowerView = [[FlowerView alloc] 
                   initWithImage:flowerImage];
    [flowerPool_ setObject:flowerView 
                    forKey:[NSNumber numberWithInt:type]];
  }

  return flowerView;
}


@end
複製程式碼
#import "ViewController.h"
#import "FlowerFactory.h"
#import "FlyweightView.h"
#import <objc/runtime.h>
#import <malloc/malloc.h>
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

// 使用享元模式
    FlowerFactory *factory = [[FlowerFactory alloc] init];
    NSMutableArray *flowerList = [[NSMutableArray alloc]
                                   initWithCapacity:500];
    for (int i = 0; i < 10000; ++i)
    {
        @autoreleasepool {
            FlowerType flowerType = arc4random() % kTotalNumberOfFlowerTypes;
            //重複利用物件
            UIImageView *flowerView = [factory flowerViewWithType:flowerType];

            CGRect screenBounds = [[UIScreen mainScreen] bounds];
            CGFloat x = (arc4random() % (NSInteger)screenBounds.size.width);
            CGFloat y = (arc4random() % (NSInteger)screenBounds.size.height);
            NSInteger minSize = 10;
            NSInteger maxSize = 50;
            CGFloat size = (arc4random() % (maxSize - minSize + 1)) + minSize;

            CGRect area = CGRectMake(x, y, size, size);
            //新建物件
            NSValue *key = [NSValue valueWithCGRect:area];
            //新建物件
            NSDictionary *dic =   [NSDictionary dictionaryWithObject:flowerView forKey:key];
            [flowerList addObject:dic];

        }

    }

    FlyweightView *view = [[FlyweightView alloc]initWithFrame:self.view.bounds];
    view.flowerList = flowerList;
    self.view = view;


}

@end
複製程式碼
#import <UIKit/UIKit.h>

@interface FlyweightView : UIView 

@property (nonatomic, retain) NSArray *flowerList;

@end

==================

#import "FlyweightView.h"
#import "FlowerView.h"

@implementation FlyweightView

extern NSString *FlowerObjectKey, *FlowerLocationKey;


- (void)drawRect:(CGRect)rect 
{
  for (NSDictionary *dic in self.flowerList)
  {

      NSValue *key = (NSValue *)[dic allKeys][0];
      FlowerView *flowerView = (FlowerView *)[dic allValues][0];
      CGRect area = [key CGRectValue];
      [flowerView drawRect:area];
  }

}

@end
複製程式碼

iOS設計模式詳解

可以看到記憶體已經降下來了,我們只是生成了物件flowerView,但是並沒有add到FlyweightView上,[self.image drawInRect:rect];使用image重新繪製了一個新的位置去顯示。

代理模式

為其他物件提供一種代理以控制對這個物件的訪問。

iOS設計模式詳解

這種模式為某些物件定義介面,使其充當其它物件的代理或佔位物件,目的是進行訪問控制。這種模式可以用於為一個可能是遠端的、建立起來開銷很大的、或者需要保證安全的物件建立代表物件,並在代表物件中為其提供訪問控制的場合。它在結構上和裝飾模式類似,但服務於不同的目的;裝飾物件的目的是為另一個物件新增行為,而代理物件則是進行訪問控制。iOS中大量的使用了代理模式,UITableView,UIScrollView,AppDelegate等。

圖中涉及的角色如下所示:

  • 協議:定義代理和委託的共同介面(方法)
  • 委託:根據指定的協議,委託代理去完成實現指定介面(方法)
  • 代理:根據指定的協議,實現委託需要實現的介面(方法)

看實現程式碼

import Foundation

protocol IProxy {//協議
    func charge()
}

class A {//委託
    var delegate: IProxy?
    
    func askProxy() {
        delegate?.charge()
    }
}

class B: IProxy {//代理
    func charge() {
        print("A委託B充值,B實現了代理方法charge")
    }
}
複製程式碼

行為型模式

在軟體工程中, 引自維基百科行為型模式為設計模式的一種型別,用來識別物件之間的常用交流模式並加以實現。如此,可在進行這些交流活動時增強彈性。

用於描述對類或物件怎樣互動和怎樣分配職責。

職責鏈模式

使多個物件都有機會處理請求,從而避免請求的傳送者和接收者之間的耦合關係。將這些物件連成一條鏈,並沿著這條鏈傳遞該請求,直到有一個物件處理它為止。

iOS設計模式詳解

Application Kit框架中包含一個稱為響應者鏈的架構。該鏈由一系列響應者物件(就是從NSResponder繼承下來的物件)組成,事件(比如滑鼠點選)或者動作訊息沿著鏈進行傳遞並(通常情況下)最終被處理。如果給定的響應者物件不處理特定的訊息,就將訊息傳遞給鏈中的下一個響應者。響應者在鏈中的順序通常由檢視的層次結構來決定,從層次較低的響應者物件向層次較高的物件傳遞,頂點是管理檢視層次結構的視窗物件,視窗物件的委託物件,或者全域性的應用程式物件。事件和動作訊息在響應者鏈中的確切傳遞路徑是不盡相同的。一個應用程式擁有的響應者鏈可能和它擁有的視窗(甚至是區域性層次結構中的檢視物件)一樣多,但每次只能有一個響應者鏈是活動的—也就是與當前活動視窗相關聯的那個響應鏈。

iOS設計模式詳解

程式碼實現如下

import Foundation

class DutyHandle : NSObject {
    /// 下一個
    var next : DutyHandle?
    
    /// 處理請求操作
    func handleRequest(str:String) {
        /// 如果可以則直接處理
        if (self.canDealWithRequest(str: str)) {
            print(str)
        }
        else {
            /// 否則如果有下一個,則下一個進行處理判斷
            if ((next) != nil) {
                next?.handleRequest(str: str)
            }
        }
    }
    
    /// 判斷能否處理請求
    func canDealWithRequest(str:String) -> Bool {
        return false
    }
}
複製程式碼

命令模式

將一個請求封裝為一個物件,從而使你可用不同的請求對客戶進行引數化;對請求排隊或記錄請求日誌,以及支援可取消的操作。請求物件將一或多個動作繫結在特定的接收者上。命令模式將發出請求的物件和接收及執行請求的物件區分開來。

iOS設計模式詳解

呼叫物件

NSInvocation類的例項用於封裝Objective-C訊息。一個呼叫物件中含有一個目標物件、一個方法選擇器、以及方法引數。您可以動態地改變呼叫物件中訊息的目標及其引數,一旦訊息被執行,您就可以從該物件得到返回值。通過一個呼叫物件可以多次呼叫目標或引數不同的訊息。

建立NSInvocation物件需要使用NSMethodSignature物件,該物件負責封裝與方法引數和返回值有關係的資訊。NSMethodSignature物件的建立又需要用到一個方法選擇器。NSInvocation的實現還用到Objective-C執行環境的一些函式。

NSInvocation物件是分散式、撤消管理、訊息傳遞、和定時器物件程式設計介面的一部分。在需要去除訊息傳送物件和接收物件之間的耦合關係的類似場合下,您也可以使用。

目標-動作

目標-動作機制使控制元件物件—也就是象按鍵或文字輸入框這樣的物件—可以將訊息傳送給另一個可以對訊息進行解釋並將它處理為具體應用程式指令的物件。接收物件,或者說是目標,通常是一個定製的控制器物件。訊息—也被稱為動作訊息—由一個選擇器來確定,選擇器是一個方法的唯一執行時標識。典型情況下,控制元件擁有的單元物件會對目標和動作進行封裝,以便在使用者點選或啟用控制元件時傳送訊息(選單項也封裝了目標和動作,以便在使用者選擇時傳送動作訊息)。目標-動作機制之所以能夠基於選擇器(而不是方法簽名),是因為Cocoa規定動作方法的簽名和選擇器名稱總是一樣的。

當您用Interface Builder構建程式的使用者介面時,可以對控制元件的動作和目標進行設定。您因此可以讓控制元件具有定製的行為,而又不必為控制元件本身書寫任何的程式碼。動作選擇器和目標連線被歸檔在nib檔案中,並在nib檔案被解檔時復活。您也可以通過向控制元件或它的單元物件傳送setTarget:和setAction:訊息來動態地改變目標和動作。

目標-動作機制經常用於通知定製控制器物件將資料從使用者介面傳遞給模型物件,或者將模型物件的資料顯示出來。Cocoa繫結技術則可以避免這種用法,有關這種技術的更多資訊請參見Cocoa繫結程式設計主題文件。

程式碼示例
import Foundation

class Command: NSObject {
    var receiver: Receiver?
    init(receiver:Receiver) {
        super.init()
        self.receiver = receiver
    }
    
    func execute() {
        
    }
    
    func undo() {
        
    }
}

class UpCommand: Command {
    override func execute() {
        receiver!.soundNumber += 1
    }
    override func undo() {
        receiver!.soundNumber -= 1
    }
}

class DownCommand: Command {
    override func execute() {
        receiver!.soundNumber -= 1
    }
    override func undo() {
        receiver!.soundNumber += 1
    }
}

class Receiver: NSObject {
    var soundNumber: Int = {0}()
}

class CommandManager: NSObject {
    var commandList: NSMutableArray = {NSMutableArray()}()
    var command: Command?
    
    func setCommand(command:Command) {
        self.command = command
    }
    
    func execute() {
        if (self.command != nil) {
            self.command!.execute()
            commandList.add(self.command!)
        }
    }
    
    func undo() {
        if self.commandList.count > 0 {
            let command = self.commandList.lastObject as! Command
            command.undo()
            self.commandList.removeLastObject()
        }
    }
}

class Client: NSObject {
    func begin() {
        let receiver = Receiver()
        receiver.soundNumber = 15
        let upCommand = UpCommand(receiver: receiver)
        let manager = CommandManager()
        manager.setCommand(command: upCommand)
        manager.execute()
    }
}
複製程式碼

直譯器模式

給定一個語言,定義它的文法的一種表示,並定義一個直譯器,這個直譯器使用該表示來解釋語言中的句子。

iOS設計模式詳解

程式碼示例如下

import Foundation

class Explain: NSObject {
    func add(a: Double, b: Double) -> Double {
        return a + b
    }
    
    func multiply(a: Double, b: Double) -> Double {
        return a * b
    }
}
複製程式碼

迭代器模式

這種模式提供一種順序訪問聚合物件(也就是一個集合)中的元素,而又不必暴露潛在表示的方法。迭代器模式將訪問和遍歷集合元素的責任從集合物件轉移到迭代器物件。迭代器定義一個訪問集合元素的介面,並對當前元素進行跟蹤。不同的迭代器可以執行不同的遍歷策略。

iOS設計模式詳解

Foundation框架中的NSEnumerator類實現了迭代器模式。NSEnumerator抽象類的私有具體子類返回的列舉器物件可以順序遍歷不同型別的集合—陣列、集合、字典(值和鍵)—並將集合中的物件返回給客戶程式碼。

NSDirectoryEnumerator是一個不緊密相關的類,它的例項可以遞迴地列舉檔案系統中目錄的內容。

NSArrayNSSet、和NSDictionary這樣的集合類都包含相應的方法,可以返回與集合的型別相適用的列舉器。所有的列舉器的工作方式都一樣。您可以在迴圈中向列舉器傳送nextObject訊息,如果該訊息返回nil,而不是集合中的下一個物件,則退出迴圈。

實現棧的迭代如下

import Foundation

/// 堆疊迭代器
class StackIterator: NSObject {
    private lazy var stack = NSMutableArray()
    
    func push(object :Any) {
        stack.add(object)
    }
    
    func pop() -> Any {
        let object = readStackRear
        if empty() {
            stack.remove(object)
        }
        return object
    }
    
    /// 讀取棧尾
    func readStackRear() -> Any {
        if empty() {
            return NSNull()
        }
        let object = stack.lastObject
        return object!
    }
    
    func count() -> Int {
        return stack.count
    }
    
    func empty() -> Bool {
        return stack.count == 0
    }
}
複製程式碼

實現佇列的迭代如下

import Foundation

/// 佇列迭代器
class QueueIterator: NSObject {
    private lazy var quene = NSMutableArray()
    
    func inQuene(object :Any) {
        quene.add(object)
    }
    
    func outQuene() -> Any {
        let object = readQueneHead()
        if empty() == false {
            quene.remove(object)
        }
        return object
    }
    
    /// 讀取隊首
    func readQueneHead()  -> Any {
        if empty() {
            return NSNull()
        }
        let object = quene.firstObject
        return object!
    }
    
    func count() -> Int {
        return quene.count
    }
    
    func empty() -> Bool {
        return quene.count == 0
    }
}
複製程式碼

實現迭代器如下

import Foundation

/// 迭代器
class EnumIterator: NSObject {
    private(set) lazy var allObjects = NSArray()
    private lazy var index = 0
    
    init(allObjects : NSArray) {
        super.init()
        self.allObjects = allObjects
    }
    
    func nextObject() -> Any {
        if index >= allObjects.count {
            return NSNull()
        }
        let object = allObjects[index]
        index += 1
        return object
    }
    
}
複製程式碼

中介者模式

用一箇中介物件來封裝一系列的物件互動。中介者使各物件不需要顯式地相互引用,從而使其耦合鬆散,而且可以獨立地改變它們之間的互動。

使用中介之前如下圖

iOS設計模式詳解

使用中介之後如下圖

iOS設計模式詳解

中介者模式很好的處理了業務中元件化方案的強耦合的問題,我們iOS當中元件化的實現都是基於中介者的模式的。其中的Mediator起到至關重要的作用,Mediator就是我們封裝的元件化的框架。框架方案可參考iOS 元件化方案探索

備忘錄模式

這種模式在不破壞封裝的情況下,捕捉和外部化物件的內部狀態,使物件在之後可以回覆到該狀態。備忘錄模式使關鍵物件的重要狀態外部化,同時保持物件的內聚性。

iOS設計模式詳解

在iOS常用的實現備忘錄模式的模組有歸檔、序列化、CoreData等。

歸檔

歸檔將一個程式中的物件以及物件的屬性(包括屬性和關係)儲存到檔案上,使之可以儲存到檔案系統中,或者在不同的處理器和網路間傳遞。檔案將程式的物件圖儲存為獨立於架構的位元組流,物件的標識和物件之間的關係都會被保留。由於物件的型別和它的資料一起被儲存,從歸檔的位元組流解碼出來的物件會被正常例項化,例項化所用的類與原來編碼的類相同。

通常情況下,您希望將程式中需要儲存狀態的物件歸檔。模型物件幾乎總是屬於這個範疇。您通過編碼將物件寫入到檔案中,而通過解碼將物件從檔案中讀取出來。通過NSCoder物件可以執行編解碼操作,在編解碼過程中最好使用鍵化的歸檔技術(需要呼叫NSKeyedArchiverNSKeyedUnarchiver類的方法)。被編解碼的物件必須遵循NSCoding協議;該協議的方法在歸檔過程中會被呼叫。

屬性列表的序列化

屬性列表是一個簡單的、具有一定結構的物件圖序列,它僅使用下面這些類的物件:NSDictionaryNSArrayNSStringNSDataNSDate、和NSNumber。這些物件通常也被稱為屬性列表物件。Cocoa中有幾個框架類提供了序列化屬性列表物件,以及定義錄寫物件內容及其層次關係的特殊資料流格式的方法。NSPropertyListSerialization類就提供了將屬性列表物件序列化為XML或其它優化的二進位制格式的類方法。

如果物件圖中包含的是簡單物件,則在捕捉和外部化物件及其狀態時,屬性列表序列化是一種靈活的、可移植的、而又非常適當的工具。然而,這種形式的序列化有它的限制,它不保留物件的全部類標識,而只保留一些一般的型別(陣列、字典、字串、等等)。這樣,從屬性列表恢復出來的物件可能和原來的類不同,特別是當物件的可變性可能發生變化時,這就會帶來問題。屬性列表序列化也不跟蹤在同一物件中被多次引用的物件,這可能導致反向序列化時產生多個例項,而在原來的物件圖中卻只有一個例項。

Core Data

Core Data是一個管理物件圖,並使其留存的框架和架構。正是第二種能力—物件的留存能力—使Core Data成為備忘錄模式的一種適配形式。

在Core Data架構中,中心的物件稱為被管理物件上下文,負責管理應用程式物件圖中的模型物件。在被管理物件上下文下面是該物件圖的持久棧,也就是一個框架物件的集合,負責協調模型物件和外部資料儲存,比如XML檔案或關聯式資料庫。持久棧物件負責建立儲存中的資料和被管理物件上下文中的物件之間的對映關係,在有多個資料儲存的時候,持久棧物件將這些儲存表現為被管理物件上下文中的一個聚合儲存。

Core Data的設計也在很大程度上受到模型-檢視-控制器以及物件建模模式的影響。

觀察者模式

這種模式定義一種物件間一對多的依賴關係,使得當一個物件的狀態發生變化時,其它具有依賴關係的物件可以自動地被通知和更新。觀察者模式本質上是個釋出-定閱模型,主體和觀察者具有寬鬆的耦合關係。觀察和被觀察物件之間可以進行通訊,而不需要太多地瞭解對方。

iOS設計模式詳解

在iOS常用的觀察者模式的模組有通知、KVO等。

通知

Cocoa的通知機制實現了一對多的訊息廣播,其實現方式符合觀察者模式。在這種機制中,程式裡的物件將自己或其它物件新增到一或多個通知的觀察者列表中,每個通知由一個全域性的字串(即通知的名稱)標識。希望向其它物件傳送通知的物件-也就是被觀察的物件-負責建立一個通知物件,並將它傳送到通知中心。通知中心則負責確定通知的觀察者,並通過訊息將通知傳送給觀察者物件。通知訊息啟用的方法必須遵循特定的單引數簽名格式,方法的引數就是通知物件,包含通知的名稱、被觀察的物件、以及一個含有補充資訊的字典。

通知的傳送是一個同步的過程。在通知中心將通知廣播給所有的觀察者之前,傳送物件不會再得到程式的控制權。對於非同步的處理方式,控制權會在您將通知放入通知佇列中之後立即返回到傳送物件,當通知到達佇列的頂部時,通知中心會將它廣播出去。

常規的通知-也就是那些由通知中心廣播的通知-只能在程式內部傳播。如果您希望將通知廣播給其它程式,需要使用分散式通知中心及其相關的API。

使用通知可以有很多原因。例如,藉助通知機制,您可以根據程式中其它地方發生的事件改變使用者介面元素顯示資訊的方式。或者,您可以用通知來保證文件中的物件在文件視窗關閉之前儲存自己的狀態。通知的一般目的是將事件通知給其它程式物件,使它們可以做出恰當的反應。

但是,通知的接收物件只能在事件發生之後進行反應,這和委託機制有顯著的不同。被委託的物件有機會拒絕或修改委託物件希望進行的操作。另一方面,觀察者物件不能直接影響一個即將發生的操作。

與通知有關的類有NSNotification(通知物件)、NSNotificationCenter(用於傳送通知和新增觀察者)、NSNotificationQueue(負責管理通知佇列)、和NSDistributedNotificationCenter。很多Cocoa框架都發布和傳送通知,其它物件都可以成為這些通知的觀察者。

KVO

鍵-值觀察是使物件可以在其它物件的具體屬性發生變化時得到通知的一種機制。它基於名為NSKeyValueObserving 的非正式協議。被觀察的屬性可以是簡單的屬性、一對一的關係、或者一對多的關係。鍵-值觀察在模型-檢視-控制器模式中特別重要,因為它使檢視物件-通過控制器層-可以觀察到模型物件的變化,因此是Cocoa繫結技術的必要元件(參見"控制器類"部分)。

Cocoa為很多NSKeyValueObserving方法提供了預設的“自動”實現,使所有遵循該協議的物件都具有屬性-觀察的能力。

鍵-值觀察和通告機制類似,但在一些重要的方面也有不同。在鍵-值觀察中,沒有為所有觀察者提供變化通告的中心物件,而是將變化的通告直接傳遞給觀察物件。還有,鍵-值觀察直接和具體的物件屬性相關聯。而通告機制則更廣泛地關注程式的事件。

參與鍵-值觀察(KVO)的物件必須滿足特定的要求-或者說是遵循KVO,記憶起來更為方便。對於自動觀察來說,這需要滿足鍵-值編碼的要求(也稱為遵循KVC),並使用遵循KVC的方法(也就是存取方法)。鍵-值編碼是一個與自動獲取和設定物件屬性值有關的機制(基於相關的非正式協議)。

您可以禁用自動的觀察者通告,並通過NSKeyValueObserving非正式協議及相關範疇中的方法實現手工通告,從而對KVO通告進行精化。

狀態模式

允許一個物件在其內部狀態改變時改變它的行為。物件看起來似乎修改了它的類。

iOS設計模式詳解

import Foundation

class Context {
    var state: IState?
    
    func request() {
        state?.handle()
    }
}

protocol IState {
    func handle()
}

class ConcreteStateA: IState {
    func handle() {
        print("狀態A")
    }
}

class ConcreteStateB: IState {
    func handle() {
        print("狀態B")
    }
}
複製程式碼

策略模式

定義一系列的演算法,把它們一個個封裝起來, 並且使它們可相互替換。本模式使得演算法可獨立於使用它的客戶而變化。

iOS設計模式詳解

import Foundation

class Context: IContextInterface {
    var quickStrategy: IStrategy?
    var insertStrategy: IStrategy?
    
    func quickSort() {
        quickStrategy?.sort()
    }
    
    func insertSort() {
        insertStrategy?.sort()
    }
}

protocol IContextInterface {
    func quickSort()
    func insertSort()
}

protocol IStrategy {
    func sort()
}

class QuickSortStrategy: IStrategy {
    func sort() {
        print("快排策略")
    }
}

class InsertSortStrategy: IStrategy {
    func sort() {
        print("插排策略")
    }
}
複製程式碼

模板方法模式

這種模式為某個操作中的演算法定義框架,並將演算法中的某些步驟推遲到子類實現。模板方法模式使子類可以重定義一個演算法中的特定步驟,而不需要改變演算法的結構。

iOS設計模式詳解

模板方法模式是Cocoa的基本設計,事實上也是一般的物件導向框架的基本設計。Cocoa中的模式使一個程式的定製元件可以將自己掛到演算法上,但何時和如何使用這些定製元件,由框架元件來決定。Cocoa類的程式設計介面通常包括一些需要被子類過載的方法。在執行環境中,框架會在自己所執行的任務過程中的某些點呼叫這些所謂的一般方法。一般方法為定製程式碼提供一個結構,目的是為當前正在執行且由框架類負責協調的任務加入具體程式的的行為和資料。

import Foundation

class Client {
    var operationC: AbstractClass?
    
    func operation() {
        //執行
        operationC?.templateMethod()
    }
}

class AbstractClass {
    func templateMethod() {
        print("執行當前邏輯...")
        
        //推遲留給子類處理邏輯...
        primitiveOperation1()
        primitiveOperation2()
    }
    
    func primitiveOperation1() {
        assert(false, "此方法需要繼承")
    }
    
    func primitiveOperation2() {
        assert(false, "此方法需要繼承")
    }
}

class ConcreteClass: AbstractClass {
    override func primitiveOperation1() {
        print("執行operation1邏輯")
    }
    
    override func primitiveOperation2() {
        print("執行operation2邏輯")
    }
}
複製程式碼

訪問者模式

表示一個作用於某物件結構中的各元素的操作。它使你可以在不改變各元素的類的前提下定義作用於這些元素的新操作。

iOS設計模式詳解

解析UML如下:

  • 定義訪問元素,包括抽象物件與具體物件。
  • 定義訪問者,在抽象物件中訪問者定義執行動作,訪問元素中接收訪問者的方法。訪問元素增加 acceptVisitor(visitor) 方法(接收訪問者),訪問者增加 visitA(A)、visitB(B)、visitC(C) 方法(根據元素物件的多少)。
  • 通過訪問元素呼叫訪問者中的事件。在訪問元素的 acceptVisitor 的實現方法中呼叫 [visitor visitX:self] 執行方法。
import Foundation

class Client: NSObject {
    func begin() {
        let visit1 = VisitorA()
        let visit2 = VisitorB()
        let element1 = VisitElementA()
        let element2 = VisitElementA()
        let element3 = VisitElementA()
        let element4 = VisitElementB()
        let element5 = VisitElementB()
        let array = [element1,element2,element3,element4,element5]
        for element in array {
            let number = arc4random()
            if number%2 == 0 {
                element.acceptVisit(visit: visit1)
            }
            else {
                element.acceptVisit(visit: visit2)
            }
        }
    }
}

class Visitor: NSObject {
    /// 訪問元素A
    func visitA(element :VisitElementA)  {
        
    }
    /// 訪問元素B
    func visitB(element :VisitElementB)  {
        
    }
}

class VisitorA: Visitor {
    override func visitA(element: VisitElementA) {
        NSLog("No1 Visit1 %@", element)
        /// 用 element 做某些操作
    }
    
    override func visitB(element: VisitElementB) {
        NSLog("No1 Visit2 %@", element)
        /// 用 element 做某些操作
    }
}

class VisitorB: Visitor {
    override func visitA(element: VisitElementA) {
        NSLog("No2 Visit1 %@", element)
        /// 用 element 做某些操作
    }
    
    override func visitB(element: VisitElementB) {
        NSLog("No2 Visit2 %@", element)
        /// 用 element 做某些操作
    }
}

class VisitElement: NSObject {
    func acceptVisit(visit :Visitor) {
    }
}

class VisitElementA: VisitElement {
    override func acceptVisit(visit :Visitor) {
        visit.visitA(element: self)
    }
}

class VisitElementB: VisitElement {
    override func acceptVisit(visit :Visitor) {
        visit.visitB(element: self)
    }
}
複製程式碼

關注我

歡迎關注公眾號:jackyshan,技術乾貨首發微信,第一時間推送。

iOS設計模式詳解

相關文章