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
Tomohiro Imaizumi
September 12, 2022
Technology
3
4.3k
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
250
プロダクトグロースと技術のベースアップを両立させるRettyのアプリ開発スタイル / Achieve Product Growth and Tech Update
imaizume
1
840
git branchを自由に操れるようになろう / Let's Play with Git branch!
imaizume
2
1.2k
スナップショットテスト実戦投入 / Practical Snapshot Testing
imaizume
14
7.5k
コーディング以外のエンジニアリング / 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.1k
循環的複雑度を上げないためのSwiftプログラミングTips / Tips of Swift Programming to Reduce Code Complexity
imaizume
11
8.4k
シングルトンではじめる状態管理と依存注入 / A way to control state using singleton pattern
imaizume
0
5k
Other Decks in Technology
See All in Technology
ある編集者のこれまでとこれから —— 開発者コミュニティと歩んだ四半世紀
inao
1
300
仕様駆動 x Codex で 超効率開発
ismk
2
1.4k
内部品質・フロー効率・コミュニケーションコストを悪化させ現場を苦しめかねない16の組織設計アンチパターン[超簡易版] / 16 Organization Design Anti-Patterns for Software Development
mtx2s
2
200
ユーザーストーリー x AI / User Stories x AI
oomatomo
0
170
What's the recommended Flutter architecture
aakira
1
970
マイクロリブート ~ACEマインドセットで実現するアジャイル~
sony
0
240
【Android】テキスト選択色の問題修正で心がけたこと
tonionagauzzi
0
130
Design and implementation of "Markdown to Google Slides" / phpconfuk 2025
k1low
1
390
"おまじない"はもう卒業! デバッガで探るSpring Bootの裏側と「学び方」の学び方
takeuchi_132917
0
110
ソフトウェア開発現代史: 55%が変化に備えていない現実 ─ AI支援型開発時代のReboot Japan #agilejapan
takabow
1
1.8k
設計は最強のプロンプト - AI時代に武器にすべきスキルとは?-
kenichirokimura
1
350
AI時代に必要なデータプラットフォームの要件とは by @Kazaneya_PR / 20251107
kazaneya
PRO
4
960
Featured
See All Featured
Building Adaptive Systems
keathley
44
2.8k
Automating Front-end Workflow
addyosmani
1371
200k
How To Stay Up To Date on Web Technology
chriscoyier
791
250k
Become a Pro
speakerdeck
PRO
29
5.6k
The Cult of Friendly URLs
andyhume
79
6.7k
The Invisible Side of Design
smashingmag
302
51k
[RailsConf 2023] Rails as a piece of cake
palkan
57
6k
GraphQLの誤解/rethinking-graphql
sonatard
73
11k
Connecting the Dots Between Site Speed, User Experience & Your Business [WebExpo 2025]
tammyeverts
10
660
VelocityConf: Rendering Performance Case Studies
addyosmani
333
24k
We Have a Design System, Now What?
morganepeng
54
7.9k
Building an army of robots
kneath
306
46k
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