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

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

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. 5PNPIJSP*NBJ[VNJ!JNBJ[VNF 3FUUZגࣜձࣾ

    'FBUVSF'MBHΛ࢖ͬͨ։ൃͰ
    ߴ଎͔ͭετϨεϑϦʔͳ
    σϦόϦʔΛ࣮ݱ͢Δ
    J04%$+BQBO

    View full-size slide

  2. 5PNPIJSP*NBJ[VNJ
    w *%!JNBJ[VNF
    w ॴଐ3FUUZ ג
    ΞϓϦνʔϜ
    w ڵຯϚΠΫϩϋοΫ4JSJγϣʔτΧοτ
    w ৯೔ຊͷڷ౔ྉཧ͝౰஍άϧϝ޷͖
    ൃදऀ

    View full-size slide

  3. νʔϜ։ൃͰͷϒϥϯνӡ༻
    'FBUVSF'MBHͱ͸
    'FBUVSF'MBHͷར༻ํ๏
    'FBUVSF'MBHͷར఺
    ࣗલ࣮૷74֎෦αʔϏε
    J04Ͱͷ࣮૷ύλʔϯ
    'FBUVSF'MBHͷ5*14
    'FBUVSF'MBHͷ஫ҙ఺ͱ՝୊
    ໨࣍

    View full-size slide

  4. νʔϜ։ൃͰͷϒϥϯνӡ༻

    View full-size slide

  5. νʔϜ։ൃͷಛ௃
    w ৽ػೳ։ൃ
    w 6*มߋ
    w όάमਖ਼
    w ϦϑΝΫλϦϯά
    w ϥΠϒϥϦߋ৽
    w FUD

    ෳ਺ϒϥϯνͰฒߦ։ൃ

    View full-size slide

  6. ϝΠϯϒϥϯν
    ϦϦʔε͢Δίʔυ
    Λյͯ͠͸ͳΒͳ͍

    View full-size slide

  7. ΍Γ͕ͪͳϒϥϯνӡ༻

    View full-size slide

  8. ઐ༻ͷ։ൃϒϥϯν
    ௨শ਌ϒϥϯν

    Λ੾Δ

    View full-size slide

  9. ਌ϒϥϯνࣜͷϒϥϯνӡ༻
    ։ൃελʔτ࣌఺

    main

    View full-size slide

  10. ਌ϒϥϯνࣜͷϒϥϯνӡ༻
    มߋू໿ͷͨΊͷ਌ϒϥϯνΛ࡞੒

    parent

    View full-size slide

  11. ਌ϒϥϯνࣜͷϒϥϯνӡ༻
    ݸผػೳΛ։ൃ͢ΔͨΊͷϒϥϯν ࢠϒϥϯν
    Λ࡞੒

    parent
    featureA
    featureB

    View full-size slide

  12. ਌ϒϥϯνʹΑΓ
    Өڹൣғͷ෼཭ʹ͸੒ޭ
    ͔͠͠

    View full-size slide

  13. ਌ϒϥϯνࣜͷϒϥϯνӡ༻
    ͋Δఔ౓։ൃ͕ਐΜͩঢ়ଶ

    parent
    featureA
    featureB

    View full-size slide

  14. ਌ϒϥϯνࣜͷϒϥϯνӡ༻
    ϝΠϯ্ͷมߋΛ਌ϒϥϯν΁औΓࠐΉඞཁ͕͋Δ

    View full-size slide

  15. ਌ϒϥϯνࣜͷϒϥϯνӡ༻
    ͞Βʹࢠϒϥϯν΁఻ൖڝ߹ղফͤ͞Δඞཁ͕͋Δ

    View full-size slide

  16. ਌ϒϥϯνࣜͷϒϥϯνӡ༻
    શࠩ෼ͷϚʔδڝ߹ղফͰॳΊͯϦϦʔεՄೳ

    View full-size slide

  17. ਌ϒϥϯνࣜͷ՝୊
    ϒϥϯνण໋ʹൺྫ͠ڝ߹ղফίετ͕૿͑Δ
    w ௕णϒϥϯν΄ͲϝΠϯͱͷࠩ෼ڝ߹ൃੜ֬཰͕૿Ճ
    w ಛʹ9.-ϑΝΠϧ Storyboard, Xib, xcodeproj
    ͸ղফ͕೉͍͠ͷͰආ͚͍ͨ
    w සൟͳϩʔΧϧϚʔδͰղফՄೳɺ͕ͩ࡞ۀίετߴ๨Ε͕ͪ

    View full-size slide

  18. ਌ϒϥϯνࣜͷ՝୊
    ΞδϟΠϧ։ൃͱ૬ੑ͕ѱ͍
    w ಈ͘ίʔυΛҡ࣋ͭͭ͠ɺ༏ઌ౓ɾείʔϓมߋ΁ͷରԠ͕ඞཁ
    w ͔͠͠
    w ڝ߹͕ൃੜ͢Δ⾣ͦͷ··Ͱ͸ʮਖ਼͘͠ಈ͔ͳ͍ʯ
    w ։ൃఀࢭɾ࠶։ͷίετ͕େ͖͍⾣։ൃ༏ઌ౓มߋ͕ͮ͠Β͍
    w ਌ϒϥϯνͷ෼ׂϚʔδ͸೉͍͠⾣։ൃείʔϓมߋ͕ͮ͠Β͍
    w ؀ڥมԽ͕ܹ͍͠։ൃͰ͸ରԠ͕೉͍͠

    包括的なドキュメントよりも動くソフトウェアを、


    ...(中略)...


    計画に従うことよりも変化への対応を価値とする


    (アジャイルソフトウェア開発宣言より)

    View full-size slide

  19. ਌ϒϥϯνࣜͷ՝୊
    NFSHFSFWFSUͷӨڹൣғ͕େ͖͍ɾݟ௨ͮ͠Β͍
    w ڊେ1VMM3FRVFTU͸Өڹൣғの把握が難しい


    • 子ブランチに対するレビューが限界


    w revert͢Δ৔߹ʹ΋ࠩ෼͕ڊେʹ
    w λΠϛϯάʹΑͬͯ͸revertࣗମ͕ෆՄೳ

    View full-size slide

  20. ਌ϒϥϯνࣜͷ՝୊
    ण໋ʹൺྫ͠ڝ߹ղফίετ͕૿͑Δ
    ΞδϟΠϧ։ൃͱ૬ੑ͕ѱ͍
    NFSHFSFWFSUͷӨڹൣғ͕େ͖͍ɾݟ௨ͮ͠Β͍

    ετϨεϑϧͳ௥ै࡞ۀ
    ௿଎ͳσϦόϦʔ

    View full-size slide

  21. ͜ΕΒͷ໰୊Λղܾ͢Δ
    ϒϥϯνӡ༻

    View full-size slide

  22. τϥϯΫϕʔε
    ։ൃ

    View full-size slide

  23. τϥϯΫϕʔε։ൃͱ͸
    w ৗʹϝΠϯϒϥϯν͔Β։࢝Ϛʔδ͢Δ։ൃख๏
    w ϝΠϯϒϥϯνʹมߋ͕ू໿͞Εɺϒϥϯνؒͷࠩ෼͕ੜ͡ʹ͍͘
    w (JU)VC'MPXʹ΄΅͍ۙ

    トランク ベース開発とは、


    開発者が細かく頻繁なアップデートをコア「トランク」


    または main ブランチにマージするバージョン管理手法です。
    "UMBTTJBOެࣜαΠτʮܧଓతσϦόϦʔʯΑΓ

    View full-size slide

  24. 3FUUZͰ΋
    ਌ϒϥϯν͔ΒτϥϯΫϕʔε΁
    w ͔ͭͯେ͖ͳࠩ෼ΛҰ౓ʹϦϦʔε͢Δɺ
    ௨শʮϏοάόϯϦϦʔεʯ͕ओྲྀͩͬͨ
    w ͔͠͠ϨϏϡʔ΍σϦόϦʔ଎౓͕௿Լ
    w ϦϦʔεޙʹ΋໰୊͕ଟ͘ൃੜ
    w ʮখ͘͞։ൃɾখ͘͞ϦϦʔεʯ͢ΔͨΊɺ
    શࣾͰτϥϯΫϕʔε։ൃΛਪਐ

    View full-size slide

  25. ͨͩ͠

    ͦͷ··ϒϥϯνΛϚʔδ͢Δͱ
    ϦϦʔε༻ίʔυʹӨڹΛ༩͑ͯ͠·͏

    View full-size slide

  26. τϥϯΫϕʔε։ൃͱ
    ηοτͰඞཁͳςΫχοΫ

    View full-size slide

  27. "UMBTTJBOެࣜαΠτʮ'FBUVSF'MBHTʯΑΓ
    'FBUVSFGMBHT BMTPDPNNPOMZLOPXOBTGFBUVSFUPHHMFT
    JT
    BTPGUXBSFFOHJOFFSJOHUFDIOJRVF
    UIBUUVSOTTFMFDUGVODUJPOBMJUZPOBOEPGGEVSJOHSVOUJNF
    XJUIPVUEFQMPZJOHOFXDPEF

    'FBUVSF'MBHͱ͸
    'FBUVSF'MBH Ұൠʹ'FBUVSF5PHHMFͱ΋
    ͸ɺ
    ৽͍͠ίʔυΛσϓϩΠ͢Δ͜ͱͳ͘ɺ
    ࣮ߦ࣌ʹબ୒ͨ͠ػೳΛΦϯ·ͨ͸Φϑʹ͢Δ
    ιϑτ΢ΣΞΤϯδχΞϦϯάख๏Ͱ͋Δɻ

    View full-size slide

  28. 'FBUVSF'MBHͷ֓ཁ
    ϑϥάͱͳΔม਺Λఆٛ
    ର৅ͷػೳදग़Λ๷͙Α͏ʹ෼ذΛ࡞੒
    ؔ࿈ࠩ෼͸ϝΠϯϒϥϯν΁Ϛʔδ
    ׬੒ஈ֊Ͱ൓సͤ͞ϦϦʔε
    ໰୊͕ͳ͚Ε͹ϑϥάͱ෼ذΛ࡟আ

    // 1. リリースまではfalseにしておく


    let isNewFeatureAvailable = false


    // 2. 分岐で機能表出を防ぐ


    if isNewFeatureAvailable {


    // 3. 本番では呼ばれない 本番では


    showNewFeature()


    }
    // 4. 反転してリリース


    let isNewFeatureAvailable = true


    if isNewFeatureAvailable {


    showNewFeature()


    }
    // 5. フラグと分岐を削除


    showNewFeature()

    View full-size slide

  29. ࢀߟ'FBUVSF'MBHͷ෼ྨ
    NBSUJOGPXMFSDPNΑΓ
    w 3FMFBTF5PHHMF։ൃதͷػೳΛϝΠϯϒϥϯνͰར༻Մೳʹɺ͍ͭͰ΋
    ຊ൪σϓϩΠՄೳʹ͢ΔͨΊͷϑϥάɻ
    w &YQFSJNFOU5PHHMF"#ςετͰৼΓ෼͚ΔͨΊͷϑϥάɻ
    w 0QT5PHHMFγεςϜͷಈ࡞Λ੍ޚ͢ΔͨΊͷϑϥάɻஈ֊తϦϦʔε౳
    w 1FSNJTTJPO5PHHMFಛఆϢʔβʔʹઌߦͯ͠ར༻Մೳʹ͢ΔͨΊͷϑϥάɻ
    Ћ൛ͷػೳ΍ϓϨϛΞϜձһ޲͚ػೳ౳

    ຊൃදͰ͸3FMFBTF5PHHMFʹߜͬͯղઆ
    &YQFSJNFOUBM5PHHMFʹ͍ͭͯ͸J04%$+BQBO಺
    5BLFTIJ*IBSB͞Μͷʮ'FBUVSF'MBHΛద੾ʹ෼ྨ͢Δ͜ͱͰ"#ςετͷӡ༻ίετΛԼ͛ΔʯͰ΋ղઆ͋Γ

    View full-size slide

  30. 'FBUVSF'MBH
    τϥϯΫϕʔε։ൃ
    ͷར఺

    View full-size slide

  31. 'FBUVSF'MBHͷར఺
    ϒϥϯνण໋͕୹͘௕ظ։ൃͰ΋ڝ߹͠ʹ͍͘
    w Ϛʔδઌͱͷࠩ෼͸ৗʹখ͍͞
    w ௥ैස౓ɾڝ߹ൃੜ֬཰͕গͳ͍
    w ςΫχοΫ࣍ୈͰ9.-ϑΝΠϧ΍YDPEFQSPKͷڝ߹΋ճආՄೳ

    'FBUVSF'MBH0'' 'FBUVSF'MBH0/

    View full-size slide

  32. 'FBUVSF'MBHͷར఺
    ΞδϟΠϧ։ൃͱ૬ੑ͕ྑ͍
    w ڝ߹͕ൃੜ͠ʹ͍͘ਖ਼͘͠ಈ͘ίʔυ͕ҡ࣋͞ΕΔ
    w ։ൃ్தͰ΋ϑϥά0''ͷ··ϦϦʔεՄೳ
    w ༏ઌ౓มߋͷӨڹΛड͚ʹ͍͘
    w Ԙ௮͚ظ͕ؒ௕ͯ͘΋௥ैɾڝ߹ղফίετ্͕͕Βͳ͍
    w ϚʔδͱϦϦʔε͕෼͔ΕΔϦϦʔεൣғɾλΠϛϯάΛॊೈʹมߋՄೳ

    ࠩ͠ࠐΈͰผͷ։ൃϦϦʔε ϝΠϯϒϥϯν͔Β࠶։
    ։ൃ్தͰ΋Ϛʔδ

    View full-size slide

  33. 'FBUVSF'MBHͷར఺
    NFSHFSFWFSU͕͠΍ࠩ͘͢෼͕খ͍͞
    w ϑϥάͷ࠶൓సͰSFWFSUՄɾࠩ෼΋খ͍͞
    w ࢀরՕॴΛݟΕ͹ӨڹൣғΛ೺Ѳ͠΍͍͢
    w ෆ҆ͳΒҰఆظؒϑϥάΛ࢒͢ͷ΋Մ

    // 再反転すればrevert完了


    let isNewFeatureAvailable = false


    if isNewFeatureAvailable {


    showNewFeature()


    }

    View full-size slide

  34. 'FBUVSF'MBHͷར఺
    ϒϥϯνण໋͕୹͘௕ظ։ൃͰ΋ڝ߹͠ʹ͍͘
    ΞδϟΠϧ։ൃͱ૬ੑ͕ྑ͍
    NFSHFSFWFSU͕͠΍ࠩ͘͢෼͕খ͍͞

    ௥ैڝ߹͕ͳ͘ετϨεϑϦʔ
    ߴ଎ͳσϦόϦʔΛ࣮ݱ

    View full-size slide

  35. ਌ϒϥϯν74τϥϯΫϕʔε'FBUVSF'MBH

    ਌ϒϥϯν ൺֱ߲໨ τϥϯΫϕʔε'FBUVSF'MBH
    ଟ͍ ϩʔΧϧϚʔδڝ߹ൃੜ গͳ͍
    ߴ͍ தஅɾ࠶։ίετ ௿͍
    ਌13ͱಉ͡ߦ਺ SFWFSU࣌ͷࠩ෼ d਺ߦ
    ೉͍͠ɾίετ͕ߴ͍ είʔϓɾλΠϛϯάมߋ ༰қ

    View full-size slide

  36. J04Ͱͷ࣮૷ύλʔϯ

    View full-size slide

  37. 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


    )


    }

    View full-size slide

  38. HStack(spacing: 0) {


    if enabledNewFeature {


    NewView(newViewModel: .init())


    } else {


    OldView(oldViewModel: .init())


    }.padding(.bottom, 16)


    w දࣔՕॴʹ௚઀෼ذΛ࣮૷Մೳ
    w ෼ذָ͕Ͱཧ૝తͳύλʔϯ
    4XJGU6*

    View full-size slide

  39. // 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

    View full-size slide

  40. 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

    View full-size slide

  41. 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ͷ૊ΈཱͯΛ෼
    ذͤͤ͞Δ
    σʔλܕ͕มΘΔ৔߹

    View full-size slide

  42. 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)


    }

    View full-size slide

  43. ࣗલ࣮૷74֎෦αʔϏε

    View full-size slide

  44. /*


    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

    View full-size slide

  45. # 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になる

    View full-size slide

  46. ֎෦αʔϏεͷൺֱද
    جຊతػೳੑʹ͸େࠩͳ͘'JSFCBTF͕͓खࠒ

    ൺֱ߲໨
    'JSFCBTF
    3FNPUF$PO
    fi
    H
    -BVODI%BSLMZ TQMJUJP 'MBHTIJQ
    ಛ௃
    ଞͷ'JSFCBTF
    αʔϏεͱͷ࿈ܞ
    ϦΞϧλΠϜੑ
    NTҎ಺Ͱ഑৴
    ܭଌ΍؂ࢹͷ
    ػೳ෇͖
    ࣗಈϩʔϧόοΫ
    ஈ֊తϦϦʔε
    ྉۚ ແྉ NPOUId ໊·Ͱແྉ NPOUId
    GFUDIճ਺੍ݶ ࣌ؒҎ಺ճ·Ͱ ެࣜͰ໌ݴͤͣ ͳ͠ ͳ͠

    View full-size slide

  47. w σϑΥϧτͰλʔήοτΞϓ
    Ϧɾ04ɾࠃͳͲΛࢦఆՄೳ
    w ೚ҙϢʔβʔϓϩύςΟʹՃ͑ɺ
    ϥϯμϜͳׂΓৼΓ΍೔෇ϕʔε
    ͷ෼ذ΋Մ
    w औಘ੍ݶͱΩϟογϡͷͨΊɺ
    සൟͳมߋʹ͸஫ҙ
    'JSFCBTF3FNPUF$POGJHͷ৚݅ࢦఆ

    View full-size slide

  48. w ύϥϝʔλ໊ɾσʔλܕɾσϑ
    Υϧτ஋Λࢦఆ
    w ෼ذ৚݅Λࢦఆ͢Δ
    w "QQ%FMFHBUFͰύϥϝʔλΛ
    GFUDIͯ͠ར༻͢Δ
    3FNPUF$POGJHͰͷϑϥάఆٛ

    import Firebase


    let remoteConfig = RemoteConfig.remoteConfig()


    let isNewFeatureEnabled =


    remoteConfig["newFeatureEnabled"].boolValue
    'FBUVSF'MBHTTXJGU

    View full-size slide

  49. ࢖͍෼͚
    ϦϦʔε SFWFSU
    λΠϛϯάͱ෼ذ৚݅࣍ୈ
    ࣗલ࣮૷
    w ΞϓϦϦϦʔεػೳϦϦʔε
    w ෼ذ৚͕݅੩త

    ֎෦αʔϏε αʔόʔ੍ޚ

    w ΞϓϦϦϦʔεͱػೳϦϦʔεͷ
    λΠϛϯάΛ෼͚͍ͨ
    w ෼ذ৚݅Λಈతɾৄࡉʹม͍͑ͨ

    View full-size slide

  50. ࣗલ࣮૷74֎෦αʔϏε
    ϦϦʔεɾλʔήςΟϯάͰ੍໿͕͋ΔͳΒ֎෦αʔϏεΛݕ౼

    ൺֱ߲໨
    ࣗલ࣮૷
    ΫϥΠΞϯτͷΈ

    ֎෦αʔϏε
    ϑϥά൓సλΠϛϯά Ϗϧυ࣌ ೚ҙλΠϛϯά
    ϑϥά؅ཧมߋ৔ॴ ΫϥΠΞϯτͷίʔυ αʔόʔ(6*
    λʔήςΟϯάɾ࣌ݶࣜ ೉͍͠ Մೳ
    ґଘ ͳ͠ 4%,͕ඞཁ
    Ձ֨ ແྉ ༗ྉͷ৔߹͋Γ

    View full-size slide

  51. 'FBUVSF'MBHͷ5*14

    View full-size slide

  52. '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 より

    View full-size slide

  53. ։ൃ్தͰͷಋೖɾมߋ΋͋Γ
    ϦϦʔεͷείʔϓɾλΠϛϯά͕มΘͬͨΒ
    w ౰ॳͷείʔϓͷҰ෦ΛઌߦతʹϦϦʔε͍ͨ͠
    w 'FBUVSF'MBHΛ෼ׂ͢Δ͜ͱͰείʔϓ෼ׂ͕Մೳ
    w ʮҙࢥܾఆϩδοΫ͔ΒҙࢥܾఆϙΠϯτΛ੾Γ཭͢ʯ
    NBSUJOGPXMFSDPNΑΓ

    w ޙ͔Βͷ'FBUVSF'MBHಋೖ΋͋Γ

    View full-size slide

  54. 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()


    }

    View full-size slide

  55. ࡟আ࣌ͷϛε๷ࢭ؍఺͔Β
    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


    }

    View full-size slide

  56. 'FBUVSF'MBHར༻࣌ͷ஫ҙ఺

    View full-size slide

  57. 'FBUVSF'MBHͷ஫ҙ఺
    ϑϥάͷ؅ཧʹ͸஫ҙ
    w ޡͬͨ൓స͕ى͜Βͳ͍Α͏ʹ஫ҙ͕ඞཁ
    w ಛJ04͸৹ࠪʹΑΓSFWFSUʹ΋͕͔͔࣌ؒΔ
    w 2"΍6*ςετ౳Ͱͷ֬ೝΛ
    w /JHIUMZ#VJMEͰ೔ࠒ͔Βࣾ಺νΣοΫ͢Δͷ΋༗ޮ

    View full-size slide

  58. 'FBUVSF'MBHͷ஫ҙ఺
    ෆཁϑϥάͷ࡟আ͸ૣΊʹ
    w ޡͬͨίʔυΛࢀর͢ΔϦεΫ͕͋ΔͷͰૣΊʹফ͢
    w ʮϑϥά࡟আ·Ͱ͕ϦϦʔεʯ
    w ࠓޙ͸࡟আͷࣗಈԽΛظ଴FH(JU)VCͷऔΓ૊Έ


    View full-size slide

  59. 'FBUVSF'MBHͷ஫ҙ఺
    1VMM3FRVFTU͸ૣࠞͥ͘Δ
    w τϥϯΫϕʔεͰࡉ͔͘1VMM3FRVFTUΛग़͢
    w ͨͩ͠ϨϏϡʔ଴ͪͷ13͕૿͑Δͷ͸Ξϯνύλʔϯ
    w ଴ͪͷ1VMM3FRVFTUΛݮΒ͠ίʔυΛ࠷৽ʹ
    w ϖΞɾϞϒϓϩ͢Δ
    w ϨϏϡʔ༻ͷ࣌ؒΛઃఆ

    View full-size slide

  60. 'FBUVSF'MBHͷ஫ҙ఺
    ར༻͕೉͍͠έʔε
    w ෳࡶͳ4UPSZCPBSE9JCϨΠΞ΢τ
    w ਌ϒϥϯνͰਐΊΔ͔࡞Γ௚͠Λݕ౼͢Δ
    w ৽چͷܕͷࠩ෼͕େ͖͍
    w ৽چܕͷϑΟʔϧυΛ༻ҙͯ͠΋෼ذࠔ೉ͳ৔߹΋
    w ந৅ԽϨΠϠʔΛઃ͚Δํ๏΋͋Γ
    w มߋՕॴ͕ΫϥΠΞϯτଆʹͳ͍
    w "1*όʔδϣϯΛม͑ΔPSαʔόʔʹϑϥάΛಋೖ

    View full-size slide

  61. ࠓ೔͔Β'FBUVSF'MBHΛೖΕͯΈΑ͏
    ߴ଎͔ͭετϨεϑϦʔͳσϦόϦʔ͕࣮ݱ
    w ฒߦ௕ظ։ൃͰ΋௥ैڝ߹͕গͳ͘ετϨεϑϦʔ
    w ༏ઌ౓ɾείʔϓมߋʹڧͯ͘ΞδϟΠϧ
    w ·ͣ͸ϩʔΧϧͷϑϥά͔Β࢝ΊΑ͏
    w ޡͬͨ൓సɾ࡟আʹ஫ҙɾؾ෇͚Δ޻෉Λ
    w ৗʹখ͘͞ɾߴ଎ʹϚʔδ͍ͯ͜͠͏

    View full-size slide

  62. 'FBUVSF'MBHΛ࢖ͬͨ։ൃͰ
    ߴ଎͔ͭετϨεϑϦʔͳσϦόϦʔΛ

    View full-size slide

  63. ࢀߟจݙ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

    View full-size slide