Slide 1

Slide 1 text

シングルトンではじめる 状態管理と依存注⼊ ROPPONGI.swift #7 at Diverse.inc @imaizume

Slide 2

Slide 2 text

• 今泉 智博 (@imaizume) • 株式会社Diverse / 株式会社Uzumaki • マッチングアプリPoiboy iOS版開発担当 • Diverse Podcast にも出てます • 近況: 最近レーシックを受けました ⾃⼰紹介

Slide 3

Slide 3 text

1. 状態管理が⾟い話 2. ブーストの状態管理をシングルトンにした話 3. 性別を依存注⼊する話 4. まとめ 本⽇のINDEX

Slide 4

Slide 4 text

• プロフィール • ユーザーランク • 課⾦情報 • アイテム使⽤状態 • キャンペーン参加状態 • Betaユーザー etc アプリ開発で⾟いこと アプリ内の状態管理 状態更新 表⽰更新 状態参照 状態参照 状態更新 状態の例

Slide 5

Slide 5 text

Matching Dev Meetup #1 でも… Reduxを取り⼊れてpairs開発はどう変わったか (@satoshin21) > とにかく複雑な状態管理 「タップル誕⽣」における開発の変化 / change_development (@corin8823) > 状態管理の多さによる 複雑さを減少させたい 男⼥出し分けと会員状態で画⾯切り分けが⼤変な話 (@kikuchy) > クライアント開発は状態整理との戦い

Slide 6

Slide 6 text

Matching Dev Meetup #1 でも… Reduxを取り⼊れてpairs開発はどう変わったか (@satoshin21) > とにかく複雑な状態管理 「タップル誕⽣」における開発の変化 / change_development (@corin8823) > 状態管理の多さによる 複雑さを減少させたい 男⼥出し分けと会員状態で画⾯切り分けが⼤変な話 (@kikuchy) > クライアント開発は状態整理との戦い どのマッチングアプリも 状態管理に苦労していた

Slide 7

Slide 7 text

最近のトレンドRedux(Flux)❓ Fundamentals of Swift & Redux (ReduxとSwiftの組み合わせ) (@fumiyasac) Reduxの原則 1. Single source of truth (ソースは1つだけ) 2. State is read-only (状態は読み取り専⽤) 3. Mutations are written as pure functions (変更はすべて純粋関数で書かれる)

Slide 8

Slide 8 text

良さそうではあるが… すぐの導⼊は難しそう (開発リソース⾯) ViewControllerが複雑 (複数責務/状態分散) ↓ViewControllerの中

Slide 9

Slide 9 text

良さそうではあるが… すぐの導⼊は難しそう (開発リソース/学習コスト) ViewControllerが複雑 (複数責務/状態分散) ↓ViewControllerの中 Redux原則1: “Single source of truth” 状態集約だけでも実現したい…

Slide 10

Slide 10 text

シングルトンパターンの利⽤ そんな⽅にオススメの⽅法

Slide 11

Slide 11 text

