设计模式(Swift) - 3.观察者模式、建造者模式

上一篇 设计模式(Swift) - 2.单例模式、备忘录模式和策略模式中讲了三种常见的设计模式.

  • 单例模式: 限制了类的实例化,一个类只能实例化一个对象,所有对单例对象的引用都是指向了同一个对象.
  • 备忘录模式: 我们可以把某个对象保存在本地,并在适当的时候恢复出来,app开发中最常见的应用就是用户数据的本地缓存.
  • 策略模式: 通过封装业务分支来屏蔽业务细节,只给出相关的策略接口作为切换.

1. 观察者模式(Observer Pattern)

1. 观察者模式概述

观察者模式: 一个对象的变化,能够被另一个对象知道.
本文除了介绍基于Runtime的KVO实现及其原理,还会自己动手去现实一套观察者模式,毕竟在swift中使用Runtime并不被推荐.


  • 被观察对象(subject): 用来被监听的可观察对象.
  • 观察者(observer): 用来监听被观察对象.

2. 基于OC Runtime的观察者模式实现

1. 实现一个继承自NSObject的可观察对象
 // @objcMembers 为了给类中每个属性添加 @objc 关键词,
@objcMembers public class KVOUser: NSObject {
    dynamic var name: String

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

@objcMembers 为了给类中每个属性添加 @objc 关键词,在swift4中继承NSObject的子类的属性不会暴露给OC的Runtime,所以只能手动添加

swift本身是门静态语言,添加@objc 关键词是为了让属性具有动态特性,可以动态的生成set和get方法,因为KVO就需要去操作set方法.

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

    // 监听kvoUser name属性的变化
    kvoObserver = kvoUser.observe(\.name, options: [.initial, .new]) {
            (user, change) in
            print("User's name is \(user.name)")
    }

第一个参数是路径,这边是简略写法.name, swift会自己转成全路径; options是 NSKeyValueObservingOptions, 这边传入的表示初始化的值和新的值

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

在任何地方改变kvoUser对象的name属性,kvoObserver的observe方法都会回调.

3. OC Runtime的观察者模式实现原理

那么KVO是怎样实现对对象属性的监听的呢?
当给一个对象添加KVO之后,OC会通过Runtime将这个对象的isa指针指向(未设定KVO的对象的isa指针指向该对象的类对象)自己定义的一个原类的子类类对象(NSKVONotifying_xxx),这个子类类对象的isa指针指向原来对象的类对象,并调用这个类对象中的set方法,然后去通知监听器哪些值发生了改变.


KVO具体的实现原理

在Swift4中,并没有在语言层级上支持KVO,如果要使用需要导入Foundation和被观察对象必须继承自NSObject,这种实现方式显然不够优雅.

4. 实现一个不基于Runtime的观察者模式

KVO的观察者模式本质上还是通过拿到属性的set方法去搞事情,基于这样的原理我们可以自己实现.直接贴代码,新建一个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: 回调
    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方法不调用,很有可能是Observer对象已经被释放掉了.

5. 观察者模式的使用场景

观察者模式一般用在MVC模式中,控制器需要监听某个模型属性的改变,而模型不需要知道控制器的类型,因此多个控制器可以监听一个模型对象.

2. 建造者模式(Buidler Pattern)

1. 建造者模式概述

建造者模式可以一步步分解复杂业务场景的实现过程.


  • 管理者(Dircetor): 通常用来管理建造者,一般是个Controller
  • 建造者(Builder): 通常是个类,用来管理Product的创建和数据输入
  • 产品(Product): 比较复杂的对象,可以是类或者结构体,通常是个模型

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 { // 来自区域
    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地区,character性格,hobby爱好,其中地区只能是一个值,性格和爱好可以支持多个值.Character和Hobby可以通过传入一个值,设置多个值.

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统一对Product进行管理,设置完数据之后再去创建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去设置builder中的数据.

 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. 建造者模式的使用注意

建造者模式是用在创造比较复杂的Product,这个Product需要设置很多值,而这用构造器又比较麻烦的情况下.如果Product比较简单,那用构造器就好了.

3. 总结

本篇主要讲了用来对对象监听的观察者模式和用在创建和管理复杂对象场景下的建造者模式.

示例代码

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

如有疑问,欢迎留言 :-D

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容

  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,937评论 3 118
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,614评论 4 59
  • 进入兰州城西已是下午五点多。太阳似乎就在我头顶,明晃晃把天地照了个透亮。这是中卫的太阳,这是银川的太阳,今天,这也...
    白杨树在北方阅读 435评论 0 4
  • 头发长了,去理发。 有人理发喜欢找同一个人,我倒是想试试不同人的手艺。 洗完头后,一个小伙子成了我的理发师。 看到...
    门罗公园阅读 104评论 0 0
  • 盼望已久的六一儿童节终于到来了,奶奶早起给做的混沌,吃过早饭带好行装,拿上跳蚤市场的用具等陪孩子一起去了学...
    张嘉宸妈妈阅读 194评论 0 0