学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

Swift花园之设计模式 中

委托代理

委托代理设计模式一种相对易用的两个对象间通信的方式。

实现 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 编译器会替你完成剩下的工作

现实世界中命令模式的用例:

  1. 各种按钮的动作
  2. 集合 / 表 视图的选择动作
  3. 在控制器间导航
  4. 历史管理 / 撤销管理器
  5. 事务行为
  6. 进度管理

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)
}
}

// 创建 EKEvent 源对象实例
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")

// 我们以 Event 协议的形式使用适配器
let adapter = EventAdapter(event: calendarEvent)
// adapter.title
// adapter.startDate
// adapter.endDate

还有一种用法,当你不得不使用几个已经存在的类或者结构体,而这些类都是 final 的,并且缺少某些功能,因此你需要基于它们之上构建一些新的接口。这种情况下,最好的选择就是实现一个包装类,也就是适配器。