独孤九剑--设计模式(iOS行为型篇)

独孤九剑--设计模式(iOS创建型篇)
独孤九剑--设计模式(iOS结构型篇)

观察者模式(Observer)

定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新

UML类图

observer.png
  • Subject:抽象主题角色,把所有对观察者对象的引用保存在一个集合中,每个抽象主题角色都可以有任意数量的观察者。
  • ConcreteSubject:具体主题角色,在内部状态改变时,给所有注册过的观察者发出通知。
  • Observer:抽象观察者角色,为所有的观察者定义一个接口,在得到主题的通知更新自己。
  • ConcreteObserver:具体观察者角色,该角色实现抽象观察者角色所要求的更新接口,以便使本身的状态和主题的状态相协调。

iOS系统中的应用

iOS中NSNotificationCenter、KVO都是观察者模式的运用

示例代码

标准格式的观察者代码,详见demo中/自定义观察者/部分;
这里仿写一下系统NotificationCenter的简单版本,省去抽象角色Subject、ConcreteSubject,实现一个观察者功能;

  1. 新建通知中心单例
// ConcreteSubject
class NotificationCenter {
    static let `default` = NotificationCenter()
    
    private init() {}
 }
  1. NotificationCenter中定义观察者集合
// NotificationCenter可以添加不同name的监听,这里直接使用字典记录每个name下所有的观察者
var observers: [String : [Observation]] = [:]
  1. 封装观察的信息
// ConcreteObserver
class Notification: NSObject {
    let name: String
    var userInfo: [String : Any]?
    
    init(name: String, userInfo: [String : Any]? = nil) {
        self.name = name
        self.userInfo = userInfo
    }
}
  1. 定义观察者封装类
// 封装观察者对象、实现的selector; 
class Observation {
    // 封装观察者对象、实现的selector;
    let observer: Any
    let selector: Selector
    
    init(observer: Any, selector: Selector) {
        self.observer = observer
        self.selector = selector
    }
    
    // 响应通知
    func update(notification: Notification) {
        guard let aClass = observer as? NSObjectProtocol else { return }
        if aClass.responds(to: selector) {
            aClass.perform(selector, with: notification)
        }
    }
}
  1. NotificationCenter实现,添加、删除观察者、发送通知的功能
func add(observer: Any, selector: Selector, name: String) {
    let observation = Observation(observer: observer, selector: selector)
    if var obs = observers[name] {
        obs.append(observation)
        observers[name] = obs
    } else {
        let obs = [observation]
        observers[name] = obs
    }
}
func remove(_ observer: Any, name: String) {
    guard var observers = observers[name] else { return }
    
    for (index, observation) in observers.enumerated() {
        if let aClass = observation.observer as? NSObjectProtocol, let bClass = observer as? NSObjectProtocol  {
            if aClass.isEqual(bClass) {
                observers.remove(at: index)
                self.observers[name] = observers
                break
            }
        }
    }
}
func post(name: String, userInfo: [String : Any]? = nil) {
    let notification = Notification(name: name, userInfo: userInfo)
    post(notification: notification)
}
func post(notification: Notification) {
    guard let observers = observers[notification.name] else { return }
    for observation in observers {
        observation.update(notification: notification)
    }
}

一个简易版通知中心使用

