設(shè)計模式(Swift) - 3.觀察者模式葛碧、建造者模式

上一篇 設(shè)計模式(Swift) - 2.單例模式假丧、備忘錄模式和策略模式中講了三種常見的設(shè)計模式.

  • 單例模式: 限制了類的實例化,一個類只能實例化一個對象,所有對單例對象的引用都是指向了同一個對象.
  • 備忘錄模式: 我們可以把某個對象保存在本地,并在適當?shù)臅r候恢復(fù)出來,app開發(fā)中最常見的應(yīng)用就是用戶數(shù)據(jù)的本地緩存.
  • 策略模式: 通過封裝業(yè)務(wù)分支來屏蔽業(yè)務(wù)細節(jié),只給出相關(guān)的策略接口作為切換.

1. 觀察者模式(Observer Pattern)

1. 觀察者模式概述

觀察者模式: 一個對象的變化,能夠被另一個對象知道.
本文除了介紹基于Runtime的KVO實現(xiàn)及其原理,還會自己動手去現(xiàn)實一套觀察者模式,畢竟在swift中使用Runtime并不被推薦.


  • 被觀察對象(subject): 用來被監(jiān)聽的可觀察對象.
  • 觀察者(observer): 用來監(jiān)聽被觀察對象.

2. 基于OC Runtime的觀察者模式實現(xiàn)

1. 實現(xiàn)一個繼承自NSObject的可觀察對象
 // @objcMembers 為了給類中每個屬性添加 @objc 關(guān)鍵詞,
@objcMembers public class KVOUser: NSObject {
    dynamic var name: String

    public init(name: String) {
        self.name = name
    }
}

@objcMembers 為了給類中每個屬性添加 @objc 關(guān)鍵詞,在swift4中繼承NSObject的子類的屬性不會暴露給OC的Runtime,所以只能手動添加

swift本身是門靜態(tài)語言,添加@objc 關(guān)鍵詞是為了讓屬性具有動態(tài)特性,可以動態(tài)的生成set和get方法,因為KVO就需要去操作set方法.

    // 注意kvoObserver的生命周期
    var kvoObserver: NSKeyValueObservation?
    let kvoUser = KVOUser(name: "Dariel")

    // 監(jiān)聽kvoUser name屬性的變化
    kvoObserver = kvoUser.observe(\.name, options: [.initial, .new]) {
            (user, change) in
            print("User's name is \(user.name)")
    }

第一個參數(shù)是路徑,這邊是簡略寫法.name, swift會自己轉(zhuǎn)成全路徑; options是 NSKeyValueObservingOptions, 這邊傳入的表示初始化的值和新的值

2. 使用繼承自NSObject的可觀察對象
 override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
      kvoUser.name = "John"
}

在任何地方改變kvoUser對象的name屬性,kvoObserver的observe方法都會回調(diào).

3. OC Runtime的觀察者模式實現(xiàn)原理

那么KVO是怎樣實現(xiàn)對對象屬性的監(jiān)聽的呢?
當給一個對象添加KVO之后,OC會通過Runtime將這個對象的isa指針指向(未設(shè)定KVO的對象的isa指針指向該對象的類對象)自己定義的一個原類的子類類對象(NSKVONotifying_xxx),這個子類類對象的isa指針指向原來對象的類對象,并調(diào)用這個類對象中的set方法,然后去通知監(jiān)聽器哪些值發(fā)生了改變.


KVO具體的實現(xiàn)原理

在Swift4中,并沒有在語言層級上支持KVO,如果要使用需要導(dǎo)入Foundation和被觀察對象必須繼承自NSObject,這種實現(xiàn)方式顯然不夠優(yōu)雅.

4. 實現(xiàn)一個不基于Runtime的觀察者模式

KVO的觀察者模式本質(zhì)上還是通過拿到屬性的set方法去搞事情,基于這樣的原理我們可以自己實現(xiàn).直接貼代碼,新建一個Observable的swift文件

public class Observable<Type> {
    
    // MARK: - Callback
    fileprivate class Callback {
        fileprivate weak var observer: AnyObject?
        fileprivate let options: [ObservableOptions]
        fileprivate let closure: (Type, ObservableOptions) -> Void
        
        fileprivate init(
            observer: AnyObject,
            options: [ObservableOptions],
            closure: @escaping (Type, ObservableOptions) -> Void) {
            
            self.observer = observer
            self.options = options
            self.closure = closure
        }
    }
    
    // MARK: - Properties
    public var value: Type {
        didSet {
            removeNilObserverCallbacks()
            notifyCallbacks(value: oldValue, option: .old)
            notifyCallbacks(value: value, option: .new)
        }
    }
    
    private func removeNilObserverCallbacks() {
        callbacks = callbacks.filter { $0.observer != nil }
    }
    
    private func notifyCallbacks(value: Type, option: ObservableOptions) {
        let callbacksToNotify = callbacks.filter { $0.options.contains(option) }
        callbacksToNotify.forEach { $0.closure(value, option) }
    }
    