シングルトンパターンとは • アプリ内に存在するインスタンスが1つ • どのモジュールからも参照可能 • Swiftではstatic letで宣⾔するだけ • アプリ内の状態を⼀箇所に集約・管理 今回はPoiboyでの実装を参考に シングルトンによる状態管理の例を紹介 class PurchaseManager() { /// γϯάϧτϯΠϯελϯε static let shared: PurchaseManager = .init() } 課⾦状態を知りたい! 画面A 画面B 画面C シングルトンオブジェクト 唯⼀の状態変数

Slide 12

Slide 12 text

例1: ブースト状態の同期

Slide 13

Slide 13 text

Poiboyのブースト機能について • ブーストは男性が使えるアイテム • 使⽤すると⼀定時間⼥性側への露出が増加 • 使⽤中はボタンに残り時間を表⽰ • ボタンタップでポップアップを表⽰

Slide 14

Slide 14 text

ブースト関連の画⾯構成 • ポップアップはブースト前/中/後の3種類 • (余談)以前は1つのViewControllerで内部分岐してた ブースト前 ブースト中 ブースト後 メイン画面 今回話すところ 今回話すところ

Slide 15

Slide 15 text

QAからのバグ報告 • カウント⽤Timerを画⾯ごとに作成 (=状態が分散) • ポップアップは若⼲初期化から表⽰が遅れる • その結果残り時間の表⽰に差が⽣じていた 12:34 メイン画⾯ ポップアップ 12:35 通信/初期化 present (初期値=12:34)

Slide 16

Slide 16 text

対応: ブースト状態を1箇所に集約 • BoostTimeManager(シングルトンクラス)を定義 • 残り時間のラベル更新にはオブザーバー同期を使⽤ 12:34 メイン画⾯ ポップアップ 12:35 present (初期値=12:34) メイン画⾯ ポップアップ ブースト時間管理用クラス 12:34 present 12:34 更新 更新 参照 BEFORE AFTER 残り時間B 残り時間A ←唯一の残り時間

Slide 17

Slide 17 text

コード(プロパティ) class BoostTimeManager { /// γϯάϧτϯΠϯελϯε static let shared: BoostTimeManager = .init() /// ϒʔετதͰ͋Ε͹trueΛฦ͢ var isInBoostTime: Bool { return self.remainingSeconds > 0 } /// ϒʔετͷ࢒Γ࣌ؒ(ඵ) private(set) var remainingSeconds: Int = 0 /// Χ΢ϯτμ΢ϯͷͨΊͷλΠϚʔΠϯελϯε private var timer: Timer? ⼀部のプロパティのみ公開し不意の変更・更新を極⼒防ぐ

Slide 18

Slide 18 text

コード (Timerでのカウントダウン) /// ϒʔετͷΧ΢ϯτμ΢ϯΛ։࢝ /// طʹ࣮ߦதͷ৔߹͸ݱࡏͷॲཧΛఀࢭ͠৽نʹΧ΢ϯτμ΢ϯΛ։࢝ func startCountDown(duration: Int) { self.remainingSeconds = duration self.timer?.invalidate() guard self.isInBoostTime else { self.timer = nil return } let timer = Timer.scheduledTimer( timeInterval: 1.0, target: self, selector: #selector(self.countDown), userInfo: nil, repeats: true ) timer.fire() self.timer = timer }

Slide 19

Slide 19 text

コード (カウントダウン時の通知) /// ϒʔετͷΧ΢ϯτμ΢ϯΛ࣮ߦ͠Observer΁௨஌͢Δ /// ϒʔετ͕ऴ͍ྃͯ͠Δ৔߹͸λΠϚʔΛఀࢭ͠ऴྃ͢Δ @objc private func countDown() { guard self.isInBoostTime else { self.timer?.invalidate() self.timer = nil return } self.remainingSeconds -= 1 let center = NotificationCenter.default center.post( name: .boostTimeNotification, object: self.remainingSeconds ) } } 通知時にobjectに追加情報を付加可能

Slide 20

Slide 20 text

コード (Notification.Name) extension Notification.Name { /// ϒʔετͷ࢒Γ࣌ؒ؂ࢹ public static let boostTimeNotification = Notification.Name("boostTime") } ʮSwift 3 Ҏ߱ͷ NotificationCenter ͷਖ਼͍͠࢖͍ํʯ (@mono0926) ΑΓ

Slide 21

Slide 21 text

コード (ポップアップ画⾯1) class BoostLiveViewController: UIViewController { override func viewDidLoad() { // ϒʔετ࢒Γ࣌ؒͷߋ৽ͱ௨஌ let center = NotificationCenter.default let selector = #selector( self.updateBoostTime(notification:) ) center.addObserver( self, selector: selector, name: .boostTimeNotification, object: nil ) } deinit { let center = NotificationCenter.default center.removeObserver( self, name: .boostTimeNotification, object: nil ) } iOS9.0以降は明⽰的な登録解除が不要に

Slide 22

Slide 22 text

コード (ポップアップ画⾯2) /// ࢒Γ࣌ؒͷߋ৽௨஌Λड͚औͬͨΒϒʔετؔ࿈ͷViewΛߋ৽ @objc fileprivate func updateBoostTime( notification: Notification ) { if BoostTimeManager.sharedInstance.isInBoostTime { /// ϒʔετதͳΒςΩετΛߋ৽
 self.timerLabel.text = DateTimeUtil.secondToMmss( BoostTimeManager.sharedInstance.remainingSeconds ) } else { /// ϒʔετ͕ऴྃͨ͠ΒϙοϓΞοϓΛด͡Δ self.dismiss( animated: true, completion: self.dismissCallback ) } } } MVPであればPresenterから更新してあげる

Slide 23

Slide 23 text

結果 BEFORE AFTER

Slide 24

Slide 24 text

ここまでのまとめ • シングルトンクラスを通じて状態を参照/更新 • 画⾯ごとの状態(ModelやTimer)を集約可能 • リアルタイムなViewの反映にはオブザーバー同期を活⽤

Slide 25

Slide 25 text

Ϣχοτςετ͸ૄ݁߹͕લఏͱͳ͍ͬͯͯɺ·ͨʮຊ෺ͷʯ࣮૷ίʔυͷ୅Θ ΓʹɺదٓϞοΫ ςετ༻ͷِऀ ࣮૷ʹࠩ͠ସ͑ΒΕΔ͜ͱ΋ॏཁ͔ͩΒͰ͢ɻ γϯάϧτϯύλʔϯ͸ͦͷ๦͛ʹͳΔͷͰ͢ɻ γϯάϧτϯύλʔϯ͸ӬଓԽ͞Εͨঢ়ଶΛ҉໧ͷ͏ͪʹ൐͏ɻ ʜதུʜ ࣮ߦલʹ͸ඞͣɺϓϩάϥϜΛط஌ͷঢ়ଶʹઃఆͰ͖Δ͸ͣͳͷͰ͢ɻෆมͰͳ ͍ঢ়ଶΛ൐͏γϯάϧτϯΛಋೖͯ͠͠·͏ͱɺͦΕ͕೉͘͠ͳΓ·͢ɻ Ճ͑ͯɺάϩʔόϧʹΞΫηεՄೳͰɺ͔ͭӬଓԽ͞Εͨঢ়ଶ͕͋Δͱɺίʔυ ͔Βঢ়ଶͷਪ࿦͕ࠔ೉ʹͳΓ·͢ɻ シングルトンの⽋点: Unitテストのしづらさ • どこからでも参照可能→暗黙的⼊⼒に • 全体で状態を更新する→暗黙的な状態永続化 [プログラマが知るべき97のこと No.70: 「シングルトンパターンの誘惑に負けない」より

Slide 26

Slide 26 text

スタブを利⽤し明⽰的な⼊⼒に変換 • 暗黙的⼊⼒をスタブ化 • 必要な状態を依存注⼊可能にする • 他のテストへの副作⽤を発⽣させない ʮ୯ମςετͷϋδϝʯ (@yokoyas000) ΑΓ

Slide 27

Slide 27 text

例2: 性別情報の取得

Slide 28

Slide 28 text

性別情報を差し替え可能にする • Poiboyではユーザー情報をシングルトンから取得 • そこで性別情報を差し替え可能にする設計に変更 • 通常時はシングルトンから状態を取得 • テストやデバッグではスタブから取得 <> • isMale: Bool • isFemale: Bool • genderCode: Int? [GenderStore] (KeychainManagerの値を返す) [GenderStoreStub] (指定した値をそのまま返す) 本実装 Unitテスト デバッグ用実装 スタブ 本物 protocol

Slide 29

Slide 29 text

コード(Protocol定義) /** ੑผ৘ใΛࢀর͢ΔͨΊͷΦϒδΣΫτ΁࣮૷͞ΕΔ΂͖ΠϯλʔϑΣΠε */ protocol GenderStoreContract { /// genderCode = 0ͰtrueΛฦ͢ var isMale: Bool { get } /// genderCode = 1ͰtrueΛฦ͢ var isFemale: Bool { get } /// ݱঢ়nilͷ৔߹͕ߟྀ͞ΕͨઃܭͳͷͰ͜͜Ͱ΋ఆٛ var genderCode: Int? { get } } ”Protocol Oriented Programming”

Slide 30

Slide 30 text

コード(実装向けクラス) /** ࣮ࡍʹΞϓϦʹอଘ͞Ε͍ͯΔੑผΛࢀরͯ͠ฦͨ͢ΊͷΫϥε */ class GenderStore: GenderStoreContract { var isFemale: Bool { return KeychainManager.shared.isFemale } var isMale: Bool { return KeychainManager.shared.isMale } var genderCode: Int? { return KeychainManager.shared.getGenderCode() } } シングルトンインスタンスを参照

Slide 31

Slide 31 text

コード(スタブクラス) /** ೚ҙͷੑผ/GenderCodeʹࠩ͠ସ͑ΒΕͨੑผΛฦ͢ελϒͷΫϥε */ class GenderStoreStub: GenderStoreContract { let isFemale: Bool let isMale: Bool let genderCode: Int? init(isFemale: Bool, isNilGenderCode: Bool) { self.isFemale = isFemale self.isMale = !isFemale if isNilGenderCode { self.genderCode = nil } else { self.genderCode = isFemale ? GenderType.female.rawValue : GenderType.male.rawValue } } } 差し替えた値を参照

Slide 32

Slide 32 text

コード(GETパラメータでの性別差し替え) /// ϦΫΤετURLʹ෇༩͢ΔGETύϥϝʔλΛந৅Խͨ͠ྻڍܕ /// ύϥϝʔλΛੜ੒͢ΔͨΊʹඞཁͳ৘ใΛassociated valueʹ౉͢ enum GetParameterType { case gender(GenderStoreContract) var keyValueString: String? { switch self { case .gender(let store): guard let code = store.genderCode else { return nil } return "gender_code=\(code)" } } } /// ࣮ࡍʹબ୒͍ͯ͠ΔAPITargetΛฦ͢ class APITargetStore: APITargetStoreContract { let target: APITarget = .init(KeychainManager.shared.getApiTarget()) } APITargetも依存注⼊可能な設計に

Slide 33

Slide 33 text

コード(URLパラメータで性別差し替え) enum WebViewLink: String { /// ϔϧϓ: உঁͰίϯςϯπ͕ҟͳΔ͜ͱʹ஫ҙ case help = “/api/web/help“ func toURLString( params: params: [GetParameterType] = [], api: api: APITargetStoreContract = APITargetStore() ) -> String { let baseUrl: String = api.target.baseWebUrlString + self.rawValue let parameterStrings: [String] = params.flatMap { $0.keyValueString } let joinedString: String
 = parameterStrings.joined(separator: "&") return “\(baseUrl)“ + “\(parameterStrings.isEmpty ? "" : "?\(joinedString)")" } } デフォルトでは実際の値を返すオブジェクトを使⽤

Slide 34

Slide 34 text

コード(テストでの性別差し替えの例) /// URL͕ਖ਼͘͠ੜ੒͞ΕΔ͜ͱΛอূ͢Δςετ XCTAssertEqual( WebViewLink.help.toURLString( params: [.gender(GenderStoreStub( isFemale: true, isNilGenderCode: false))], api: APITargetStoreStub(target: .dev01) ), “https://dev01.hoge.com/api/web/help?gender_code=2” ) } XCTAssertEqual( WebViewLink.help.toURLString( gender: .gender(GenderStoreStub( isFemale: false, isNilGenderCode: true)), api: APITargetStoreStub(target: .dev02) ), “https://dev02.hoge.com/api/web/help” ) } テストではスタブにより差し替えた値を使⽤ テスト以外にデバッグ時の強制表⽰などでも使えます

Slide 35

Slide 35 text

シングルトンのメリットデメリット • 複数シングルトン間の連携処理 (Reduxにするタイミング?) • 排他制御(Lock)は⾃前実装が必要 (Reduxはどうなってる?) • 初期化コストの⾼い処理を⼀⻫に⾏うと動作が遅くなる? • 状態が1箇所に集約される • 責務をある程度適切に切り出すことが可能 • ラッパークラスにより依存注⼊も可能 メリット )デメリット

Slide 36

Slide 36 text

まとめ • シングルトンパターンで簡易的に状態管理を実現 • ViewControllerから責務や状態を分離する第⼀歩に • リアルタイム更新にはオブザーバー同期を活⽤ • テストやデバッグのための依存注⼊を実現するために • Protocol Orientedな実装にする • 差し替えのためのスタブを定義する • 正しい責務分離を⼼がけ設計の改善につなげましょう❗

Slide 37

Slide 37 text

参考リンク • Reduxを取り⼊れてpairs開発はどう変わったか (@satoshin21) https://speakerdeck.com/satoshin21/reduxwoqu-riru-retekai-fa-hapairskai-fa-hadoubian-watutaka • 「タップル誕⽣」における開発の変化 / change_development (@corin8823) https://speakerdeck.com/corin8823/change-development • 男⼥出し分けと会員状態で画⾯切り分けが⼤変な話 (@kikuchy) https://docs.google.com/presentation/d/1jglMmosRj4n4T0QEJ18iR6kgxLQjQWDCHv2NcF1QQR8 • リアルタイム情報同期 (@yuyakaido) https://speakerdeck.com/yuyakaido/matching-dev-meetup-1 • Fundamentals of Swift & Redux (ReduxとSwiftの組み合わせ) (@fumiyasak) https://www.slideshare.net/fumiyasakai37/fundamentals-of-swift-redux-reduxswift • Swift 3 以降の NotificationCenter の正しい使い⽅ (@mono0926) https://qiita.com/mono0926/items/754c5d2dbe431542c75e • シングルトンパターンの誘惑に負けない | プログラマが知るべき97のこと https://プログラマが知るべき97のこと.com/エッセイ/シングルトンパターンの誘惑に負けない/ • 単体テストのハジメ (@yokoyas000) https://speakerdeck.com/yokoyas000/dan-ti-tesutofalsehazime