委托代理
委托代理设计模式一种相对易用的两个对象间通信的方式。
实现 Swift 中的委托代理
委托协议:一个委托者将任务委派出去,一个代理对象实现委托协议,完成”老板“交待的工作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| import Foundation
protocol InputDelegate {
var shouldContinueListening: Bool { get }
func didStartListening() func didReceive(input: String) }
class InputHandler {
var delegate: InputDelegate?
func listen() { self.delegate?.didStartListening()
repeat { guard let input = readLine() else { continue } self.delegate?.didReceive(input: input) } while self.delegate?.shouldContinueListening ?? false } }
struct InputReceiver: InputDelegate {
var shouldContinueListening: Bool { return true }
func didStartListening() { print(" \"hi\", 离开时请说 \"bye\":") }
func didReceive(input: String) { switch input { case "hi": print(" Hello world!") case "bye": print(" Bye!") exit(0) default: print(" 找不到命令,请重试:") } } }
let inputHandler = InputHandler() let inputReceiver = InputReceiver() inputHandler.delegate = inputReceiver inputHandler.listen()
|
Apple 在底层其实做着一样的事情,比如 UICollectionViewDataSource
, UICollectionViewDelegate
等等。你只需要实现代理,而它们提供协议和委托者。
Weak属性,委托和类
类的所有代理对象都应该是 weak 属性,否则你可能会陷入糟糕的引用循环。
1 2 3 4 5 6 7 8 9 10 11 12
| protocol InputDelegate: class { }
class InputHandler {
weak var delegate: InputDelegate? }
class InputReceiver: InputDelegate { }
|
builder
实现 builder
模式,隐藏创建拥有大量属性的对象的复杂性。
builder 模式如何工作?
Builder
设计模式的意图在于从复杂对象的表达中分离出构造对象的部分。
因此,如果你的对象有很多属性,而你想要隐藏构造过程的复杂性,那么你可以写一个 builder
,通过它来构造你的对象。这个 builder
可以简单如一个 build
方法,也可以复杂如一个控制全部构建过程的外部类。这些都是由环境决定的。
简单发射器 builder
SKEmitterNode
是一个很不错的例子。如果你打算创建自定义发射器并且通过程序设置属性,通过是在 SpriteKit
游戏当中。那么像下面这样的一个发射器 builder
类就是一种很合理的解决方案。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| class EmitterBuilder { func build() -> SKEmitterNode { let emitter = SKEmitterNode() emitter.particleTexture = SKTexture(imageNamed: "MyTexture") emitter.particleBirthRate = 100 emitter.particleLifetime = 60 emitter.particlePositionRange = CGVector(dx: 100, dy: 100) emitter.particleSpeed = 10 emitter.particleColor = .red emitter.particleColorBlendFactor = 1 return emitter } }
EmitterBuilder().build()
|
简单主题 builder
想象一下你正在为你的 UIKit 应用设计一个主题引擎,它有许多自定义字体、颜色等。构造标准主题时一个 builder 会很有用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| struct Theme { let textColor: UIColor? let backgroundColor: UIColor? }
class ThemeBuilder {
enum Style { case light case dark }
func build(_ style: Style) -> Theme { switch style { case .light: return Theme(textColor: .black, backgroundColor: .white) case .dark: return Theme(textColor: .white, backgroundColor: .black) } } }
let builder = ThemeBuilder() let light = builder.build(.light) let dark = builder.build(.dark)
|
“链式” URL builder
利用这种方式,你可以用各种方法配置对象,每一个方法都会返回同一个 builder 对象,然后在最后一步构建最终产品。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| class URLBuilder { private var components: URLComponents
init() { self.components = URLComponents() } func set(scheme: String) -> URLBuilder { self.components.scheme = scheme return self }
func set(host: String) -> URLBuilder { self.components.host = host return self } func set(port: Int) -> URLBuilder { self.components.port = port return self }
func set(path: String) -> URLBuilder { var path = path if !path.hasPrefix("/") { path = "/" + path } self.components.path = path return self }
func addQueryItem(name: String, value: String) -> URLBuilder { if self.components.queryItems == nil { self.components.queryItems = [] } self.components.queryItems?.append(URLQueryItem(name: name, value: value)) return self } func build() -> URL? { return self.components.url } }
let url = URLBuilder() .set(scheme: "https") .set(host: "localhost") .set(path: "api/v1") .addQueryItem(name: "sort", value: "name") .addQueryItem(name: "order", value: "asc") .build()
|
带director的builder模式
让我们来见一见director
对象,它的功能是将 builder
和配置部分的代码解耦。
举个例子,你本来想创建一个圆形,可是忽然改主意要换成一个正方形。这可以很容易实现。只需要创建一个新的 builder,其他的都不变。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| protocol NodeBuilder { var name: String { get set } var color: SKColor { get set } var size: CGFloat { get set }
func build() -> SKShapeNode }
protocol NodeDirector { var builder: NodeBuilder { get set } func build() -> SKShapeNode }
class CircleNodeBuilder: NodeBuilder { var name: String = "" var color: SKColor = .clear var size: CGFloat = 0
func build() -> SKShapeNode { let node = SKShapeNode(circleOfRadius: self.size) node.name = self.name node.fillColor = self.color return node } }
class PlayerNodeDirector: NodeDirector {
var builder: NodeBuilder init(builder: NodeBuilder) { self.builder = builder } func build() -> SKShapeNode { self.builder.name = "Hello" self.builder.size = 32 self.builder.color = .red return self.builder.build() } }
let builder = CircleNodeBuilder() let director = PlayerNodeDirector(builder: builder) let player = director.build()
|
基于block的builders
一种更 Swift 的方法可以使用 block 来代替 builder 类来配置对象。当然,可能有人要问这样还算不算 builder 模式…
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| extension UILabel {
static func build(block: ((UILabel) -> Void)) -> UILabel { let label = UILabel(frame: .zero) block(label) return label } }
let label = UILabel.build { label in label.translatesAutoresizingMaskIntoConstraints = false label.text = "Hello wold!" label.font = UIFont.systemFont(ofSize: 12) }
|
注意,builder 的实现根据特定的用例可能有所差别。有的时候 builder 会跟工厂结合。每个人对这个模式的解读都不同,但并不是问题。设计模式是制作精良的准则,但有的时候规则是可以被打破的。
命令
当你需要为支持即时调用的不同的动作提供一个通用接口时,命令模式可以派上用场。通常来说,命令是一个封装了执行动作需要用到的所有数据和方法的单一对象。
命令常被用来处理人机接口的动作,实现撤销管理器,或者管理事务。让我们来看一下 Swift 中如何实现一个命令模式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| import Foundation
protocol Command { func execute() }
class HelpCommand: Command {
func execute() { Help().info() } }
class Help {
func info() { print("""
Commander v1.0
Available commands:
help This command ls List documents
Bye!
""") } }
class ListCommand: Command {
func execute() { List().homeDirectoryContents() } }
class List {
func homeDirectoryContents() { let fileManager = FileManager.default guard let documentsURL = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { print("Could not open documents directory") exit(-1) } do { let fileURLs = try fileManager.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil) print("\n\t Listing documents directory:\n") print(fileURLs.map { "\t\t " + $0.lastPathComponent }.joined(separator: "\n\n") + "\n" ) } catch { print(error.localizedDescription) exit(-1) }
} }
class App {
var commands: [String:Command] = [:]
init() { self.commands["help"] = HelpCommand() self.commands["ls"] = ListCommand() }
func run() { let arguments = CommandLine.arguments[1...]
guard let key = arguments.first, self.commands[key] != nil else { print("Usage: ./command.swift [\(self.commands.keys.joined(separator: "|"))]") exit(-1) }
self.commands[key]!.execute() } }
App().run()
|
保存下这个文件,然后在终端中输入./file-name.swift
运行它。Swift 编译器会替你完成剩下的工作
现实世界中命令模式的用例:
- 各种按钮的动作
- 集合 / 表 视图的选择动作
- 在控制器间导航
- 历史管理 / 撤销管理器
- 事务行为
- 进度管理
Apple 甚至还为它实现了一个专门的类叫 NSInvocation
,不过由于这个类的动作行为,在 Swift 中并不可用。
适配器
将一个不兼容的对象转换成目标接口或者类,这是适配器模式的作用。
下面这件东西是适配器模式在现实世界中最贴切的表达。
实现适配器模式
在 Swift 中,实现适配器相当简单。你只需要创建一个新对象,把旧对象放进去,然后在新对象的类或者结构体上实现要求的接口。换言之,这个将源角色包装起来的类就是我们用来适配目标接口的适配器。
源角色
我们要适配目标接口的对象(例如,旧的USB-A接口)。
适配器
包装原始对象并且实现某个目标接口要求的对象。
目标
需要和源角色一起使用的对象 (我们的USB-C插槽)。
Swift中如何使用适配器模式?
当你想要在代码中集成第三方库时,可以用到适配器。第三方库的接口可能不符合你的需求,因此你可以在整个SDK或者后端API节点上创建一个包装器,以便提供统一的接口。
举个例子,我们将包装一个 EKEvent
对象,实现一个全新的协议。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| import Foundation import EventKit
protocol Event { var title: String { get } var startDate: String { get } var endDate: String { get } }
class EventAdapter {
private lazy var dateFormatter: DateFormatter = { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy. MM. dd. HH:mm" return dateFormatter }()
private var event: EKEvent
init(event: EKEvent) { self.event = event } }
extension EventAdapter: Event {
var title: String { return self.event.title } var startDate: String { return self.dateFormatter.string(from: event.startDate) } var endDate: String { return self.dateFormatter.string(from: event.endDate) } }
let dateFormatter = DateFormatter() dateFormatter.dateFormat = "MM/dd/yyyy HH:mm"
let calendarEvent = EKEvent(eventStore: EKEventStore()) calendarEvent.title = "Adapter tutorial deadline" calendarEvent.startDate = dateFormatter.date(from: "07/30/2018 10:00") calendarEvent.endDate = dateFormatter.date(from: "07/30/2018 11:00")
let adapter = EventAdapter(event: calendarEvent)
|
还有一种用法,当你不得不使用几个已经存在的类或者结构体,而这些类都是 final
的,并且缺少某些功能,因此你需要基于它们之上构建一些新的接口。这种情况下,最好的选择就是实现一个包装类,也就是适配器。