Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

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

Avatar for Tomohiro Imaizumi

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 }