Design Patterns 实战应用|封装 Socket.IO 即时通讯架构与七大设计模式解析
针对 iOS 与 Android 即时通讯需求,解析封装 Socket.IO 过程中遇到的复杂连线管理与跨平台挑战,透过七大设计模式优化连线复用、离线事件缓存与状态管理,提升系统稳定性与扩充性,助你打造高效且易维护的双向通讯架构。
Click here to view the English version of this article.
點擊這裡查看本文章正體中文版本。
基于 SEO 考量,本文标题与描述经 AI 调整,原始版本请参考内文。
Design Patterns 的实战应用纪录
封装 Socket.IO Client Library 需求时遇到的问题场景及解决方法应用到的 Design Patterns
Photo by Daniel McCullough
前言
此篇文章是真实的需求开发,所运用到 Design Pattern 解决问题的场景记录;内容篇幅会涵盖需求背景、实际遇到的问题场景 (What?)、为何要套用 Pattern 解决问题 (Why?)、实作上如何使用 (How?),建议可以从头阅读会比较有连贯性。
本文会介绍四个开发此需求遇到的场景及七个解决此场景的 Design Patterns 应用。
背景
组织架构
敝司于今年拆分出 Feature Teams (multiple) 与 Platform Team;前者不必多说主要负责使用者端需求、Platform Team 这边则面对的是公司内部的成员,其中一个工作项目就是技术引入、基础建设及做好系统性整合,为 Feature Teams 开发需求时先锋铺好道路。
当前需求
Feature Teams 要将原本的讯息功能 (进页面打 API 拿讯息资料,要更新最新讯息只能重整) 改为 即时通讯 (能即时收到最新讯息、对传讯息)。
Platform Team 工作
Platform Team 著重的点不只是当下的即时通讯需求,而是长远的建设与复用性;评估后 webSocket 双向通讯的机制在现代 App 中是不可或缺,除了此次的需求之外,以后也有很多机会都会用到,加上人力资源许可,故投入协助设计开发介面。
目标:
封装 Pinkoi Server Side 与 Socket.IO 通讯、身份验证逻辑
封装 Socket.IO 烦琐操作,提供基于 Pinkoi 商业需求的可扩充及方便使用介面
统一双平台介面 (Socket.IO 的 Android 与 iOS Client Side Library 支援的功能及介面不相同)
Feature 端无需了解 Socket.IO 机制
Feature 端无需管理复杂的连线状态
未来有 webSocket 双向通讯需求能直接使用
时间及人力:
iOS & Android 各投入一位
开发时程:时程 3 周
技术细节
Web & iOS & Android 三平台均会支援此 Feature;要引入 webSocket 双向通讯协议来实现,后端预计直接使用 Socket.io 服务。
首先要说 Socket != WebSocket
关于 Socket 与 WebSocket 及技术细节可参考以下两篇文章:
简而言之:
1
2
Socket 是 TCP/UDP 传输层的抽象封装介面,而 WebSocket 是应用层的传输协议。
Socket 与 WebSocket 的关系就像狗跟热狗的关系一样,没有关系。
Socket.IO 是 Engine.IO 的一层抽象操作封装,Engine.IO 则是对 WebSocket 的使用封装,每层只负责对上对下之间的交流,不允许贯穿操作(e.g. Socket.IO 直接操作 WebSocket 连线)。
Socket.IO/Engine.IO 除了基本的 WebSocket 连线外还实做了很多方便好用的功能集合(e.g. 离线发送 Event 机制、类似 Http Request 机制、Room/Group 机制…等等)。
Platform Team 这层的主要职责是桥接 Socket.IO 与 Pinkoi Server Side 之间的逻辑,供应上层 Feature Teams 开发功能时使用。
Socket.IO Swift Client 有坑
已许久未更新 (最新一版还在 2019),不确定是否还有在维护。
Client & Server Side Socket IO Version 要对齐,Server Side 可加上
{allowEIO3: true}
/ 或 Client Side 指定相同版本.version
否则怎么连都连不上。命名方式、介面与官网范例很多都对不起来。
Socket.io 官网范例都是拿 Web 做介绍,实际上 Swift Client 并不一定有全支援官网写的功能 。 此次实作发现 iOS 这边 Library 并未实现离线发送 Event 机制 (我们是自行实现的,请往后继续阅读)
建议有要采用 Socket.IO 前先实验看看你想要的机制是否支援。
Socket.IO Swift Client 是基于 Starscream WebSocket Library 的封装,必要时可降级使用 Starscream。
1
背景资讯补充到此结束,接下来进入正题。
Design Patterns
设计模式说穿了就只是软体设计当中常见问题的解决方案,不一定要用设计模式才能开发、设计模式不一定能适用所有场景、也没人说不能自行归纳出新的设计模式。
The Catalog of Design Patterns
但现有的设计模式 (The 23 Gang of Four Design Patterns) 已是软体设计中的共同知识,只要提到 XXX Pattern 大家脑中就会有相应的架构蓝图,不需多做解释、后续维护也比较好知道脉络、且已是经过业界验证的方法不太需要花时间审视物件依赖问题;在适合的场景选用适合的模式可以降低沟通及维护成本,提升开发效率。
设计模式可以组合使用,但不建议对现有设计模式魔改、强行为套用而套用、套用不符合分类的 Pattern (e.g. 用责任练模式来产生物件),会失去使用的意义更可能造成后续接手的人的误会。
本篇会提到的 Design Patterns:
会逐一在后面解释什么场境用了、为何要用。
本文著重在 Design Pattern 的应用,而非 Socket.IO 的操作,部分示例会因为描述方便而有所删减, 无法适用真实的 Socket.IO 封装 。
因篇幅有限,本文不会详细介绍每个设计模式的架构,请先点各个模式的连结进入了解该模式的架构后再继续阅读。
Demo Code 会使用 Swift 撰写。
需求场景 1.
What?
使用相同的 Path 在不同页面、Object 请求 Connection 时能复用取得相同的物件。
Connection 需为抽象介面,不直接依赖 Socket.IO Object
Why?
减少记忆体开销及重复连线的时间、流量成本。
为未来抽换成其他框架预留空间
How?
Singleton Pattern :创建型 Pattern,保证一个物件只会有一个实体。
Flywieght Pattern :结构型 Pattern,基于共享多个物件相同的状态,重复使用。
Factory Pattern :创建型 Pattern,抽象物件产生方法,使其能在外部抽换。
实际案例使用:
Singleton Pattern:
ConnectionManager
在 App Lifecycle 中仅存在一个的物件,用来管理Connection
取用操作。Flywieght Pattern:
ConnectionPool
顾名思义就是 Connection 的共用池子,统一从这个池子的方法拿出 Connection,其中逻辑就会包含当发现 URL Path 一样时直接给予已经在池子里的 Connection。ConnectionHandler
则做为Connection
的外在操作、状态管理器。Factory Pattern:
ConnectionFactory
搭配上面 Flywieght Pattern 当发现池子没有可复用的Connection
时则用此工厂介面去产生。
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
import Combine
import Foundation
protocol Connection {
var url: URL {get}
var id: UUID {get}
init(url: URL)
func connect()
func disconnect()
func sendEvent(_ event: String)
func onEvent(_ event: String) -> AnyPublisher<Data?, Never>
}
protocol ConnectionFactory {
func create(url: URL) -> Connection
}
class ConnectionPool {
private let connectionFactory: ConnectionFactory
private var connections: [Connection] = []
init(connectionFactory: ConnectionFactory) {
self.connectionFactory = connectionFactory
}
func getOrCreateConnection(url: URL) -> Connection {
if let connection = connections.first(where: { $0.url == url }) {
return connection
} else {
let connection = connectionFactory.create(url: url)
connections.append(connection)
return connection
}
}
}
class ConnectionHandler {
private let connection: Connection
init(connection: Connection) {
self.connection = connection
}
func getConnectionUUID() -> UUID {
return connection.id
}
}
class ConnectionManager {
static let shared = ConnectionManager(connectionPool: ConnectionPool(connectionFactory: SIOConnectionFactory()))
private let connectionPool: ConnectionPool
private init(connectionPool: ConnectionPool) {
self.connectionPool = connectionPool
}
//
func requestConnectionHandler(url: URL) -> ConnectionHandler {
let connection = connectionPool.getOrCreateConnection(url: url)
return ConnectionHandler(connection: connection)
}
}
// Socket.IO Implementation
class SIOConnection: Connection {
let url: URL
let id: UUID = UUID()
required init(url: URL) {
self.url = url
//
}
func connect() {
//
}
func disconnect() {
//
}
func sendEvent(_ event: String) {
//
}
func onEvent(_ event: String) -> AnyPublisher<Data?, Never> {
//
return PassthroughSubject<Data?, Never>().eraseToAnyPublisher()
}
}
class SIOConnectionFactory: ConnectionFactory {
func create(url: URL) -> Connection {
//
return SIOConnection(url: url)
}
}
//
print(ConnectionManager.shared.requestConnectionHandler(url: URL(string: "wss://pinkoi.com/1")!).getConnectionUUID().uuidString)
print(ConnectionManager.shared.requestConnectionHandler(url: URL(string: "wss://pinkoi.com/1")!).getConnectionUUID().uuidString)
print(ConnectionManager.shared.requestConnectionHandler(url: URL(string: "wss://pinkoi.com/2")!).getConnectionUUID().uuidString)
// output:
// D99F5429-1C6D-4EB5-A56E-9373D6F37307
// D99F5429-1C6D-4EB5-A56E-9373D6F37307
// 599CF16F-3D7C-49CF-817B-5A57C119FE31
需求场景 2.
What?
如背景技术细节所述,Socket.IO Swift Client 的 Send Event
并不支援离线发送 (但 Web/Android 版的 Library 却可以),因此 iOS 端需要自行实现此功能。
1
神奇的是 Socket.IO Swift Client - onEvent 是支援离线订阅的。
Why?
跨平台功能统一
程式码容易理解
How?
- Command Pattern :行为型 Pattern,将操作包装成对象,提供队列、延迟、取消…等等集合操作。
- Command Pattern:
SIOManager
为与 Socket.IO 沟通的最底层封装,其中的send
、request
方法都是对 Socket.IO Send Event 的操作,当发现当前 Socket.IO 处于断线状态,则将请求参数放到bufferedCommands
中,当连上之后就逐一拿出来处理 (First In First Out)。
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
protocol BufferedCommand {
var sioManager: SIOManagerSpec? { get set }
var event: String { get }
func execute()
}
struct SendBufferedCommand: BufferedCommand {
let event: String
weak var sioManager: SIOManagerSpec?
func execute() {
sioManager?.send(event)
}
}
struct RequestBufferedCommand: BufferedCommand {
let event: String
let callback: (Data?) -> Void
weak var sioManager: SIOManagerSpec?
func execute() {
sioManager?.request(event, callback: callback)
}
}
protocol SIOManagerSpec: AnyObject {
func connect()
func disconnect()
func onEvent(event: String, callback: @escaping (Data?) -> Void)
func send(_ event: String)
func request(_ event: String, callback: @escaping (Data?) -> Void)
}
enum ConnectionState {
case created
case connected
case disconnected
case reconnecting
case released
}
class SIOManager: SIOManagerSpec {
var state: ConnectionState = .disconnected {
didSet {
if state == .connected {
executeBufferedCommands()
}
}
}
private var bufferedCommands: [BufferedCommand] = []
func connect() {
state = .connected
}
func disconnect() {
state = .disconnected
}
func send(_ event: String) {
guard state == .connected else {
appendBufferedCommands(connectionCommand: SendBufferedCommand(event: event, sioManager: self))
return
}
print("Send:\(event)")
}
func request(_ event: String, callback: @escaping (Data?) -> Void) {
guard state == .connected else {
appendBufferedCommands(connectionCommand: RequestBufferedCommand(event: event, callback: callback, sioManager: self))
return
}
print("request:\(event)")
}
func onEvent(event: String, callback: @escaping (Data?) -> Void) {
//
}
func appendBufferedCommands(connectionCommand: BufferedCommand) {
bufferedCommands.append(connectionCommand)
}
func executeBufferedCommands() {
// First in, first out
bufferedCommands.forEach { connectionCommand in
connectionCommand.execute()
}
bufferedCommands.removeAll()
}
func removeAllBufferedCommands() {
bufferedCommands.removeAll()
}
}
let manager = SIOManager()
manager.send("send_event_1")
manager.send("send_event_2")
manager.request("request_event_1") { _ in
//
}
manager.state = .connected
同理也可以实现到 onEvent
上。
延伸:可以再套用 Proxy Pattern ,将 Buffer 功能视为一种 Proxy。
需求场景 3.
What?
Connection 有多个状态,有序的状态与状态间切换、各状态允许不同的操作。
Created:物件被建立,允许 ->
Connected
或直接进Disconnected
Connected:已连上 Socket.IO,允许 ->
Disconnected
Disconnected:已与 Socket.IO 断线,允许 ->
Reconnectiong
、Released
Reconnectiong:正在尝试重新连上 Socket.IO,允许 ->
Connected
、Disconnected
Released:物件已被标示为等待被记忆体回收,不允许任何操作及切换状态
Why?
状态与状态的切换逻辑跟表述不容易
各状态要限制操作方法(e.g. State = Released 时无法 Call Send Event),直接使用 if. .else 会让程式难以维护阅读
How?
Finite State Machine :管理状态间的切换
State Pattern :行为型 Pattern,对象的状态有变化时,有不同的相应处理
Finite State Machine :
SIOConnectionStateMachine
为状态机实作,currentSIOConnectionState
为当前状态,created、connected、disconnected、reconnecting、released
表列出此状态机可能的切换状态。enterXXXState() throws
为从 Current State 进入某个状态时的允许与不允许(throw error)实作。State Pattern :
SIOConnectionState
为所有状态会用到的操作方法介面抽象。
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
protocol SIOManagerSpec: AnyObject {
func connect()
func disconnect()
func onEvent(event: String, callback: @escaping (Data?) -> Void)
func send(_ event: String)
func request(_ event: String, callback: @escaping (Data?) -> Void)
}
enum ConnectionState {
case created
case connected
case disconnected
case reconnecting
case released
}
class SIOManager: SIOManagerSpec {
var state: ConnectionState = .disconnected {
didSet {
if state == .connected {
executeBufferedCommands()
}
}
}
private var bufferedCommands: [BufferedCommand] = []
func connect() {
state = .connected
}
func disconnect() {
state = .disconnected
}
func send(_ event: String) {
guard state == .connected else {
appendBufferedCommands(connectionCommand: SendBufferedCommand(event: event, sioManager: self))
return
}
print("Send:\(event)")
}
func request(_ event: String, callback: @escaping (Data?) -> Void) {
guard state == .connected else {
appendBufferedCommands(connectionCommand: RequestBufferedCommand(event: event, callback: callback, sioManager: self))
return
}
print("request:\(event)")
}
func onEvent(event: String, callback: @escaping (Data?) -> Void) {
//
}
func appendBufferedCommands(connectionCommand: BufferedCommand) {
bufferedCommands.append(connectionCommand)
}
func executeBufferedCommands() {
// First in, first out
bufferedCommands.forEach { connectionCommand in
connectionCommand.execute()
}
bufferedCommands.removeAll()
}
func removeAllBufferedCommands() {
bufferedCommands.removeAll()
}
}
let manager = SIOManager()
manager.send("send_event_1")
manager.send("send_event_2")
manager.request("request_event_1") { _ in
//
}
manager.state = .connected
//
class SIOConnectionStateMachine {
private(set) var currentSIOConnectionState: SIOConnectionState!
private var created: SIOConnectionState!
private var connected: SIOConnectionState!
private var disconnected: SIOConnectionState!
private var reconnecting: SIOConnectionState!
private var released: SIOConnectionState!
init() {
self.created = SIOConnectionCreatedState(stateMachine: self)
self.connected = SIOConnectionConnectedState(stateMachine: self)
self.disconnected = SIOConnectionDisconnectedState(stateMachine: self)
self.reconnecting = SIOConnectionReconnectingState(stateMachine: self)
self.released = SIOConnectionReleasedState(stateMachine: self)
self.currentSIOConnectionState = created
}
func enterConnected() throws {
if [created.connectionState, reconnecting.connectionState].contains(currentSIOConnectionState.connectionState) {
enter(connected)
} else {
throw SIOConnectionStateMachineError("\(currentSIOConnectionState.connectionState) can't enter to Connected")
}
}
func enterDisconnected() throws {
if [created.connectionState, connected.connectionState, reconnecting.connectionState].contains(currentSIOConnectionState.connectionState) {
enter(disconnected)
} else {
throw SIOConnectionStateMachineError("\(currentSIOConnectionState.connectionState) can't enter to Disconnected")
}
}
func enterReconnecting() throws {
if [disconnected.connectionState].contains(currentSIOConnectionState.connectionState) {
enter(reconnecting)
} else {
throw SIOConnectionStateMachineError("\(currentSIOConnectionState.connectionState) can't enter to Reconnecting")
}
}
func enterReleased() throws {
if [disconnected.connectionState].contains(currentSIOConnectionState.connectionState) {
enter(released)
} else {
throw SIOConnectionStateMachineError("\(currentSIOConnectionState.connectionState) can't enter to Released")
}
}
private func enter(_ state: SIOConnectionState) {
currentSIOConnectionState = state
}
}
protocol SIOConnectionState {
var connectionState: ConnectionState { get }
var stateMachine: SIOConnectionStateMachine { get }
init(stateMachine: SIOConnectionStateMachine)
func onConnected() throws
func onDisconnected() throws
func connect(socketManager: SIOManagerSpec) throws
func disconnect(socketManager: SIOManagerSpec) throws
func release(socketManager: SIOManagerSpec) throws
func request(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws
func onEvent(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws
func send(socketManager: SIOManagerSpec, event: String) throws
}
struct SIOConnectionStateMachineError: Error {
let message: String
init(_ message: String) {
self.message = message
}
var localizedDescription: String {
return message
}
}
class SIOConnectionCreatedState: SIOConnectionState {
let connectionState: ConnectionState = .created
let stateMachine: SIOConnectionStateMachine
required init(stateMachine: SIOConnectionStateMachine) {
self.stateMachine = stateMachine
}
func onConnected() throws {
try stateMachine.enterConnected()
}
func onDisconnected() throws {
try stateMachine.enterDisconnected()
}
func release(socketManager: SIOManagerSpec) throws {
throw SIOConnectionStateMachineError("ConnectedState can't release!")
}
func onEvent(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
func request(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
func send(socketManager: SIOManagerSpec, event: String) throws {
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
func connect(socketManager: SIOManagerSpec) throws {
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
func disconnect(socketManager: SIOManagerSpec) throws {
throw SIOConnectionStateMachineError("CreatedState can't disconnect!")
}
}
class SIOConnectionConnectedState: SIOConnectionState {
let connectionState: ConnectionState = .connected
let stateMachine: SIOConnectionStateMachine
required init(stateMachine: SIOConnectionStateMachine) {
self.stateMachine = stateMachine
}
func onConnected() throws {
//
}
func onDisconnected() throws {
try stateMachine.enterDisconnected()
}
func release(socketManager: SIOManagerSpec) throws {
throw SIOConnectionStateMachineError("ConnectedState can't release!")
}
func onEvent(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
func request(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
func send(socketManager: SIOManagerSpec, event: String) throws {
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
func connect(socketManager: SIOManagerSpec) throws {
throw SIOConnectionStateMachineError("ConnectedState can't connect!")
}
func disconnect(socketManager: SIOManagerSpec) throws {
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
}
class SIOConnectionDisconnectedState: SIOConnectionState {
let connectionState: ConnectionState = .disconnected
let stateMachine: SIOConnectionStateMachine
required init(stateMachine: SIOConnectionStateMachine) {
self.stateMachine = stateMachine
}
func onConnected() throws {
try stateMachine.enterConnected()
}
func onDisconnected() throws {
//
}
func release(socketManager: SIOManagerSpec) throws {
try stateMachine.enterReleased()
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
func onEvent(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
func request(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
func send(socketManager: SIOManagerSpec, event: String) throws {
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
func connect(socketManager: SIOManagerSpec) throws {
try stateMachine.enterReconnecting()
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
func disconnect(socketManager: SIOManagerSpec) throws {
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
}
class SIOConnectionReconnectingState: SIOConnectionState {
let connectionState: ConnectionState = .reconnecting
let stateMachine: SIOConnectionStateMachine
required init(stateMachine: SIOConnectionStateMachine) {
self.stateMachine = stateMachine
}
func onConnected() throws {
try stateMachine.enterConnected()
}
func onDisconnected() throws {
try stateMachine.enterDisconnected()
}
func release(socketManager: SIOManagerSpec) throws {
throw SIOConnectionStateMachineError("ReconnectState can't release!")
}
func onEvent(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
func request(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
func send(socketManager: SIOManagerSpec, event: String) throws {
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
func connect(socketManager: SIOManagerSpec) throws {
throw SIOConnectionStateMachineError("ReconnectState can't connect!")
}
func disconnect(socketManager: SIOManagerSpec) throws {
// allow
// can use Helper to reduce the repeating code
// e.g. helper.XXX(socketManager: SIOManagerSpec, ....)
}
}
class SIOConnectionReleasedState: SIOConnectionState {
let connectionState: ConnectionState = .released
let stateMachine: SIOConnectionStateMachine
required init(stateMachine: SIOConnectionStateMachine) {
self.stateMachine = stateMachine
}
func onConnected() throws {
throw SIOConnectionStateMachineError("ReleasedState can't onConnected!")
}
func onDisconnected() throws {
throw SIOConnectionStateMachineError("ReleasedState can't onDisconnected!")
}
func release(socketManager: SIOManagerSpec) throws {
throw SIOConnectionStateMachineError("ReleasedState can't release!")
}
func request(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
throw SIOConnectionStateMachineError("ReleasedState can't request!")
}
func onEvent(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
throw SIOConnectionStateMachineError("ReleasedState can't receiveOn!")
}
func send(socketManager: SIOManagerSpec, event: String) throws {
throw SIOConnectionStateMachineError("ReleasedState can't send!")
}
func connect(socketManager: SIOManagerSpec) throws {
throw SIOConnectionStateMachineError("ReleasedState can't connect!")
}
func disconnect(socketManager: SIOManagerSpec) throws {
throw SIOConnectionStateMachineError("ReleasedState can't disconnect!")
}
}
do {
let stateMachine = SIOConnectionStateMachine()
// mock on socket.io connect:
// socketIO.on(connect){
try stateMachine.currentSIOConnectionState.onConnected()
try stateMachine.currentSIOConnectionState.send(socketManager: manager, event: "test")
try stateMachine.currentSIOConnectionState.release(socketManager: manager)
try stateMachine.currentSIOConnectionState.send(socketManager: manager, event: "test")
// }
} catch {
print("error: \(error)")
}
// output:
// error: SIOConnectionStateMachineError(message: "ConnectedState can\'t release!")
需求场景 3.
What?
结合场景 1. 2.,有了 ConnectionPool
享元池子加上 State Pattern 状态管理后;我们继续往下延伸,如背景目标所述,Feature 端不需去管背后 Connection 的连线机制;因此我们建立了一个轮询器 (命名为 ConnectionKeeper
) 会定时扫描 ConnectionPool
中强持有的 Connection
,并在发生以下状况时做操作:
Connection
有人在使用且状态非Connected
:将状态改为Reconnecting
并尝试重新连线Connection
已无人使用且状态为Connected
:将状态改为Disconnected
Connection
已无人使用且状态为Disconnected
:将状态改为Released
并从ConnectionPool
中移除
Why?
三个操作有上下关系且互斥 (disconnected -> released or reconnecting)
可弹性抽换、增加状况操作
未封装的话只能将三个判断及操作直接写在方法中 (难以测试其中逻辑)
e.g:
1
2
3
4
5
6
7
if !connection.isOccupie() && connection.state == .connected then
... connection.disconnected()
else if !connection.isOccupie() && state == .released then
... connection.release()
else if connection.isOccupie() && state == .disconnected then
... connection.reconnecting()
end
How?
- Chain Of Resposibility :行为型 Pattern,顾名思义是一条链,每个节点都有相应的操作,输入资料后节点可决定是否要操作还是丢给下一个节点处理,另一个现实应用是 iOS Responder Chain 。
照定义 Chain of responsibility Pattern 是不允许某个节点已经接下处理资料,但处理完又丢给下一个节点继续处理, 要做就做完,不然不要做 。
如果是上述场景比较适合的应该是 Interceptor Pattern 。
- Chain of responsibility:
ConnectionKeeperHandler
为炼的节点抽象,特别抽出canExcute
方法避免发生上述 这个节点接下来处理了,但做完又想呼叫后面的节点继续执行的状况、handle
为炼的节点串连、excute
为要处理的话会怎么处理的逻辑。ConnectionKeeperHandlerContext
用来存放会用到的资料,isOccupie
代表 Connection 有无人在使用。
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
enum ConnectionState {
case created
case connected
case disconnected
case reconnecting
case released
}
protocol Connection {
var connectionState: ConnectionState {get}
var url: URL {get}
var id: UUID {get}
init(url: URL)
func connect()
func reconnect()
func disconnect()
func sendEvent(_ event: String)
func onEvent(_ event: String) -> AnyPublisher<Data?, Never>
}
// Socket.IO Implementation
class SIOConnection: Connection {
let connectionState: ConnectionState = .created
let url: URL
let id: UUID = UUID()
required init(url: URL) {
self.url = url
//
}
func connect() {
//
}
func disconnect() {
//
}
func reconnect() {
//
}
func sendEvent(_ event: String) {
//
}
func onEvent(_ event: String) -> AnyPublisher<Data?, Never> {
//
return PassthroughSubject<Data?, Never>().eraseToAnyPublisher()
}
}
//
struct ConnectionKeeperHandlerContext {
let connection: Connection
let isOccupie: Bool
}
protocol ConnectionKeeperHandler {
var nextHandler: ConnectionKeeperHandler? { get set }
func handle(context: ConnectionKeeperHandlerContext)
func execute(context: ConnectionKeeperHandlerContext)
func canExcute(context: ConnectionKeeperHandlerContext) -> Bool
}
extension ConnectionKeeperHandler {
func handle(context: ConnectionKeeperHandlerContext) {
if canExcute(context: context) {
execute(context: context)
} else {
nextHandler?.handle(context: context)
}
}
}
class DisconnectedConnectionKeeperHandler: ConnectionKeeperHandler {
var nextHandler: ConnectionKeeperHandler?
func execute(context: ConnectionKeeperHandlerContext) {
context.connection.disconnect()
}
func canExcute(context: ConnectionKeeperHandlerContext) -> Bool {
if context.connection.connectionState == .connected && !context.isOccupie {
return true
}
return false
}
}
class ReconnectConnectionKeeperHandler: ConnectionKeeperHandler {
var nextHandler: ConnectionKeeperHandler?
func execute(context: ConnectionKeeperHandlerContext) {
context.connection.reconnect()
}
func canExcute(context: ConnectionKeeperHandlerContext) -> Bool {
if context.connection.connectionState == .disconnected && context.isOccupie {
return true
}
return false
}
}
class ReleasedConnectionKeeperHandler: ConnectionKeeperHandler {
var nextHandler: ConnectionKeeperHandler?
func execute(context: ConnectionKeeperHandlerContext) {
context.connection.disconnect()
}
func canExcute(context: ConnectionKeeperHandlerContext) -> Bool {
if context.connection.connectionState == .disconnected && !context.isOccupie {
return true
}
return false
}
}
let connection = SIOConnection(url: URL(string: "wss://pinkoi.com")!)
let disconnectedHandler = DisconnectedConnectionKeeperHandler()
let reconnectHandler = ReconnectConnectionKeeperHandler()
let releasedHandler = ReleasedConnectionKeeperHandler()
disconnectedHandler.nextHandler = reconnectHandler
reconnectHandler.nextHandler = releasedHandler
disconnectedHandler.handle(context: ConnectionKeeperHandlerContext(connection: connection, isOccupie: false))
需求场景 4.
What?
我们封装出的 Connection
需要经过 setup 后才能使用,例如给予 URL Path、设定 Config…等等
Why?
可以弹性的增减构建开口
可复用构建逻辑
未封装的话,外部可以不照预期操作类别
e.g.:
1
2
3
4
5
6
7
8
❌
let connection = Connection()
connection.send(event) // unexpected method call, should call .connect() first
✅
let connection = Connection()
connection.connect()
connection.send(event)
// but...who knows???
How?
- Builder Pattern :创建型 Pattern,能够分步骤构建对象及复用构建方法。
- Builder Pattern:
SIOConnectionBuilder
为Connection
的构建器,负责设定、存放构建Connection
时会用到的资料;ConnectionConfiguration
抽象介面用来保证要使用Connection
前必须呼叫.connect()
才能拿到Connection
实体。
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
83
84
85
86
87
88
89
90
91
92
93
94
95
enum ConnectionState {
case created
case connected
case disconnected
case reconnecting
case released
}
protocol Connection {
var connectionState: ConnectionState {get}
var url: URL {get}
var id: UUID {get}
init(url: URL)
func connect()
func reconnect()
func disconnect()
func sendEvent(_ event: String)
func onEvent(_ event: String) -> AnyPublisher<Data?, Never>
}
// Socket.IO Implementation
class SIOConnection: Connection {
let connectionState: ConnectionState = .created
let url: URL
let id: UUID = UUID()
required init(url: URL) {
self.url = url
//
}
func connect() {
//
}
func disconnect() {
//
}
func reconnect() {
//
}
func sendEvent(_ event: String) {
//
}
func onEvent(_ event: String) -> AnyPublisher<Data?, Never> {
//
return PassthroughSubject<Data?, Never>().eraseToAnyPublisher()
}
}
//
class SIOConnectionClient: ConnectionConfiguration {
private let url: URL
private let config: [String: Any]
init(url: URL, config: [String: Any]) {
self.url = url
self.config = config
}
func connect() -> Connection {
// set config
return SIOConnection(url: url)
}
}
protocol ConnectionConfiguration {
func connect() -> Connection
}
class SIOConnectionBuilder {
private(set) var config: [String: Any] = [:]
func setConfig(_ config: [String: Any]) -> SIOConnectionBuilder {
self.config = config
return self
}
// url is required parameter
func build(url: URL) -> ConnectionConfiguration {
return SIOConnectionClient(url: url, config: self.config)
}
}
let builder = SIOConnectionBuilder().setConfig(["test":123])
let connection1 = builder.build(url: URL(string: "wss://pinkoi.com/1")!).connect()
let connection2 = builder.build(url: URL(string: "wss://pinkoi.com/1")!).connect()
延伸:这里也可以再套用 Factory Pattern ,将用工厂产出 SIOConnection
。
完结!
以上就是本次封装 Socket.IO 中遇到的四个场景及七个使用到解决问题的 Design Patterns。
最后附上此次封装 Socket.IO 的完整设计蓝图
与文中命名、示范略为不同,这张图才是真实的设计架构;有机会再请原设计者分享设计理念及开源。
Who?
谁做了这些设计跟负责 Socket.IO 封装专案呢?
Sean Zheng , Android Engineer @ Pinkoi
主要架构设计者、Design Pattern 评估套用、在 Android 端使用 Kotlin 实现设计。
ZhgChgLi , Enginner Lead/iOS Enginner @ Pinkoi
Platform Team 专案负责人、Pair programming、在 iOS 端使用 Swift 实现设计、讨论并提出质疑(a.k.a. 出一张嘴)及最后撰写本文与大家分享。
延伸阅读
Design Patterns 的实战应用纪录 — In WKWebView with Builder, Strategy & Chain of Responsibility Pattern
有任何问题及指教欢迎 与我联络 。
本文首次发表于 Medium (点击查看原始版本),由 ZMediumToMarkdown 提供自动转换与同步技术。