Design Patterns の実践応用記録
Socket.IO クライアントライブラリのラッピングで直面した問題シナリオと解決方法に適用したデザインパターン

Photo by Daniel McCullough
はじめに
此記事は実際の要件開発で、デザインパターンを用いて問題を解決した事例の記録です。内容は要件の背景、実際に直面した問題の場面(What?)、なぜパターンを適用して問題を解決するのか(Why?)、実装でどのように使うか(How?)を含みます。最初から読むことをおすすめします。
本記事では、この要件の開発で直面した4つのシナリオと、それらのシナリオを解決するための7つのデザインパターンの適用について紹介します。
背景
組織構成
敝社は今年、Feature Teams(複数)とPlatform Teamに分割しました。前者は主にユーザー側の要件を担当し、Platform Teamは社内メンバーを対象としています。Platform Teamの主な業務の一つは技術導入、インフラ整備、システム統合を行い、Feature Teamsの開発要件に先駆けて道を整えることです。
現在の要件
Feature Teams は、元のメッセージ機能(ページ遷移時にAPIでメッセージデータを取得し、最新メッセージの更新はリロードが必要)をリアルタイム通信(最新メッセージをリアルタイムで受信し、メッセージ送信が可能)に変更する必要があります。
Platform Team の仕事
Platform Team が重視しているのは、単なる即時通信のニーズだけでなく、長期的な構築と再利用性です。評価の結果、webSocket の双方向通信の仕組みは現代のアプリにおいて不可欠であり、今回の要件以外にも将来的に多くの機会で利用されるため、人員リソースが許す限り、設計と開発の支援に注力しています。
目標:
-
Pinkoi サーバーサイドと Socket.IO 通信、認証ロジックのラップ
-
Socket.IO の複雑な操作を封装し、Pinkoi のビジネスニーズに応じた拡張性と使いやすさを備えたインターフェースを提供する
-
統一されたクロスプラットフォームインターフェース (Socket.IO の Android と iOS クライアントライブラリは機能やインターフェースが異なります)
-
Feature 側は Socket.IO の仕組みを理解する必要がない
-
Feature 側で複雑な接続状態を管理する必要がない
-
将来、WebSocket の双方向通信が必要な場合は直接利用可能です。
時間と人員:
-
iOS と Android にそれぞれ1名ずつ配置
-
開発スケジュール:期間 3 週間
技術的詳細情報
Web & iOS & Android の三つのプラットフォームでこの機能をサポートします。双方向通信プロトコルとして WebSocket を導入し、バックエンドは直接 Socket.io サービスを使用する予定です。
まず最初に言いたいのは Socket != WebSocket です
Socket と WebSocket および技術的な詳細については、以下の2つの記事をご参照ください:
簡単に言うと:
Socket は TCP/UDP トランスポート層の抽象的なラッピングインターフェースであり、WebSocket はアプリケーション層の通信プロトコルです。
Socket と WebSocket の関係は、犬とホットドッグの関係のように、全く関係がありません。

Socket.IO は Engine.IO の一層の抽象操作ラッパーであり、Engine.IO は WebSocket の利用をラップしています。各層は上下間の通信のみを担当し、貫通操作(例:Socket.IO が直接 WebSocket 接続を操作すること)は許可されていません。
Socket.IO/Engine.IO は基本的な WebSocket 接続に加えて、多くの便利な機能群(例:オフラインイベント送信機能、HTTPリクエストに似た機構、Room/Group 機能など)を実装しています。
Platform Team の主な役割は、Socket.IO と Pinkoi サーバーサイド間のロジックを橋渡しし、上位の Feature Teams が機能開発を行う際に利用できるようにすることです。
Socket.IO Swift Client に落とし穴あり
-
長い間更新されておらず(最新バージョンはまだ2019年で)、メンテナンスされているかは不明です。
-
クライアントとサーバーの Socket.IO バージョンは合わせる必要があります。サーバー側では
{allowEIO3: true}を追加するか、クライアント側で同じバージョンを.versionで指定してください。
そうしないと接続できません。 -
命名規則やインターフェースが公式サンプルと多く異なっています。
-
Socket.IO 公式サイトのサンプルはすべて Web を対象にしており、実際には Swift クライアントが公式サイトの機能をすべてサポートしているわけではありません。
今回の実装で、iOS のライブラリにはオフライン時のイベント送信機能が実装されていないことが判明しました。
(私たちは独自に実装していますので、引き続きお読みください)
Socket.IO を採用する前に、必要な機能がサポートされているかどうかをテストして確認することをお勧めします。
Socket.IO Swift Client は Starscream WebSocket ライブラリをベースにしたラッパーであり、必要に応じて Starscream にフォールバックできます。
背景情報の補足はここまでです。次に本題に入ります。
デザインパターン
設計パターンとは、ソフトウェア設計におけるよくある問題の解決策に過ぎません。設計パターンを使わなければ開発できないわけでもなく、すべての場面に適用できるわけでもありませんし、新しい設計パターンを独自にまとめても問題ありません。

