シングルトンではじめる状態管理と依存注入 / A way to control state using singleton pattern

シングルトンではじめる状態管理と依存注入 / A way to control state using singleton pattern

ROPPONGI.swift 第7回 (try! Swift Tokyo 2019 プレイベント) での発表資料です。
https://roppongi-swift.connpass.com/event/122146/

1a74617b91d2757b839b9cf3614648ce?s=128

Tomohiro Imaizumi

March 07, 2019
Tweet

Transcript

  1. 2.

    • 今泉 智博 (@imaizume) • 株式会社Diverse / 株式会社Uzumaki • マッチングアプリPoiboy

    iOS版開発担当 • Diverse Podcast にも出てます • 近況: 最近レーシックを受けました ⾃⼰紹介
  2. 4.

    • プロフィール • ユーザーランク • 課⾦情報 • アイテム使⽤状態 • キャンペーン参加状態

    • Betaユーザー etc アプリ開発で⾟いこと アプリ内の状態管理 状態更新 表⽰更新 状態参照 状態参照 状態更新 状態の例
  3. 5.

    Matching Dev Meetup #1 でも… Reduxを取り⼊れてpairs開発はどう変わったか (@satoshin21) > とにかく複雑な状態管理 「タップル誕⽣」における開発の変化

    / change_development (@corin8823) > 状態管理の多さによる 複雑さを減少させたい 男⼥出し分けと会員状態で画⾯切り分けが⼤変な話 (@kikuchy) > クライアント開発は状態整理との戦い
  4. 6.

    Matching Dev Meetup #1 でも… Reduxを取り⼊れてpairs開発はどう変わったか (@satoshin21) > とにかく複雑な状態管理 「タップル誕⽣」における開発の変化

    / change_development (@corin8823) > 状態管理の多さによる 複雑さを減少させたい 男⼥出し分けと会員状態で画⾯切り分けが⼤変な話 (@kikuchy) > クライアント開発は状態整理との戦い どのマッチングアプリも 状態管理に苦労していた
  5. 7.

    最近のトレンド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 (変更はすべて純粋関数で書かれる)
  6. 11.

    シングルトンパターンとは • アプリ内に存在するインスタンスが1つ • どのモジュールからも参照可能 • Swiftではstatic letで宣⾔するだけ • アプリ内の状態を⼀箇所に集約・管理

    今回はPoiboyでの実装を参考に シングルトンによる状態管理の例を紹介 class PurchaseManager() { /// γϯάϧτϯΠϯελϯε static let shared: PurchaseManager = .init() } 課⾦状態を知りたい! 画面A 画面B 画面C シングルトンオブジェクト 唯⼀の状態変数
  7. 16.

    対応: ブースト状態を1箇所に集約 • BoostTimeManager(シングルトンクラス)を定義 • 残り時間のラベル更新にはオブザーバー同期を使⽤ 12:34 メイン画⾯ ポップアップ 12:35

    present (初期値=12:34) メイン画⾯ ポップアップ ブースト時間管理用クラス 12:34 present 12:34 更新 更新 参照 BEFORE AFTER 残り時間B 残り時間A ←唯一の残り時間
  8. 17.

    コード(プロパティ) 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? ⼀部のプロパティのみ公開し不意の変更・更新を極⼒防ぐ
  9. 18.

    コード (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 }
  10. 19.

    コード (カウントダウン時の通知) /// ϒʔετͷΧ΢ϯτμ΢ϯΛ࣮ߦ͠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に追加情報を付加可能
  11. 20.

    コード (Notification.Name) extension Notification.Name { /// ϒʔετͷ࢒Γ࣌ؒ؂ࢹ public static let

    boostTimeNotification = Notification.Name("boostTime") } ʮSwift 3 Ҏ߱ͷ NotificationCenter ͷਖ਼͍͠࢖͍ํʯ (@mono0926) ΑΓ
  12. 21.

    コード (ポップアップ画⾯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以降は明⽰的な登録解除が不要に
  13. 22.

    コード (ポップアップ画⾯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から更新してあげる
  14. 28.

    性別情報を差し替え可能にする • Poiboyではユーザー情報をシングルトンから取得 • そこで性別情報を差し替え可能にする設計に変更 • 通常時はシングルトンから状態を取得 • テストやデバッグではスタブから取得 <<GenderStoreContract>>

    • isMale: Bool • isFemale: Bool • genderCode: Int? [GenderStore] (KeychainManagerの値を返す) [GenderStoreStub] (指定した値をそのまま返す) 本実装 Unitテスト デバッグ用実装 スタブ 本物 protocol
  15. 29.

    コード(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”
  16. 30.

    コード(実装向けクラス) /** ࣮ࡍʹΞϓϦʹอଘ͞Ε͍ͯΔੑผΛࢀরͯ͠ฦͨ͢ΊͷΫϥε */ class GenderStore: GenderStoreContract { var isFemale:

    Bool { return KeychainManager.shared.isFemale } var isMale: Bool { return KeychainManager.shared.isMale } var genderCode: Int? { return KeychainManager.shared.getGenderCode() } } シングルトンインスタンスを参照
  17. 31.

    コード(スタブクラス) /** ೚ҙͷੑผ/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 } } } 差し替えた値を参照
  18. 32.

    コード(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も依存注⼊可能な設計に
  19. 33.

    コード(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)")" } } デフォルトでは実際の値を返すオブジェクトを使⽤
  20. 34.

    コード(テストでの性別差し替えの例) /// 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” ) } テストではスタブにより差し替えた値を使⽤ テスト以外にデバッグ時の強制表⽰などでも使えます
  21. 37.

    参考リンク • 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