Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Feature Flagを使った開発で高速かつストレスフリーなデリバリーを実現する / Fas...

Feature Flagを使った開発で高速かつストレスフリーなデリバリーを実現する / Fast and stress-free delivery with Feature Flag-based development

iOSDC Japan 2022 Day2 Track Aにて発表した「Feature Flagを使った開発で高速でストレスフリーなデリバリーを実現する」の発表資料です。

Tomohiro Imaizumi

September 12, 2022
Tweet

More Decks by Tomohiro Imaizumi

Other Decks in Technology

Transcript

  1. 

  2. 

  3.  νʔϜ։ൃͰͷϒϥϯνӡ༻  'FBUVSF'MBHͱ͸  'FBUVSF'MBHͷར༻ํ๏  'FBUVSF'MBHͷར఺  ࣗલ࣮૷74֎෦αʔϏε

     J04Ͱͷ࣮૷ύλʔϯ  'FBUVSF'MBHͷ5*14  'FBUVSF'MBHͷ஫ҙ఺ͱ՝୊ ໨࣍ 
  4. ਌ϒϥϯνࣜͷ՝୊ ΞδϟΠϧ։ൃͱ૬ੑ͕ѱ͍ w ಈ͘ίʔυΛҡ࣋ͭͭ͠ɺ༏ઌ౓ɾείʔϓมߋ΁ͷରԠ͕ඞཁ w ͔͠͠ w ڝ߹͕ൃੜ͢Δ⾣ͦͷ··Ͱ͸ʮਖ਼͘͠ಈ͔ͳ͍ʯ w ։ൃఀࢭɾ࠶։ͷίετ͕େ͖͍⾣։ൃ༏ઌ౓มߋ͕ͮ͠Β͍

    w ਌ϒϥϯνͷ෼ׂϚʔδ͸೉͍͠⾣։ൃείʔϓมߋ͕ͮ͠Β͍ w ؀ڥมԽ͕ܹ͍͠։ൃͰ͸ରԠ͕೉͍͠  包括的なドキュメントよりも動くソフトウェアを、 ...(中略)... 計画に従うことよりも変化への対応を価値とする (アジャイルソフトウェア開発宣言より)
  5. τϥϯΫϕʔε։ൃͱ͸ w ৗʹϝΠϯϒϥϯν͔Β։࢝Ϛʔδ͢Δ։ൃख๏ w ϝΠϯϒϥϯνʹมߋ͕ू໿͞Εɺϒϥϯνؒͷࠩ෼͕ੜ͡ʹ͍͘ w (JU)VC'MPXʹ΄΅͍ۙ  トランク ベース開発とは、

    開発者が細かく頻繁なアップデートをコア「トランク」 または main ブランチにマージするバージョン管理手法です。 "UMBTTJBOެࣜαΠτʮܧଓతσϦόϦʔʯΑΓ
  6. "UMBTTJBOެࣜαΠτʮ'FBUVSF'MBHTʯΑΓ 'FBUVSFGMBHT BMTPDPNNPOMZLOPXOBTGFBUVSFUPHHMFT JT BTPGUXBSFFOHJOFFSJOHUFDIOJRVF UIBUUVSOTTFMFDUGVODUJPOBMJUZPOBOEPGGEVSJOHSVOUJNF  XJUIPVUEFQMPZJOHOFXDPEF  'FBUVSF'MBHͱ͸

    'FBUVSF'MBH Ұൠʹ'FBUVSF5PHHMFͱ΋ ͸ɺ ৽͍͠ίʔυΛσϓϩΠ͢Δ͜ͱͳ͘ɺ ࣮ߦ࣌ʹબ୒ͨ͠ػೳΛΦϯ·ͨ͸Φϑʹ͢Δ ιϑτ΢ΣΞΤϯδχΞϦϯάख๏Ͱ͋Δɻ
  7. 'FBUVSF'MBHͷ֓ཁ  ϑϥάͱͳΔม਺Λఆٛ  ର৅ͷػೳදग़Λ๷͙Α͏ʹ෼ذΛ࡞੒  ؔ࿈ࠩ෼͸ϝΠϯϒϥϯν΁Ϛʔδ  ׬੒ஈ֊Ͱ൓సͤ͞ϦϦʔε 

    ໰୊͕ͳ͚Ε͹ϑϥάͱ෼ذΛ࡟আ  // 1. リリースまではfalseにしておく let isNewFeatureAvailable = false // 2. 分岐で機能表出を防ぐ if isNewFeatureAvailable { // 3. 本番では呼ばれない 本番では showNewFeature() } // 4. 反転してリリース let isNewFeatureAvailable = true if isNewFeatureAvailable { showNewFeature() } // 5. フラグと分岐を削除 showNewFeature()
  8. ࢀߟ'FBUVSF'MBHͷ෼ྨ NBSUJOGPXMFSDPNΑΓ w 3FMFBTF5PHHMF։ൃதͷػೳΛϝΠϯϒϥϯνͰར༻Մೳʹɺ͍ͭͰ΋ ຊ൪σϓϩΠՄೳʹ͢ΔͨΊͷϑϥάɻ w &YQFSJNFOU5PHHMF"#ςετͰৼΓ෼͚ΔͨΊͷϑϥάɻ w 0QT5PHHMFγεςϜͷಈ࡞Λ੍ޚ͢ΔͨΊͷϑϥάɻஈ֊తϦϦʔε౳ w

    1FSNJTTJPO5PHHMFಛఆϢʔβʔʹઌߦͯ͠ར༻Մೳʹ͢ΔͨΊͷϑϥάɻ Ћ൛ͷػೳ΍ϓϨϛΞϜձһ޲͚ػೳ౳  ຊൃදͰ͸3FMFBTF5PHHMFʹߜͬͯղઆ &YQFSJNFOUBM5PHHMFʹ͍ͭͯ͸J04%$+BQBO಺ 5BLFTIJ*IBSB͞Μͷʮ'FBUVSF'MBHΛద੾ʹ෼ྨ͢Δ͜ͱͰ"#ςετͷӡ༻ίετΛԼ͛ΔʯͰ΋ղઆ͋Γ
  9. ਌ϒϥϯν74τϥϯΫϕʔε 'FBUVSF'MBH  ਌ϒϥϯν ൺֱ߲໨ τϥϯΫϕʔε 'FBUVSF'MBH ଟ͍ ϩʔΧϧϚʔδڝ߹ൃੜ গͳ͍

    ߴ͍ தஅɾ࠶։ίετ ௿͍ ਌13ͱಉ͡ߦ਺ SFWFSU࣌ͷࠩ෼ d਺ߦ ೉͍͠ɾίετ͕ߴ͍ είʔϓɾλΠϛϯάมߋ ༰қ
  10. if enabledNewFeature { present(viewController: NewViewController()) } else { present(viewController: OldViewController())

    } w QSFTFOUɾQVTIՕॴͰ෼ذ w ෼ذָ͕Ͱཧ૝తͳύλʔϯ 7JFX$POUSPMMFS  if enabledNewFeature { navigationController?.pushViewController( NewViewController(), animated: true ) } else { navigationController?.pushViewController( OldViewController(), animated: true ) }
  11. HStack(spacing: 0) { if enabledNewFeature { NewView(newViewModel: .init()) } else

    { OldView(oldViewModel: .init()) }.padding(.bottom, 16) w දࣔՕॴʹ௚઀෼ذΛ࣮૷Մೳ w ෼ذָ͕Ͱཧ૝తͳύλʔϯ 4XJGU6* 
  12. // XibやStoryboardを使ったレイアウト @IBOutlet private var oldViewHeight: NSLayoutConstraint! @IBOutlet private var

    newViewHeight: NSLayoutConstraint! if enabledNewFeature { newViewHeight.priority = .defaultHigh oldViewHeight.priority = .defaultLow } else { newViewHeight.priority = .defaultLow oldViewHeight.priority = .defaultHigh } // コードのみでのレイアウト let viewToAdd: UIView = enabledNewFeature ? newView : oldView view.addSubview(viewToAdd) w /4$POTUSBJOU-BZPVUͰͷ෼ذ ʹ'FBUVSF'MBHΛ෇͚Δ w ίʔυͰͷϨΠΞ΢τͳΒ BEE4VC7JFXΛ'FBUVSF'MBHͰ ෼ذͯ͠΋0, w ϨΠΞ΢τ৚݅ʹԠͨ͡બ୒Λ "VUP-BZPVU 
  13. func tableView( _ tableView: UITableView, cellForRowAt indexPath: IndexPath ) ->

    UITableViewCell { if familiarCategoryFeatureEnabled { return tableView.dequeueReusableCell( withIdentifier: "NewCell", for: indexPath ) as! NewCell } else { return tableView.dequeueReusableCell( withIdentifier: "OldCell", for: indexPath ) as? OldCell } w EFRVFVF3FVTBCMF$FMMͷ JEFOUJ fi FSΛม͑Ε͹ྑ͍ w ྆ํͷDFMMΛEFRVFVFͭͭ͠ IFJHIUΛʹઃఆ͢Δͷ΋͋Γ 6*5BCMF7JFX$FMM 
  14. func setupViews() { if isNewFeatureEnabled { let response = fetchNewResponse()

    titleView.text = response.title newView.value = response.newField newView.isHidden = false oldView.isHidden = true } else { let response = fetchOldResponse() titleView.text = response.title oldView.isHidden = response.oldField newView.isHidden = true oldView.isHidden = false } } func fetchNewResponse() -> NewResponse { return NewResponse(title: ..., newField: ...) } func fetchOldResponse() -> OldResponse { .init(title: ..., oldField: ...) } 6*,JU w ৽چͷܕʹରԠ͢ΔϑΟʔϧυ Λఆٛ w ӨڹΛड͚Δ7JFXΛ෼཭ w දࣔঢ়ଶɾ7JFXͷ૊ΈཱͯΛ෼ ذͤͤ͞Δ σʔλܕ͕มΘΔ৔߹ 
  15. class ViewModel: ObservableObject { @Published var newItem: NewItem @Published var

    oldItem: OldItem } 4XJGU6* w ৽چͷܕʹରԠ͢ΔϑΟʔϧυ Λఆٛ w ӨڹΛड͚Δ7JFXΛ෼཭ w දࣔঢ়ଶɾ7JFXͷ૊ΈཱͯΛ෼ ذͤͤ͞Δ σʔλܕ͕มΘΔ৔߹  if isNewFeatureEnabled { Text(viewModel.newItem.value) } else { Text(viewModel.oldItem.value) }
  16. /* OTHER_LDFLAGSにコンパイルフラグを渡すことで ビルド時にフラグの値を指定可能 */ #if FEATURE_FLAG_NEW_FEATURE let isNewFeatureAvailable = true

    #else let isNewFeatureAvailable = false #endif ϑϥάఆ਺ͷఆٛͱࢀর w άϩʔόϧͳఆ਺Λ༻ҙ w Ϗϧυ࣌ͷม਺Ͱ֎෦੾Γସ͑ Ͱ͖ΔΑ͏ʹ͓ͯ͘͠ w ͋ͱ͸දग़ՕॴͰࢀর͢Δ͚ͩ ࣗલ࣮૷Ͱͷ޻෉  'FBUVSF'MBHTTXJGU // 使用箇所での分岐に使用するだけ if isNewFeatureAvailable { present(newViewController, animated: true) } else { present(viewController, animated: true) } 7JFX$POUSPMMFSTXJGU
  17. # FeatureFlags = HONYA,MORAKE と入ってくるので # それぞれプレフィックスに FEATURE_FLAG_ を付与して渡す。 activate_feature_flags

    = (options[:activate_feature_flags] || '').split(",") xcodebuild_args = activate_feature_flags .map { |k| "-D FEATURE_FLAG_#{k.shellescape}" }.join(' ') build_app( workspace: "MyApp.xcworkspace", scheme: "MyApp", export_options: { xcargs: xcodebuild_args.length > 0 ? "OTHER_SWIFT_FLAGS='$(inherited) #{xcodebuild_args}'" : "" ) 'BTUMBOFͷઃఆ w Ҿ਺Ͱ'FBUVSF'MBH༻ͷίϯύ ΠϧϑϥάΛड͚औΔ w ଞͷҾ਺ͱ۠ผͰ͖ΔΑ͏ʹɺ ϓϨϑΟοΫεΛ෇͚Δ౳ͷ޻෉ ࣗલ࣮૷Ͱͷ޻෉  'BTU fi MF $ fastlane build_my_app activate_feature_flags:HONYA,MORAKE > FEATURE_FLAG_HONYAとFEATURE_FLAG_MORAKEがtrueになる
  18. ֎෦αʔϏεͷൺֱද جຊతػೳੑʹ͸େࠩͳ͘'JSFCBTF͕͓खࠒ  ൺֱ߲໨ 'JSFCBTF 3FNPUF$PO fi H -BVODI%BSLMZ TQMJUJP

    'MBHTIJQ ಛ௃ ଞͷ'JSFCBTF αʔϏεͱͷ࿈ܞ ϦΞϧλΠϜੑ NTҎ಺Ͱ഑৴ ܭଌ΍؂ࢹͷ ػೳ෇͖ ࣗಈϩʔϧόοΫ ஈ֊తϦϦʔε ྉۚ ແྉ NPOUId ໊·Ͱແྉ NPOUId GFUDIճ਺੍ݶ ࣌ؒҎ಺ճ·Ͱ ެࣜͰ໌ݴͤͣ ͳ͠ ͳ͠
  19. w ύϥϝʔλ໊ɾσʔλܕɾσϑ Υϧτ஋Λࢦఆ w ෼ذ৚݅Λࢦఆ͢Δ w "QQ%FMFHBUFͰύϥϝʔλΛ GFUDIͯ͠ར༻͢Δ 3FNPUF$POGJHͰͷϑϥάఆٛ 

    import Firebase let remoteConfig = RemoteConfig.remoteConfig() let isNewFeatureEnabled = remoteConfig["newFeatureEnabled"].boolValue 'FBUVSF'MBHTTXJGU
  20. ࢖͍෼͚ ϦϦʔε SFWFSU λΠϛϯάͱ෼ذ৚݅࣍ୈ ࣗલ࣮૷ w ΞϓϦϦϦʔεػೳϦϦʔε w ෼ذ৚͕݅੩త 

    ֎෦αʔϏε αʔόʔ੍ޚ  w ΞϓϦϦϦʔεͱػೳϦϦʔεͷ λΠϛϯάΛ෼͚͍ͨ w ෼ذ৚݅Λಈతɾৄࡉʹม͍͑ͨ
  21. ࣗલ࣮૷74֎෦αʔϏε ϦϦʔεɾλʔήςΟϯάͰ੍໿͕͋ΔͳΒ֎෦αʔϏεΛݕ౼  ൺֱ߲໨ ࣗલ࣮૷ ΫϥΠΞϯτͷΈ ֎෦αʔϏε ϑϥά൓సλΠϛϯά Ϗϧυ࣌ ೚ҙλΠϛϯά

    ϑϥά؅ཧมߋ৔ॴ ΫϥΠΞϯτͷίʔυ αʔόʔ(6* λʔήςΟϯάɾ࣌ݶࣜ ೉͍͠ Մೳ ґଘ ͳ͠ 4%,͕ඞཁ Ձ֨ ແྉ ༗ྉͷ৔߹͋Γ
  22. 'FBUVSF'MBH͸࠷ऴखஈ ৗʹখ͘͞Ϛʔδ͢Δઃܭɾ౒ྗΛ w ಈ࡞ʹӨڹ͠ͳ͍ࠩ෼͸ૣ͘Ϛʔδ͢Δ΂͖Ͱ'FBUVSF'MBH͸ෆཁ w ྫίϯϙʔωϯτఆٛͷΈͷࠩ෼ w ྫϑϩϯτ͔Β౸ୡෆՄೳͳൣғͰͷ݁߹ w Ϣʔβʔ͕ࢀরՄೳʹͳΔಋઢͰॳΊͯ'FBUVSF'MBHΛݕ౼͢Δ

     > Only if you can't do small releases or UI last should you employ release toggles. (小さなリリースや UI を最後に実行できない場合にのみ、リリース トグルを使用するべきである。) martinfowler.com より
  23. w 'FBUVSF'MBHͷ௚઀ࢀর͸ɺ ʮҙࢥܾఆϩδοΫϙΠϯτʯ w ϦϦʔεͷείʔϓ΍৚͕݅ม͑ ͮΒ͍ w ؒ઀ϨΠϠʔΛڬΉ͜ͱͰґଘ ͕ബ͘ͳΓ྆ऀΛ෼཭Մೳʹ w

    ґଘؔ܎ٯసͰ%*΋Մೳʹ ҙࢥܾఆϙΠϯτͱϩδοΫͷ෼཭  // Feature Flagが意思決定ロジック=意思決定ポイントに let isNewFeatureEnabled: Bool = true if isNewFeatureEnabled { showView() editView() } // 意思決定ロジック(Feature Flagを入力にする) func canShowView(featureFlags: [String: Bool]) -> Bool { featureFlags["newFeature"] == true && conditionForShowView } func canEditView(featureFlags: [String: Bool]) -> Bool { featureFlags["newFeature"] == true && conditionForEditView } // 意思決定ポイント let isNewFeatureEnabled: Bool = true if canShowView(["newFeature": isNewFeatureEnabled]) { showView() } if canEditView(["newFeature": isNewFeatureEnabled]) { editView() }
  24. ࡟আ࣌ͷϛε๷ࢭ؍఺͔Β w ϒϩοΫͰ·ͱ·͍ͬͯΔ৔߹ɺ ফ͠࿙Ε΍ޡ࡟আ๷ࢭ͠΍͍͢ w %3:͕ྑ͍ͱ͸ݶΒͳ͍ ෼ذ͸ۃྗϒϩοΫʹ·ͱΊΔ  // 各行で削除が必要なため誤削除・漏れに注意が必要

    view.addSubview(isNewFeatureEnabled ? newView : oldView) newView.isHidden = isNewFeatureEnabled oldView.isHidden = !isNewFeatureEnabled // 削除対象がブロックなので範囲が明確 if isNewFeatureEnabled { view.addSubview(newView) newView.isHidden = false oldView.isHidden = true } else { view.addSubview(oldView) newView.isHidden = true oldView.isHidden = false }