The Catalog of Design Patterns
しかし既存の設計パターン(The 23 Gang of Four Design Patterns)はソフトウェア設計の共通知識であり、XXXパターンと言えば誰もが対応するアーキテクチャの青写真を思い浮かべるため、詳しい説明は不要です。後続の保守も文脈を把握しやすく、業界で検証済みの手法なのでオブジェクト依存問題を検討する時間もあまり必要ありません。適切な場面で適切なパターンを選択することで、コミュニケーションや保守コストを削減し、開発効率を向上させることができます。
デザインパターンは組み合わせて使用できますが、既存のデザインパターンを無理に改変したり、無理やり適用したり、分類に合わないパターンを適用すること(例:チェーン・オブ・リスポンシビリティパターンでオブジェクトを生成する)は推奨されません。そうすると使用の意義を失い、後から引き継ぐ人の誤解を招く可能性があります。
本記事で取り上げるデザインパターン:
後でそれぞれのシナリオで何を使い、なぜ使ったのかを順に説明します。
本記事はデザインパターンの応用に重点を置いており、Socket.IOの操作方法についてではありません。説明を簡潔にするため、一部の例は省略しており、実際のSocket.IOのラッピングには適用できません。
本文の分量の関係で、各デザインパターンの構造について詳細には説明しません。各パターンのリンクをクリックして構造を理解してから、読み進めてください。
Demo Code は Swift で記述します。
要求シナリオ 1.
何?
-
同じパスで異なるページやオブジェクトが接続を要求する際に、同じオブジェクトを再利用できるようにする。
-
Connection は抽象インターフェースであり、Socket.IO オブジェクトに直接依存しないこと
なぜ?
-
メモリ使用量と重複接続の時間、通信コストを削減する。
-
将来的に他のフレームワークに切り替える余地を残す。
どのように?
-
Singleton Pattern :生成に関するパターンで、オブジェクトがただ一つのインスタンスだけを持つことを保証します。
-
Flyweight Pattern :構造型パターンで、複数のオブジェクトが同じ状態を共有し、再利用することに基づいています。
-
Factory Pattern :生成に関するパターンで、オブジェクトの生成方法を抽象化し、外部から差し替え可能にします。
実際のケース使用:

-
Singletonパターン:
ConnectionManagerはアプリのライフサイクル中に一つだけ存在し、Connectionの取得と操作を管理します。 -
Flyweightパターン:
ConnectionPoolは名前の通り Connection の共有プールであり、このプールのメソッドから Connection を取得します。その中のロジックは、URL Path が同じ場合は既にプール内にある Connection を直接返します。
ConnectionHandlerはConnectionの外部操作および状態管理を担当します。 -
Factoryパターン:
ConnectionFactoryは上記の Flyweightパターン と組み合わせて、プールに再利用可能なConnectionがない場合にこのファクトリーインターフェースを使って生成します。
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 実装
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)
// 出力例:
// D99F5429-1C6D-4EB5-A56E-9373D6F37307
// D99F5429-1C6D-4EB5-A56E-9373D6F37307
// 599CF16F-3D7C-49CF-817B-5A57C119FE31
要求シナリオ 2.
何?
背景技術の詳細にあるように、Socket.IO Swift Client の Send Event はオフライン送信をサポートしていません(しかし Web/Android 版のライブラリは対応しています)。そのため、iOS 側でこの機能を独自に実装する必要があります。
驚くべきことに、Socket.IO Swift Client の onEvent はオフライン購読をサポートしています。
なぜ?
-
クロスプラットフォーム機能の統一
-
コードが理解しやすい
どうやって?
- Command Pattern :振る舞いパターンで、操作をオブジェクトとしてラップし、キューイング、遅延、キャンセルなどの一括操作を提供します。