    // MARK: - Object Lifecycle
    public init(_ value: Type) {
        self.value = value
    }
    
    // MARK: - Managing Observers
    private var callbacks: [Callback] = []
    
    
    /// 添加觀察者
    ///
    /// - Parameters:
    ///   - observer: 觀察者
    ///   - removeIfExists: 如果觀察者存在需要移除
    ///   - options: 被觀察者
    ///   - closure: 回調(diào)
    public func addObserver(
        _ observer: AnyObject,
        removeIfExists: Bool = true,
        options: [ObservableOptions] = [.new],
        closure: @escaping (Type, ObservableOptions) -> Void) {
        
        if removeIfExists {
            removeObserver(observer)
        }
        
        let callback = Callback(observer: observer, options: options, closure: closure)
        callbacks.append(callback)
        
        if options.contains(.initial) {
            closure(value, .initial)
        }
    }
    
    public func removeObserver(_ observer: AnyObject) {
        callbacks = callbacks.filter { $0.observer !== observer }
    }
}

// MARK: - ObservableOptions
public struct ObservableOptions: OptionSet {
    
    public static let initial = ObservableOptions(rawValue: 1 << 0)
    public static let old = ObservableOptions(rawValue: 1 << 1)
    public static let new = ObservableOptions(rawValue: 1 << 2)
    
    public var rawValue: Int
    
    public init(rawValue: Int) {
        self.rawValue = rawValue
    }
}

使用:

public class User {
    // 被觀察的屬性需要是Observable類型
    public let name: Observable<String>
    public init(name: String) {
        self.name = Observable(name)
    }
}
// 用來管理觀察者
public class Observer {}

