Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Feature Flagを使った開発で高速かつストレスフリーなデリバリーを実現する / Fas...
Search
Sponsored
·
Your Podcast. Everywhere. Effortlessly.
Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.
→
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
290
プロダクトグロースと技術のベースアップを両立させるRettyのアプリ開発スタイル / Achieve Product Growth and Tech Update
imaizume
1
860
git branchを自由に操れるようになろう / Let's Play with Git branch!
imaizume
2
1.3k
スナップショットテスト実戦投入 / Practical Snapshot Testing
imaizume
14
7.7k
コーディング以外のエンジニアリング / 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
5.1k
Other Decks in Technology
See All in Technology
AIエージェントを開発しよう!-AgentCore活用の勘所-
yukiogawa
0
180
外部キー制約の知っておいて欲しいこと - RDBMSを正しく使うために必要なこと / FOREIGN KEY Night
soudai
PRO
12
5.6k
Why Organizations Fail: ノーベル経済学賞「国家はなぜ衰退するのか」から考えるアジャイル組織論
kawaguti
PRO
1
180
Context Engineeringの取り組み
nutslove
0
380
Bedrock PolicyでAmazon Bedrock Guardrails利用を強制してみた
yuu551
0
260
マーケットプレイス版Oracle WebCenter Content For OCI
oracle4engineer
PRO
5
1.6k
生成AIを活用した音声文字起こしシステムの2つの構築パターンについて
miu_crescent
PRO
3
220
SREじゃなかった僕らがenablingを通じて「SRE実践者」になるまでのリアル / SRE Kaigi 2026
aeonpeople
6
2.5k
データの整合性を保ちたいだけなんだ
shoheimitani
8
3.2k
Cosmos World Foundation Model Platform for Physical AI
takmin
0
960
Tebiki Engineering Team Deck
tebiki
0
24k
Oracle Cloud Observability and Management Platform - OCI 運用監視サービス概要 -
oracle4engineer
PRO
2
14k
Featured
See All Featured
Max Prin - Stacking Signals: How International SEO Comes Together (And Falls Apart)
techseoconnect
PRO
0
87
Stop Working from a Prison Cell
hatefulcrawdad
273
21k
Mobile First: as difficult as doing things right
swwweet
225
10k
職位にかかわらず全員がリーダーシップを発揮するチーム作り / Building a team where everyone can demonstrate leadership regardless of position
madoxten
58
50k
The Art of Programming - Codeland 2020
erikaheidi
57
14k
WCS-LA-2024
lcolladotor
0
450
The Myth of the Modular Monolith - Day 2 Keynote - Rails World 2024
eileencodes
26
3.3k
Ecommerce SEO: The Keys for Success Now & Beyond - #SERPConf2024
aleyda
1
1.8k
Visual Storytelling: How to be a Superhuman Communicator
reverentgeek
2
430
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
PRO
196
71k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
659
61k
Become a Pro
speakerdeck
PRO
31
5.8k
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