- Command Pattern:
SIOManagerは Socket.IO と通信する最下層のラッパーであり、その中のsendやrequestメソッドは Socket.IO の送信イベント操作に対応しています。現在の Socket.IO が切断状態であると判断した場合、リクエストパラメータをbufferedCommandsに保存し、接続が復旧した際に順番に取り出して処理します(先入れ先出し)。
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("送信:\(event)")
}
func request(_ event: String, callback: @escaping (Data?) -> Void) {
guard state == .connected else {
appendBufferedCommands(connectionCommand: RequestBufferedCommand(event: event, callback: callback, sioManager: self))
return
}
print("リクエスト:\(event)")
}
func onEvent(event: String, callback: @escaping (Data?) -> Void) {
//
}
func appendBufferedCommands(connectionCommand: BufferedCommand) {
bufferedCommands.append(connectionCommand)
}
func executeBufferedCommands() {
// 先入れ先出し
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 にも実装可能です。
拡張:Buffer 機能を Proxy の一種として扱うために、Proxy Pattern を適用することもできます。
要求シナリオ 3.
何?
Connection は複数の状態を持ち、状態間の切り替えは順序立てられており、各状態で許可される操作が異なります。


-
Created:オブジェクトが作成され、
Connectedまたは直接Disconnectedに遷移可能 -
Connected:Socket.IOに接続済み、許可 ->
Disconnected -
Disconnected:Socket.IO との接続が切断されました。許可される状態 ->
Reconnectiong、Released -
Reconnectiong:Socket.IOへの再接続を試み中、許可される状態 ->
Connected、Disconnected -
Released:オブジェクトはメモリ回収待ちとしてマークされており、操作や状態変更は許可されていません
なぜ?
-
状態と状態の切り替えロジックや表現が難しい
-
各状態で操作方法を制限する必要があります(例:State = Released のときは Send Event を呼び出せません)。if…else を直接使うと、コードの保守や読みやすさが難しくなります。
どうやって?
-
Finite State Machine :状態遷移の管理
-
State Pattern :オブジェクトの状態変化に応じて異なる振る舞いを実現するパターンです。

-
Finite State Machine :
SIOConnectionStateMachineは状態機の実装であり、currentSIOConnectionStateは現在の状態を示します。created、connected、disconnected、reconnecting、releasedはこの状態機が遷移可能な状態を列挙しています。
enterXXXState() throwsは現在の状態から特定の状態に入る際の許可・不許可(エラーをスローする)を実装しています。 -
State Pattern :
SIOConnectionStateはすべての状態で使用される操作メソッドのインターフェース抽象です。
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() {
// 先入れ先出し
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) は Connected に遷移できません")
}
}
func enterDisconnected() throws {
if [created.connectionState, connected.connectionState, reconnecting.connectionState].contains(currentSIOConnectionState.connectionState) {
enter(disconnected)
} else {
throw SIOConnectionStateMachineError("\(currentSIOConnectionState.connectionState) は Disconnected に遷移できません")
}
}
func enterReconnecting() throws {
if [disconnected.connectionState].contains(currentSIOConnectionState.connectionState) {
enter(reconnecting)
} else {
throw SIOConnectionStateMachineError("\(currentSIOConnectionState.connectionState) は Reconnecting に遷移できません")
}
}
func enterReleased() throws {
if [disconnected.connectionState].contains(currentSIOConnectionState.connectionState) {
enter(released)
} else {
throw SIOConnectionStateMachineError("\(currentSIOConnectionState.connectionState) は 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("CreatedState はリリースできません!")
}
func onEvent(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: helper.XXX(socketManager: SIOManagerSpec, ....)
}
func request(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: helper.XXX(socketManager: SIOManagerSpec, ....)
}
func send(socketManager: SIOManagerSpec, event: String) throws {
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: helper.XXX(socketManager: SIOManagerSpec, ....)
}
func connect(socketManager: SIOManagerSpec) throws {
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: helper.XXX(socketManager: SIOManagerSpec, ....)
}
func disconnect(socketManager: SIOManagerSpec) throws {
throw SIOConnectionStateMachineError("CreatedState は切断できません!")
}
}
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 はリリースできません!")
}
func onEvent(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: helper.XXX(socketManager: SIOManagerSpec, ....)
}
func request(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: helper.XXX(socketManager: SIOManagerSpec, ....)
}
func send(socketManager: SIOManagerSpec, event: String) throws {
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: helper.XXX(socketManager: SIOManagerSpec, ....)
}
func connect(socketManager: SIOManagerSpec) throws {
throw SIOConnectionStateMachineError("ConnectedState は接続できません!")
}
func disconnect(socketManager: SIOManagerSpec) throws {
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: 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()
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: helper.XXX(socketManager: SIOManagerSpec, ....)
}
func onEvent(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: helper.XXX(socketManager: SIOManagerSpec, ....)
}
func request(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: helper.XXX(socketManager: SIOManagerSpec, ....)
}
func send(socketManager: SIOManagerSpec, event: String) throws {
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: helper.XXX(socketManager: SIOManagerSpec, ....)
}
func connect(socketManager: SIOManagerSpec) throws {
try stateMachine.enterReconnecting()
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: helper.XXX(socketManager: SIOManagerSpec, ....)
}
func disconnect(socketManager: SIOManagerSpec) throws {
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: 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 はリリースできません!")
}
func onEvent(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: helper.XXX(socketManager: SIOManagerSpec, ....)
}
func request(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: helper.XXX(socketManager: SIOManagerSpec, ....)
}
func send(socketManager: SIOManagerSpec, event: String) throws {
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: helper.XXX(socketManager: SIOManagerSpec, ....)
}
func connect(socketManager: SIOManagerSpec) throws {
throw SIOConnectionStateMachineError("ReconnectState は接続できません!")
}
func disconnect(socketManager: SIOManagerSpec) throws {
// 許可
// 繰り返しコードを減らすためにヘルパーを使うことができます
// 例: 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 は onConnected できません!")
}
func onDisconnected() throws {
throw SIOConnectionStateMachineError("ReleasedState は onDisconnected できません!")
}
func release(socketManager: SIOManagerSpec) throws {
throw SIOConnectionStateMachineError("ReleasedState はリリースできません!")
}
func request(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
throw SIOConnectionStateMachineError("ReleasedState は request できません!")
}
func onEvent(socketManager: SIOManagerSpec, event: String, callback: @escaping (Data?) -> Void) throws {
throw SIOConnectionStateMachineError("ReleasedState は receiveOn できません!")
}
func send(socketManager: SIOManagerSpec, event: String) throws {
throw SIOConnectionStateMachineError("ReleasedState は send できません!")
}
func connect(socketManager: SIOManagerSpec) throws {
throw SIOConnectionStateMachineError("ReleasedState は connect できません!")
}
func disconnect(socketManager: SIOManagerSpec) throws {
throw SIOConnectionStateMachineError("ReleasedState は disconnect できません!")
}
}
do {
let stateMachine = SIOConnectionStateMachine()
// socket.io 接続時のモック:
// 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)")
}
// 出力:
// error: SIOConnectionStateMachineError(message: "ConnectedState はリリースできません!")
要求シナリオ 3.
何?
場面1と2を組み合わせて、ConnectionPoolというフライウェイトプールとState Patternによる状態管理を導入しました。その後、背景目標にあるように、Feature側は背後のConnectionの接続機構を意識する必要がありません。そこで、ConnectionKeeperと名付けたポーリング機能を作成し、ConnectionPool内で強く保持されているConnectionを定期的にスキャンし、以下の状況が発生した場合に操作を行います:
-
Connectionを使用中で状態がConnectedでない場合:状態をReconnectingに変更し、再接続を試みる -
Connectionが使用されておらず、状態がConnectedの場合:状態をDisconnectedに変更する -
Connectionが未使用で状態がDisconnectedの場合:状態をReleasedに変更し、ConnectionPoolから削除する
なぜ?
-
三つの操作は上下関係があり、排他(disconnected -> released または reconnecting)
-
状況操作の柔軟な差し替え・追加が可能
-
未封装の場合、3つの判定と操作を直接メソッド内に書くしかなく(ロジックのテストが難しい)
-
例:
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
どうやって?
- Chain Of Resposibility :振る舞いパターンの一つで、その名の通りチェーン状になっており、各ノードが対応する処理を持ちます。入力データを受け取ったノードは処理を行うか、次のノードに渡すかを決定します。もう一つの例はiOS Responder Chainです。
定義によると、Chain of Responsibilityパターンでは、あるノードが処理を引き受けた場合、処理を完了してから次のノードに渡す必要があります。途中で処理を終えずに次に渡すことは許されません。
もし上記のシナリオに適しているのは Interceptor Pattern だと思います。

