Lock in $30 Savings on PRO—Offer Ends Soon! ⏳
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Feature Flagを使った開発で高速かつストレスフリーなデリバリーを実現する / Fas...
Search
Tomohiro Imaizumi
September 12, 2022
Technology
3
4.4k
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
Share
More Decks by Tomohiro Imaizumi
See All by Tomohiro Imaizumi
VisionFrameworkで実現する - プライバシーに配慮した「顔ぼかし」機能 / Face blurring with Vision Framework
imaizume
0
270
プロダクトグロースと技術のベースアップを両立させるRettyのアプリ開発スタイル / Achieve Product Growth and Tech Update
imaizume
1
850
git branchを自由に操れるようになろう / Let's Play with Git branch!
imaizume
2
1.3k
スナップショットテスト実戦投入 / Practical Snapshot Testing
imaizume
14
7.6k
コーディング以外のエンジニアリング / About Engineering Without Coding
imaizume
1
1.8k
Firebase Remote Configの運用で知ったこと・知っておくと良いこと / Things I Learned from Operation of Firebase Remote Config
imaizume
6
4.2k
iOSアプリのテストを書きたいのに書けないあなたへ / How You Should Start to Write Your First Unit Test for iOS
imaizume
6
5.2k
循環的複雑度を上げないためのSwiftプログラミングTips / Tips of Swift Programming to Reduce Code Complexity
imaizume
11
8.5k
シングルトンではじめる状態管理と依存注入 / A way to control state using singleton pattern
imaizume
0
5k
Other Decks in Technology
See All in Technology
AWS re:Invent 2025~初参加の成果と学び~
kubomasataka
0
180
ExpoのインダストリーブースでみたAWSが見せる製造業の未来
hamadakoji
0
190
re:Invent2025 3つの Frontier Agents を紹介 / introducing-3-frontier-agents
tomoki10
0
370
事業の財務責任に向き合うリクルートデータプラットフォームのFinOps
recruitengineers
PRO
2
180
[2025-12-12]あの日僕が見た胡蝶の夢 〜人の夢は終わらねェ AIによるパフォーマンスチューニングのすゝめ〜
tosite
0
130
Snowflake だけで実現する “自立的データ品質管理” ~Data Quality Monitoring 解説 ~@ BUILD Meetup: TOKYO 2025
ryo_suzuki
0
120
松尾研LLM講座2025 応用編Day3「軽量化」 講義資料
aratako
0
310
Kiro を用いたペアプロのススメ
taikis
4
1.6k
20251219 OpenIDファウンデーション・ジャパン紹介 / OpenID Foundation Japan Intro
oidfj
0
410
M&Aで拡大し続けるGENDAのデータ活用を促すためのDatabricks権限管理 / AEON TECH HUB #22
genda
0
210
1人1サービス開発しているチームでのClaudeCodeの使い方
noayaoshiro
2
560
「もしもデータ基盤開発で『強くてニューゲーム』ができたなら今の僕はどんなデータ基盤を作っただろう」
aeonpeople
0
200
Featured
See All Featured
職位にかかわらず全員がリーダーシップを発揮するチーム作り / Building a team where everyone can demonstrate leadership regardless of position
madoxten
47
33k
Technical Leadership for Architectural Decision Making
baasie
0
180
SEO Brein meetup: CTRL+C is not how to scale international SEO
lindahogenes
0
2.2k
We Analyzed 250 Million AI Search Results: Here's What I Found
joshbly
0
240
Improving Core Web Vitals using Speculation Rules API
sergeychernyshev
21
1.3k
How to build a perfect <img>
jonoalderson
0
4.6k
Redefining SEO in the New Era of Traffic Generation
szymonslowik
1
160
Visualizing Your Data: Incorporating Mongo into Loggly Infrastructure
mongodb
48
9.8k
How to Talk to Developers About Accessibility
jct
1
83
Being A Developer After 40
akosma
91
590k
Measuring Dark Social's Impact On Conversion and Attribution
stephenakadiri
0
91
Public Speaking Without Barfing On Your Shoes - THAT 2023
reverentgeek
1
280
Transcript
5PNPIJSP*NBJ[VNJ!JNBJ[VNF 3FUUZגࣜձࣾ 'FBUVSF'MBHΛͬͨ։ൃͰ ߴ͔ͭετϨεϑϦʔͳ σϦόϦʔΛ࣮ݱ͢Δ J04%$+BQBO
5PNPIJSP*NBJ[VNJ w *%!JNBJ[VNF w ॴଐ3FUUZ ג ΞϓϦνʔϜ w ڵຯϚΠΫϩϋοΫ4JSJγϣʔτΧοτ w
৯ຊͷڷྉཧ͝άϧϝ͖ ൃදऀ
νʔϜ։ൃͰͷϒϥϯνӡ༻ 'FBUVSF'MBHͱ 'FBUVSF'MBHͷར༻ํ๏ 'FBUVSF'MBHͷར ࣗલ࣮74֎෦αʔϏε
J04Ͱͷ࣮ύλʔϯ 'FBUVSF'MBHͷ5*14 'FBUVSF'MBHͷҙͱ՝ ࣍
νʔϜ։ൃͰͷϒϥϯνӡ༻
νʔϜ։ൃͷಛ w ৽ػೳ։ൃ w 6*มߋ w όάमਖ਼ w ϦϑΝΫλϦϯά w
ϥΠϒϥϦߋ৽ w FUD ෳϒϥϯνͰฒߦ։ൃ
ϝΠϯϒϥϯν ϦϦʔε͢Δίʔυ Λյͯ͠ͳΒͳ͍
Γ͕ͪͳϒϥϯνӡ༻
ઐ༻ͷ։ൃϒϥϯν ௨শϒϥϯν ΛΔ
ϒϥϯνࣜͷϒϥϯνӡ༻ ։ൃελʔτ࣌ main
ϒϥϯνࣜͷϒϥϯνӡ༻ มߋूͷͨΊͷϒϥϯνΛ࡞ parent
ϒϥϯνࣜͷϒϥϯνӡ༻ ݸผػೳΛ։ൃ͢ΔͨΊͷϒϥϯν ࢠϒϥϯν Λ࡞ parent featureA featureB
ϒϥϯνʹΑΓ Өڹൣғͷʹޭ ͔͠͠
ϒϥϯνࣜͷϒϥϯνӡ༻ ͋Δఔ։ൃ͕ਐΜͩঢ়ଶ parent featureA featureB
ϒϥϯνࣜͷϒϥϯνӡ༻ ϝΠϯ্ͷมߋΛϒϥϯνऔΓࠐΉඞཁ͕͋Δ
ϒϥϯνࣜͷϒϥϯνӡ༻ ͞Βʹࢠϒϥϯνൖڝ߹ղফͤ͞Δඞཁ͕͋Δ
ϒϥϯνࣜͷϒϥϯνӡ༻ શࠩͷϚʔδڝ߹ղফͰॳΊͯϦϦʔεՄೳ
ϒϥϯνࣜͷ՝ ϒϥϯνण໋ʹൺྫ͠ڝ߹ղফίετ͕૿͑Δ w णϒϥϯν΄ͲϝΠϯͱͷࠩڝ߹ൃੜ͕֬૿Ճ w ಛʹ9.-ϑΝΠϧ Storyboard, Xib, xcodeproj ղফ͕͍͠ͷͰආ͚͍ͨ
w සൟͳϩʔΧϧϚʔδͰղফՄೳɺ͕ͩ࡞ۀίετߴΕ͕ͪ
ϒϥϯνࣜͷ՝ ΞδϟΠϧ։ൃͱ૬ੑ͕ѱ͍ w ಈ͘ίʔυΛҡ࣋ͭͭ͠ɺ༏ઌɾείʔϓมߋͷରԠ͕ඞཁ w ͔͠͠ w ڝ߹͕ൃੜ͢Δ⾣ͦͷ··Ͱʮਖ਼͘͠ಈ͔ͳ͍ʯ w ։ൃఀࢭɾ࠶։ͷίετ͕େ͖͍⾣։ൃ༏ઌมߋ͕ͮ͠Β͍
w ϒϥϯνͷׂϚʔδ͍͠⾣։ൃείʔϓมߋ͕ͮ͠Β͍ w ڥมԽ͕ܹ͍͠։ൃͰରԠ͕͍͠ 包括的なドキュメントよりも動くソフトウェアを、 ...(中略)... 計画に従うことよりも変化への対応を価値とする (アジャイルソフトウェア開発宣言より)
ϒϥϯνࣜͷ՝ NFSHFSFWFSUͷӨڹൣғ͕େ͖͍ɾݟ௨ͮ͠Β͍ w ڊେ1VMM3FRVFTUӨڹൣғの把握が難しい • 子ブランチに対するレビューが限界 w revert͢Δ߹ʹ͕ࠩڊେʹ w λΠϛϯάʹΑͬͯrevertࣗମ͕ෆՄೳ
ϒϥϯνࣜͷ՝ ण໋ʹൺྫ͠ڝ߹ղফίετ͕૿͑Δ ΞδϟΠϧ։ൃͱ૬ੑ͕ѱ͍ NFSHFSFWFSUͷӨڹൣғ͕େ͖͍ɾݟ௨ͮ͠Β͍ ετϨεϑϧͳै࡞ۀ ͳσϦόϦʔ
͜ΕΒͷΛղܾ͢Δ ϒϥϯνӡ༻
τϥϯΫϕʔε ։ൃ
τϥϯΫϕʔε։ൃͱ w ৗʹϝΠϯϒϥϯν͔Β։࢝Ϛʔδ͢Δ։ൃख๏ w ϝΠϯϒϥϯνʹมߋ͕ू͞Εɺϒϥϯνؒͷ͕ࠩੜ͡ʹ͍͘ w (JU)VC'MPXʹ΄΅͍ۙ トランク ベース開発とは、
開発者が細かく頻繁なアップデートをコア「トランク」 または main ブランチにマージするバージョン管理手法です。 "UMBTTJBOެࣜαΠτʮܧଓతσϦόϦʔʯΑΓ
3FUUZͰ ϒϥϯν͔ΒτϥϯΫϕʔε w ͔ͭͯେ͖ͳࠩΛҰʹϦϦʔε͢Δɺ ௨শʮϏοάόϯϦϦʔεʯ͕ओྲྀͩͬͨ w ͔͠͠ϨϏϡʔσϦόϦʔ͕Լ w ϦϦʔεޙʹ͕ଟ͘ൃੜ w
ʮখ͘͞։ൃɾখ͘͞ϦϦʔεʯ͢ΔͨΊɺ શࣾͰτϥϯΫϕʔε։ൃΛਪਐ
ͨͩ͠ ͦͷ··ϒϥϯνΛϚʔδ͢Δͱ ϦϦʔε༻ίʔυʹӨڹΛ༩͑ͯ͠·͏
τϥϯΫϕʔε։ൃͱ ηοτͰඞཁͳςΫχοΫ
'FBUVSF'MBH
"UMBTTJBOެࣜαΠτʮ'FBUVSF'MBHTʯΑΓ 'FBUVSFGMBHT BMTPDPNNPOMZLOPXOBTGFBUVSFUPHHMFT JT BTPGUXBSFFOHJOFFSJOHUFDIOJRVF UIBUUVSOTTFMFDUGVODUJPOBMJUZPOBOEPGGEVSJOHSVOUJNF XJUIPVUEFQMPZJOHOFXDPEF 'FBUVSF'MBHͱ
'FBUVSF'MBH Ұൠʹ'FBUVSF5PHHMFͱ ɺ ৽͍͠ίʔυΛσϓϩΠ͢Δ͜ͱͳ͘ɺ ࣮ߦ࣌ʹબͨ͠ػೳΛΦϯ·ͨΦϑʹ͢Δ ιϑτΣΞΤϯδχΞϦϯάख๏Ͱ͋Δɻ
'FBUVSF'MBHͷ֓ཁ ϑϥάͱͳΔมΛఆٛ ରͷػೳදग़Λ͙Α͏ʹذΛ࡞ ؔ࿈ࠩϝΠϯϒϥϯνϚʔδ ஈ֊Ͱసͤ͞ϦϦʔε
͕ͳ͚ΕϑϥάͱذΛআ // 1. リリースまではfalseにしておく let isNewFeatureAvailable = false // 2. 分岐で機能表出を防ぐ if isNewFeatureAvailable { // 3. 本番では呼ばれない 本番では showNewFeature() } // 4. 反転してリリース let isNewFeatureAvailable = true if isNewFeatureAvailable { showNewFeature() } // 5. フラグと分岐を削除 showNewFeature()
ࢀߟ'FBUVSF'MBHͷྨ NBSUJOGPXMFSDPNΑΓ w 3FMFBTF5PHHMF։ൃதͷػೳΛϝΠϯϒϥϯνͰར༻Մೳʹɺ͍ͭͰ ຊ൪σϓϩΠՄೳʹ͢ΔͨΊͷϑϥάɻ w &YQFSJNFOU5PHHMF"#ςετͰৼΓ͚ΔͨΊͷϑϥάɻ w 0QT5PHHMFγεςϜͷಈ࡞Λ੍ޚ͢ΔͨΊͷϑϥάɻஈ֊తϦϦʔε w
1FSNJTTJPO5PHHMFಛఆϢʔβʔʹઌߦͯ͠ར༻Մೳʹ͢ΔͨΊͷϑϥάɻ Ћ൛ͷػೳϓϨϛΞϜձһ͚ػೳ ຊൃදͰ3FMFBTF5PHHMFʹߜͬͯղઆ &YQFSJNFOUBM5PHHMFʹ͍ͭͯJ04%$+BQBO 5BLFTIJ*IBSB͞Μͷʮ'FBUVSF'MBHΛదʹྨ͢Δ͜ͱͰ"#ςετͷӡ༻ίετΛԼ͛ΔʯͰղઆ͋Γ
'FBUVSF'MBH τϥϯΫϕʔε։ൃ ͷར
'FBUVSF'MBHͷར ϒϥϯνण໋͕͘ظ։ൃͰڝ߹͠ʹ͍͘ w Ϛʔδઌͱͷࠩৗʹখ͍͞ w ैසɾڝ߹ൃੜ͕֬গͳ͍ w ςΫχοΫ࣍ୈͰ9.-ϑΝΠϧYDPEFQSPKͷڝ߹ճආՄೳ 'FBUVSF'MBH0''
'FBUVSF'MBH0/
'FBUVSF'MBHͷར ΞδϟΠϧ։ൃͱ૬ੑ͕ྑ͍ w ڝ߹͕ൃੜ͠ʹ͍͘ਖ਼͘͠ಈ͘ίʔυ͕ҡ࣋͞ΕΔ w ։ൃ్தͰϑϥά0''ͷ··ϦϦʔεՄೳ w ༏ઌมߋͷӨڹΛड͚ʹ͍͘ w Ԙ௮͚ظ͕ؒͯ͘ैɾڝ߹ղফίετ্͕͕Βͳ͍
w ϚʔδͱϦϦʔε͕͔ΕΔϦϦʔεൣғɾλΠϛϯάΛॊೈʹมߋՄೳ ࠩ͠ࠐΈͰผͷ։ൃϦϦʔε ϝΠϯϒϥϯν͔Β࠶։ ։ൃ్தͰϚʔδ
'FBUVSF'MBHͷར NFSHFSFWFSU͕͕ࠩ͘͢͠খ͍͞ w ϑϥάͷ࠶సͰSFWFSUՄɾࠩখ͍͞ w ࢀরՕॴΛݟΕӨڹൣғΛѲ͍͢͠ w ෆ҆ͳΒҰఆظؒϑϥάΛ͢ͷՄ //
再反転すればrevert完了 let isNewFeatureAvailable = false if isNewFeatureAvailable { showNewFeature() }
'FBUVSF'MBHͷར ϒϥϯνण໋͕͘ظ։ൃͰڝ߹͠ʹ͍͘ ΞδϟΠϧ։ൃͱ૬ੑ͕ྑ͍ NFSHFSFWFSU͕͕ࠩ͘͢͠খ͍͞ ैڝ߹͕ͳ͘ετϨεϑϦʔ ߴͳσϦόϦʔΛ࣮ݱ
ϒϥϯν74τϥϯΫϕʔε 'FBUVSF'MBH ϒϥϯν ൺֱ߲ τϥϯΫϕʔε 'FBUVSF'MBH ଟ͍ ϩʔΧϧϚʔδڝ߹ൃੜ গͳ͍
ߴ͍ தஅɾ࠶։ίετ ͍ 13ͱಉ͡ߦ SFWFSU࣌ͷࠩ dߦ ͍͠ɾίετ͕ߴ͍ είʔϓɾλΠϛϯάมߋ ༰қ
J04Ͱͷ࣮ύλʔϯ
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 ) }
HStack(spacing: 0) { if enabledNewFeature { NewView(newViewModel: .init()) } else
{ OldView(oldViewModel: .init()) }.padding(.bottom, 16) w දࣔՕॴʹذΛ࣮Մೳ w ذָ͕Ͱཧతͳύλʔϯ 4XJGU6*
// 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
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
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ͷΈཱͯΛ ذͤͤ͞Δ σʔλܕ͕มΘΔ߹
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) }
ࣗલ࣮74֎෦αʔϏε
/* 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
# 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になる
֎෦αʔϏεͷൺֱද جຊతػೳੑʹେࠩͳ͘'JSFCBTF͕͓खࠒ ൺֱ߲ 'JSFCBTF 3FNPUF$PO fi H -BVODI%BSLMZ TQMJUJP
'MBHTIJQ ಛ ଞͷ'JSFCBTF αʔϏεͱͷ࿈ܞ ϦΞϧλΠϜੑ NTҎͰ৴ ܭଌࢹͷ ػೳ͖ ࣗಈϩʔϧόοΫ ஈ֊తϦϦʔε ྉۚ ແྉ NPOUId ໊·Ͱແྉ NPOUId GFUDIճ੍ݶ ࣌ؒҎճ·Ͱ ެࣜͰ໌ݴͤͣ ͳ͠ ͳ͠
w σϑΥϧτͰλʔήοτΞϓ Ϧɾ04ɾࠃͳͲΛࢦఆՄೳ w ҙϢʔβʔϓϩύςΟʹՃ͑ɺ ϥϯμϜͳׂΓৼΓϕʔε ͷذՄ w औಘ੍ݶͱΩϟογϡͷͨΊɺ සൟͳมߋʹҙ
'JSFCBTF3FNPUF$POGJHͷ݅ࢦఆ
w ύϥϝʔλ໊ɾσʔλܕɾσϑ ΥϧτΛࢦఆ w ذ݅Λࢦఆ͢Δ w "QQ%FMFHBUFͰύϥϝʔλΛ GFUDIͯ͠ར༻͢Δ 3FNPUF$POGJHͰͷϑϥάఆٛ
import Firebase let remoteConfig = RemoteConfig.remoteConfig() let isNewFeatureEnabled = remoteConfig["newFeatureEnabled"].boolValue 'FBUVSF'MBHTTXJGU
͍͚ ϦϦʔε SFWFSU λΠϛϯάͱذ݅࣍ୈ ࣗલ࣮ w ΞϓϦϦϦʔεػೳϦϦʔε w ذ͕݅੩త
֎෦αʔϏε αʔόʔ੍ޚ w ΞϓϦϦϦʔεͱػೳϦϦʔεͷ λΠϛϯάΛ͚͍ͨ w ذ݅Λಈతɾৄࡉʹม͍͑ͨ
ࣗલ࣮74֎෦αʔϏε ϦϦʔεɾλʔήςΟϯάͰ੍͕͋ΔͳΒ֎෦αʔϏεΛݕ౼ ൺֱ߲ ࣗલ࣮ ΫϥΠΞϯτͷΈ ֎෦αʔϏε ϑϥάసλΠϛϯά Ϗϧυ࣌ ҙλΠϛϯά
ϑϥάཧมߋॴ ΫϥΠΞϯτͷίʔυ αʔόʔ(6* λʔήςΟϯάɾ࣌ݶࣜ ͍͠ Մೳ ґଘ ͳ͠ 4%,͕ඞཁ Ձ֨ ແྉ ༗ྉͷ߹͋Γ
'FBUVSF'MBHͷ5*14
'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 より
։ൃ్தͰͷಋೖɾมߋ͋Γ ϦϦʔεͷείʔϓɾλΠϛϯά͕มΘͬͨΒ w ॳͷείʔϓͷҰ෦ΛઌߦతʹϦϦʔε͍ͨ͠ w 'FBUVSF'MBHΛׂ͢Δ͜ͱͰείʔϓׂ͕Մೳ w ʮҙࢥܾఆϩδοΫ͔ΒҙࢥܾఆϙΠϯτΛΓ͢ʯ NBSUJOGPXMFSDPNΑΓ
w ޙ͔Βͷ'FBUVSF'MBHಋೖ͋Γ
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() }
আ࣌ͷϛεࢭ؍͔Β 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 }
'FBUVSF'MBHར༻࣌ͷҙ
'FBUVSF'MBHͷҙ ϑϥάͷཧʹҙ w ޡͬͨస͕ى͜Βͳ͍Α͏ʹҙ͕ඞཁ w ಛJ04৹ࠪʹΑΓSFWFSUʹ͕͔͔࣌ؒΔ w 2"6*ςετͰͷ֬ೝΛ w /JHIUMZ#VJMEͰࠒ͔ΒࣾνΣοΫ͢Δͷ༗ޮ
'FBUVSF'MBHͷҙ ෆཁϑϥάͷআૣΊʹ w ޡͬͨίʔυΛࢀর͢ΔϦεΫ͕͋ΔͷͰૣΊʹফ͢ w ʮϑϥάআ·Ͱ͕ϦϦʔεʯ w ࠓޙআͷࣗಈԽΛظFH(JU)VCͷऔΓΈ
'FBUVSF'MBHͷҙ 1VMM3FRVFTUૣࠞͥ͘Δ w τϥϯΫϕʔεͰࡉ͔͘1VMM3FRVFTUΛग़͢ w ͨͩ͠ϨϏϡʔͪͷ13͕૿͑ΔͷΞϯνύλʔϯ w ͪͷ1VMM3FRVFTUΛݮΒ͠ίʔυΛ࠷৽ʹ w ϖΞɾϞϒϓϩ͢Δ
w ϨϏϡʔ༻ͷ࣌ؒΛઃఆ
'FBUVSF'MBHͷҙ ར༻͕͍͠έʔε w ෳࡶͳ4UPSZCPBSE9JCϨΠΞτ w ϒϥϯνͰਐΊΔ͔࡞Γ͠Λݕ౼͢Δ w ৽چͷܕͷ͕ࠩେ͖͍ w ৽چܕͷϑΟʔϧυΛ༻ҙͯ͠ذࠔͳ߹
w நԽϨΠϠʔΛઃ͚Δํ๏͋Γ w มߋՕॴ͕ΫϥΠΞϯτଆʹͳ͍ w "1*όʔδϣϯΛม͑ΔPSαʔόʔʹϑϥάΛಋೖ
·ͱΊ
ࠓ͔Β'FBUVSF'MBHΛೖΕͯΈΑ͏ ߴ͔ͭετϨεϑϦʔͳσϦόϦʔ͕࣮ݱ w ฒߦظ։ൃͰैڝ߹͕গͳ͘ετϨεϑϦʔ w ༏ઌɾείʔϓมߋʹڧͯ͘ΞδϟΠϧ w ·ͣϩʔΧϧͷϑϥά͔Β࢝ΊΑ͏ w ޡͬͨసɾআʹҙɾؾ͚ΔΛ
w ৗʹখ͘͞ɾߴʹϚʔδ͍ͯ͜͠͏
'FBUVSF'MBHΛͬͨ։ൃͰ ߴ͔ͭετϨεϑϦʔͳσϦόϦʔΛ
ࢀߟจݙ63- wΞδϟΠϧιϑτΣΞ։ൃએݴ 🔗IUUQTBHJMFNBOJGFTUPPSHJTPKBNBOJGFTUPIUNM wτϥϯΫϕʔε։ൃc"UMBTTJBO 🔗IUUQTXXXBUMBTTJBODPNKBDPOUJOVPVTEFMJWFSZDPOUJOVPVTJOUFHSBUJPOUSVOLCBTFEEFWFMPQNFOU w'FBUVSF5PHHMFT BLB'FBUVSF'MBHT 🔗IUUQTNBSUJOGPXMFSDPNBSUJDMFTGFBUVSFUPHHMFTIUNM w'FBUVSF'MBHΛదʹྨ͢Δ͜ͱͰ"#ςετͷӡ༻ίετΛԼ͛ΔJ04%$5BLFTIJ*IBSB
"#&." 🔗IUUQTTQFBLFSEFDLDPNOPODIBMBOUCUFTVUPGBMTFZVOZPOHLPTVUPXPYJBHFSV w'FBUVSF'MBHT 5PHHMFT $POUSPMTr5IF)VCGPS'FBUVSF'MBH%SJWFO%FWFMPQNFOUc'FBUVSF'MBHT 🔗IUUQTGFBUVSF fl BHTJP