var observer: Observer? // 當observer置為nil的時候,可觀察對象會自動釋放.
let user = User(name: "Made")
observer = Observer()
user.name.addObserver(observer!, options: [.new]) { name, change in     
    print("name:\(name), change:\(change)")                        
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {        
    user.name.value = "Amel"
}

注意: 在使用過程中,如果改變value, addObserver方法不調(diào)用,很有可能是Observer對象已經(jīng)被釋放掉了.

5. 觀察者模式的使用場景

觀察者模式一般用在MVC模式中,控制器需要監(jiān)聽某個模型屬性的改變,而模型不需要知道控制器的類型,因此多個控制器可以監(jiān)聽一個模型對象.

2. 建造者模式(Buidler Pattern)

1. 建造者模式概述

建造者模式可以一步步分解復(fù)雜業(yè)務(wù)場景的實現(xiàn)過程.


  • 管理者(Dircetor): 通常用來管理建造者,一般是個Controller
  • 建造者(Builder): 通常是個類,用來管理Product的創(chuàng)建和數(shù)據(jù)輸入
  • 產(chǎn)品(Product): 比較復(fù)雜的對象,可以是類或者結(jié)構(gòu)體,通常是個模型

1. 建造者模式舉例

1. Product
// MARK: - Product
public struct Person {
    public let area: Area
    public let character: Character
    public let hobby: Hobby
}
extension Person: CustomStringConvertible {
    public var description: String {
        return area.rawValue
    }
}
public enum Area: String { // 來自區(qū)域
    case ShangHai
    case ShenZhen
    case HangZhou
    case Toronto
}
public struct Character: OptionSet { // 性格
    
    public static let independent = Character(rawValue: 1 << 1) // 2
    public static let ambitious = Character(rawValue: 1 << 2) // 4
    public static let outgoing = Character(rawValue: 1 << 3) // 8
    public static let unselfish = Character(rawValue: 1 << 4) // 16
    public static let expressivity = Character(rawValue: 1 << 5) // 32

    public let rawValue: Int
    public init(rawValue: Int) {
        self.rawValue = rawValue
    }
}
public struct Hobby: OptionSet { // 愛好
    
    public static let mountaineering = Hobby(rawValue: 1 << 1)
    public static let boating = Hobby(rawValue: 1 << 2)
    public static let climbing = Hobby(rawValue: 1 << 3)
    public static let running = Hobby(rawValue: 1 << 4)
    public static let camping = Hobby(rawValue: 1 << 5)
    
    public let rawValue: Int
    public init(rawValue: Int) {
        self.rawValue = rawValue
    }
}

Person中定義了三個屬性,area地區(qū),character性格,hobby愛好,其中地區(qū)只能是一個值,性格和愛好可以支持多個值.Character和Hobby可以通過傳入一個值,設(shè)置多個值.

2. Builder
// MARK: - Builder
public class PersonStatistics {
    public private(set) var area: Area = .HangZhou
    public private(set) var characters: Character = []
    public private(set) var hobbys: Hobby = []
    
    private var outOfAreas: [Area] = [.Toronto]
    
    public func addCharacter(_ character: Character) {
        characters.insert(character)
    }
    
    public func removeCharacter(_ character: Character) {
        characters.remove(character)
    }
    
    public func addHobby(_ hobby: Hobby) {
        hobbys.insert(hobby)
    }
    
    public func removeHobby(_ hobby: Hobby) {
        hobbys.remove(hobby)
    }
    
    public func setArea(_ area: Area) throws {
        guard isAvailable(area) else { throw Error.OutOfArea }
        self.area = area
    }
    
    public func build() -> Person {
        return Person(area: area, character: characters, hobby: hobbys)
    }
    
    public func isAvailable(_ area: Area) -> Bool {
        return !outOfAreas.contains(area)
    }
    
    public enum Error: Swift.Error {
        case OutOfArea
    }
}

通過builder統(tǒng)一對Product進行管理,設(shè)置完數(shù)據(jù)之后再去創(chuàng)建Person對象.

3. Director
public class ManagerStatistics {

    public func createLiLeiData() throws -> Person {
        let builder = PersonStatistics()
        try builder.setArea(.HangZhou)
        builder.addCharacter(.ambitious)
        builder.addHobby([.climbing, .boating, .camping])
        return builder.build()
    }
    
    public func createLucyData() throws -> Person {
        let builder = PersonStatistics()
        try builder.setArea(.Toronto)
        builder.addCharacter([.ambitious, .independent, .outgoing])
        builder.addHobby([.boating, .climbing, .camping])
        return builder.build()
    }
}

通過Director去設(shè)置builder中的數(shù)據(jù).

 let manager = ManagerStatistics()
        
 if let Lucy = try? manager.createLucyData() {
     print(Lucy.description)
     print(Lucy.character)
     print(Lucy.hobby)
 }else {
     print("Out of area here")
 }
        
 if let Lilei = try? manager.createLiLeiData() {
     print(Lilei.description)
     print(Lilei.character)
     print(Lilei.hobby)
 }

2. 建造者模式的使用注意

建造者模式是用在創(chuàng)造比較復(fù)雜的Product,這個Product需要設(shè)置很多值,而這用構(gòu)造器又比較麻煩的情況下.如果Product比較簡單,那用構(gòu)造器就好了.

3. 總結(jié)

本篇主要講了用來對對象監(jiān)聽的觀察者模式和用在創(chuàng)建和管理復(fù)雜對象場景下的建造者模式.

示例代碼

參考:
The Swift Programming Language (Swift 4.1)
Objective-C編程之道
Design Patterns by Tutorials

如有疑問,歡迎留言 :-D

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末欺抗,一起剝皮案震驚了整個濱河市视事,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌礼旅,老刑警劉巖膳叨,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異痘系,居然都是意外死亡菲嘴,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進店門汰翠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來龄坪,“玉大人,你說我怎么就攤上這事复唤〗√铮” “怎么了?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵佛纫,是天一觀的道長妓局。 經(jīng)常有香客問我,道長呈宇,這世上最難降的妖魔是什么跟磨? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮攒盈,結(jié)果婚禮上抵拘,老公的妹妹穿的比我還像新娘。我一直安慰自己型豁,他們只是感情好僵蛛,可當我...
    茶點故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布尚蝌。 她就那樣靜靜地躺著,像睡著了一般充尉。 火紅的嫁衣襯著肌膚如雪飘言。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天驼侠,我揣著相機與錄音姿鸿,去河邊找鬼。 笑死倒源,一個胖子當著我的面吹牛苛预,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播笋熬,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼热某,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了胳螟?” 一聲冷哼從身側(cè)響起昔馋,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎糖耸,沒想到半個月后秘遏,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡嘉竟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年邦危,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片周拐。...
    茶點故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡铡俐,死狀恐怖凰兑,靈堂內(nèi)的尸體忽然破棺而出妥粟,到底是詐尸還是另有隱情,我是刑警寧澤吏够,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布勾给,位于F島的核電站,受9級特大地震影響锅知,放射性物質(zhì)發(fā)生泄漏播急。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一售睹、第九天 我趴在偏房一處隱蔽的房頂上張望桩警。 院中可真熱鬧,春花似錦昌妹、人聲如沸捶枢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烂叔。三九已至谨胞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蒜鸡,已是汗流浹背胯努。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留逢防,地道東北人叶沛。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像胞四,于是被迫代替她去往敵國和親恬汁。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 1辜伟、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明先生_X自主閱讀 15,969評論 3 119
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫氓侧、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 12,059評論 4 62
  • 進入蘭州城西已是下午五點多导狡。太陽似乎就在我頭頂约巷,明晃晃把天地照了個透亮。這是中衛(wèi)的太陽旱捧,這是銀川的太陽独郎,今天,這也...
    白楊樹在北方閱讀 480評論 0 4
  • 頭發(fā)長了,去理發(fā)贫橙。 有人理發(fā)喜歡找同一個人贪婉,我倒是想試試不同人的手藝。 洗完頭后卢肃,一個小伙子成了我的理發(fā)師疲迂。 看到...
    門羅公園閱讀 127評論 0 0
  • 盼望已久的六一兒童節(jié)終于到來了,奶奶早起給做的混沌莫湘,吃過早飯帶好行裝尤蒿,拿上跳蚤市場的用具等陪孩子一起去了學...
    張嘉宸媽媽閱讀 215評論 0 0