NotificationCenter.default.add(observer: self, selector: #selector(notice(n:)), name: "test")
NotificationCenter.default.add(observer: PatternTest(), selector: #selector(notice(n:)), name: "test")
NotificationCenter.default.remove(self, name: "test")
NotificationCenter.default.post(name: "test")

ps:系统库中的NotificationCenter实现远比这个复杂,他需要兼容处理各种情况,但本质原理基本是这样的;
如对NotificationCenter系统实现有兴趣的可以参考:
NSNotificationCenter底层探究

中介者模式(Mediator)

用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变他们之间的交互

UML类图

mediator.png
  • Mediator:抽象中介者,定义了同事对象到中介者对象之间的接口。
  • ConcreteMediator:具体中介者,实现抽象中介者的方法,需要从具体的同事类那里接收消息,并且向具体的同事类发送信息。
  • Colleague:抽象同事类
  • ConcreteColleague:具体同事类,需要知道自己的行为即可,但是它们都需要认识中介者

示例

有3个控制器A,B,C,A,B,C三者之间都能相互跳转;
简单的代码实现:A,B,C内部均引用、耦合其他控制器;

// ViewControllerA
 switch random {
 case 0:
     self.present(ViewControllerB(), animated: true)
 default:
     self.present(ViewControllerC(), animated: true)
 }

// ViewControllerB
 switch random {
 case 0:
     self.present(ViewControllerA(), animated: true)
 default:
     self.present(ViewControllerC(), animated: true)
 }

// ViewControllerC
 switch random {
 case 0:
     self.present(ViewControllerA(), animated: true)
 default:
     self.present(ViewControllerB(), animated: true)
 }

它们间的关系如下:


可以看到,这还是只有3个类的情况,最多就已经有6种关系;按照无向图2个节点的关系数量计算,如果有n个对象,那就会有(n-1)xn种关系;
如果系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象;

使用中介者模式优化

  1. 抽象中介者协议
// Mediator
protocol RouterMediator {
    var viewControllers: [String : ViewControllerSubject] { get }

    mutating func register(vc: ViewControllerSubject, path: String)
    func router(from: ViewControllerSubject, to path: String)
}
  1. 实现具体路由中介
// ConcreteMediator
struct RouterStructure: RouterMediator {
    var viewControllers: [String : ViewControllerSubject]
    
    // 通过注册的方式 Mediator保存所有的ConcreteColleague
    mutating func register(vc: ViewControllerSubject, path: String) {
        viewControllers[path] = vc
    }
    
    func router(from: ViewControllerSubject, to path: String) {
        if let toVc = viewControllers[path] {
            from.present(toVc, animated: true)
        }
    }
}
  1. 抽象路由的Controller类,提供mediator引用
// Colleague
class ViewControllerSubject: UIViewController {
    var router: RouterMediator?
}
  1. 具体controller实现,所有跳转逻辑交由mediator实现
switch random {
case 0:
    self.router?.router(from: self, to: "/A")
default:
    self.router?.router(from: self, to: "/C")
}

// 其他vc代码类似

使用:

var mediator = RouterStructure()
let a = ViewControllerA()
a.router = mediator
mediator.register(vc: a, path: "/A")
....

关系图:

以上只是基于中介者模式的最基本代码实现,还有很多优化空间;比如完全没必要新建ViewControllerSubject类,可以直接使用extension实现;还有每次都需要实现注册代码,比较繁琐也容易出错,也完全可以使用动态特性、硬编码实现,通过类名字符串得到对应具体对象;
在中介者模式的基础上,可以实现一套完整的组件化框架;CTMediator就是一个很好的例子;

策略模式(Strategy)

定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式使得算法可独立于使用它的客户而变化

UML类图

strategy.png
  • Context:环境角色,持有一个策略类的引用,最终给客户端调用
  • Strategy:抽象策略角色,通常由一个接口或者抽象类实现
  • ConcreteStrategy:具体策略角色,包装了相关的算法和行为

示例

以一个将2个数加减乘的功能为例
(为了演示方便,只使用最简单的算法;实际上如果真的只是加减这么简单就完成不用策略了;这些算法可以联想成购物时不同的折扣算法)

  • 简单实现
func operate<T: Numeric>(a: T, b: T) -> T {
    switch type {
    case .add:
        return a + b
    case .sub:
        return a - b
    case .mul:
        return a * b
    }
}

把一堆算法塞到同一段代码中,然后使用一系列的if else 或switch case来决定哪个算法;
可以把相关算法分离成不同的类,成为策略。

  • 策略模式
  1. 抽象策略协议,提供算法接口
protocol Strategy {
    associatedtype T

    func algorithm(a: T, b: T) -> T
}
  1. 创建具体策略类,实现对应算法
struct AddStrategy<T: Numeric> : Strategy {
    func algorithm(a: T, b: T) -> T {
        return a + b
    }

}

struct SubStrategy<T: Numeric> : Strategy {
    func algorithm(a: T, b: T) -> T {
        return a - b
    }

}

struct MulStrategy<T: Numeric> : Strategy {
    func algorithm(a: T, b: T) -> T {
        return a * b
    }

}
  1. 创建context类,使用策略
struct STContext<S: Strategy> {
    var strategy: S
    
    func operate(a: S.T, b: S.T) -> S.T {
        return strategy.algorithm(a: a, b: b)
    }
}

使用:

let strategy1 = AddStrategy<Int>()
let strategy2 = MulStrategy<Double>()
let context1 = STContext(strategy: strategy1)
print(context1.operate(a: 1, b: 2))
let context2 = STContext(strategy: strategy2)
print(context2.operate(a: 1.1, b: 2.1))

策略模式和简单工厂模式的区别

从UML图看,策略模式和简单工厂模式非常相似;
简单工厂模式是创建型的模式,它接受类型指令创建符合要求的产品实例;而策略模式是行为型的模式,它接受已经创建好的算法实例,实现不同的行为。

状态模式(State)

允许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。

UML类图

state.png
  • Context:环境角色,定义操作接口,维护不同状态。
  • State:抽象状态角色,定义状态接口。
  • ConcreteState:具体状态角色,实现Context的一个状态所对应的行为。

示例

以线上定房为例,房间的状态有:空闲、已预订、已入住。空闲房间的状态可以转变为:已预订、已入住。已预订状态房间的状态可以转变为:已入住、空闲。已入住房间的状态可以转变为:空闲。

  1. 抽象房间状态,定义接口
// state
protocol RoomState {
    func book() -> Bool
    func checkIn() -> Bool
    func checkOut() -> Bool
}
  1. 创建具体房间状态类,实现对应方法

// concreteState
class FreeState : RoomState {
    func book() -> Bool {
        print("预约成功")
        return true
    }
    
    func checkIn() -> Bool {
        print("未预约")
        return false
    }
    
    func checkOut() -> Bool {
        print("未预约")
        return false
    }
}

class CheckInState : RoomState {
    func book() -> Bool {
        print("已入住,不能预约")
        return false
    }
    
    func checkIn() -> Bool {
        print("已有入住,不能再入住")
        return false
    }
    
    func checkOut() -> Bool {
        print("退房成功")
        return true
    }
}

class BookState : RoomState {
    func book() -> Bool {
        print("已有预约,不能再预约")
        return false
    }
    
    func checkIn() -> Bool {
        print("入住成功")
        return true
    }
    
    func checkOut() -> Bool {
        print("取消预约成功")
        return true
    }
}
  1. 创建房间环境类,定义实现操作接口,维护状态逻辑
// context
struct Room {
    // 所有状态
    let freeState = FreeState()
    let checkInState = CheckInState()
    let bookState = BookState()
    
    // 当前状态
    var state: RoomState
    
    init() {
        state = freeState
    }
    
    mutating func book() {
        // 预约
        let result = state.book()
        if result {
            // 预约成功 状态变更为已预约
            state = bookState
        }
    }
    
    mutating func checkIn() {
        // 入住
        let result = state.checkIn()
        if result {
            // 入住成功 状态变更为已入住
            state = checkInState
        }
    }
    
    mutating func checkOut() {
        // 退房
        let result = state.checkOut()
        if result {
            // 退房成功 状态变更为空闲
            state = freeState
        }
    }
}

使用:

var room = Room()
room.book()
room.book()
room.checkIn()

/*预约成功
已有预约,不能再预约
入住成功
*/

适用场景

  • 对象的行为依赖于它的状态,并且可以根据它的状态而改变它的相关行为
  • 代码中包含大量与对象状态相关的条件语句

状态模式和策略模式的区别

状态模式和策略模式也非常相似,都是由context来选择、调用具体的行为;
区别在于策略模式context角色,只引用并使用具体的某个strategy类,不同的strategy类没有转接关系;状态模式context角色,引用并使用了所有的state类,并在内部维护、切换不同的state,不同的state直接存在着转接关系;

命令模式(Command)

将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作。

UML类图

command.png
  • Command:抽象命令角色,声明一个给所有命令类的抽象接口。
  • ConcreteCommand:具体命令角色,定义一个接收者和行为之间的弱耦合;实现execute()方法,负责调用接收者的相应操作。
  • Invoker:请求者角色,负责调用命令对象执行请求。
  • Receiver:接收者角色,负责具体实施和执行一个请求。

示例

编写一个电视遥控功能,不同的按键处理不同的操作;

  1. 定义receiver电视类,支持打开、关闭、切换频道功能
// receiver
struct TV {
    func open() {
        print("tv open")
    }
    
    func close() {
        print("tv close")
    }
    
    func `switch`() {
        print("channel switch")
    }
}
  1. 定义抽象命令及具体命令
    每个具体命令对应不同的操作,内部实际是调用receiver处理;在具体命令执行时可以写入日志,实现记录请求日志
// command
protocol TVCommand {
    var tv: TV { get }
    
    func execute()
}

// concreteCommand
struct TVOpenCommand : TVCommand {
    var tv: TV
    
    func execute() {
        tv.open()
        
        // 记录请求日志
        print("执行了open命令")
    }
}

struct TVCloseCommand : TVCommand {
    var tv: TV
    
    func execute() {
        tv.close()
        
        // 记录请求日志
        print("执行了close命令")
    }
}

struct SwitchCommand : TVCommand {
    var tv: TV
    
    func execute() {
        tv.switch()
        
        // 记录请求日志
        print("执行了switch命令")
    }
}
  1. 定义Invoker命令执行类
  • 通过字典存储所有要执行的command,字典的key映射具体的command,实现用不同的请求对客户进行参数化
  • 记录所有command执行顺序,实现对请求排队
  • 支持删除command,模拟实现撤销功能;实际开发是可以使用NSUndoManager等实现撤销与恢复功能
// invoker
struct TVInvoker {
    var commandMaps: [String : TVCommand] = [:] // 存储所有command (对不同的请求进行参数化)
    var keys: [String] = [] // 记录command顺序
    
    mutating func addCommand(_ cm: TVCommand, for key: String) {
        keys.append(key)
        commandMaps[key] = cm
    }
    
    mutating func removeCommand(for key: String) {
        keys.removeAll(where: { $0 == key })
        commandMaps.removeValue(forKey: key)
    }
    
    func invoke(key: String) {
        if let command = commandMaps[key] {
            command.execute()
        }
    }
    
    func invoke() {
        // 按顺序执行 (对请求排队)
        for key in keys {
            invoke(key: key)
        }
    }
}

测试:

let receiver = TV()
let commandOpen = TVOpenCommand(tv: receiver)
let commandClose = TVCloseCommand(tv: receiver)
let commandSwitch = SwitchCommand(tv: receiver)
var invoker2 = TVInvoker()
invoker2.addCommand(commandOpen, for: "o")
invoker2.addCommand(commandSwitch, for: "s")
invoker2.addCommand(commandClose, for: "c")
//  invoker2.invoke(key: "o")
//  invoker2.invoke(key: "c")
invoker2.removeCommand(for: "s")
invoker2.invoke()

/*tv open
执行了open命令
tv close
执行了close命令
*/

适用场景

  • 应用程序需要支持撤销与恢复功能
  • 需要在不同的时间指定请求、将请求排队
  • 用对象参数化一个动作以执行操作
  • 记录修改日志,在系统故障时能重做一遍

命令模式在iOS系统中的使用

NSInvocation,NSUndoManager是该模式的典型应用;

NSInvocation对应ConcreteCommand

@interface NSInvocation : NSObject

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

@property (nullable, assign) id target;
@property SEL selector;
...
- (void)invoke;
....
@end

其中target就是接受者receiver,并将receiver的操作action用selector保存;invoke方法即为具体命令的执行execute;

NSInvocation.png

简单使用

 NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
 NSInvocation *command = [NSInvocation invocationWithMethodSignature:sig];
 command.target = self;
 command.selector = @selector(test:);
 NSString *param = @"from command";
 [command setArgument:&param atIndex:2];
 [command invoke];

完整代码


参考:
《Objective-C编程之道》
《精通Swift设计模式》
《大话设计模式》
设计模式:状态模式(State)

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

推荐阅读更多精彩内容