- Chain of responsibility:
ConnectionKeeperHandlerはチェーンのノードの抽象であり、特にcanExcuteメソッドを抽出して、次のノードが処理を行った後にさらに後続のノードを呼び出して実行する状況を避けています。handleはチェーンのノードを連結し、excuteは処理すべきロジックを表します。
ConnectionKeeperHandlerContextは使用するデータを格納するもので、isOccupieは Connection が使用中かどうかを示します。
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 実装
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.
何?
私たちがラップした Connection は、使用する前に setup を経る必要があります。例えば、URL Path の指定や Config の設定などです。
なぜ?
-
構築のステップを柔軟に増減可能
-
再利用可能な構築ロジック
-
封装していない場合、外部が期待通りにクラスを操作しない可能性があります
-
例:
❌
let connection = Connection()
connection.send(event) // 予期しないメソッド呼び出し、先に.connect()を呼ぶべき
✅
let connection = Connection()
connection.connect()
connection.send(event)
// しかし…誰が知っているのか???
どうやって?
- Builder Pattern :生成に関するパターンで、段階的にオブジェクトを構築し、構築手順を再利用できます。

- Builderパターン:
SIOConnectionBuilderはConnectionのビルダーであり、Connectionを構築する際に使用するデータの設定と保存を担当します。ConnectionConfiguration抽象インターフェースは、Connectionを使用する前に必ず.connect()を呼び出してConnectionの実体を取得することを保証します。
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 実装
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 {
// 設定をセット
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 は必須パラメータ
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 封装で直面した4つのシナリオと、それらの問題解決に用いた7つのデザインパターンです。
最後に今回の Socket.IO パッケージ化の完全な設計図を添付します

本文中の命名や例示とは少し異なりますが、この図が実際の設計アーキテクチャです。機会があれば、元の設計者に設計理念やオープンソースについて共有してもらいたいです。
誰が?
これらの設計を行い、Socket.IOのラッピングプロジェクトを担当したのは誰ですか?
Sean Zheng 、PinkoiのAndroidエンジニア

主なアーキテクチャ設計者、デザインパターンの評価と適用、Android側でKotlinを使用して設計を実装。
ZhgChgLi , エンジニアリード / iOSエンジニア @ Pinkoi

Platform Team プロジェクトリーダー、ペアプログラミング、iOS側で Swift を使った設計実装、議論と疑問提起(いわゆる口だけ参加)、そして最後にこの記事を執筆して皆さんと共有。
関連記事
Post Mediumから変換 by ZMediumToMarkdown.



コメント