$30 off During Our Annual Pro Sale. View Details »

循環的複雑度を上げないためのSwiftプログラミングTips / Tips of Swift Programming to Reduce Code Complexity

循環的複雑度を上げないためのSwiftプログラミングTips / Tips of Swift Programming to Reduce Code Complexity

2019年4月10日に日本経済新聞社で行われたOtemachi.swift #3で発表した資料です。
Swiftプログラミングで循環的複雑度を上げないためのプログラミングTipsについて解説しました。

サンプルコード
imaizume/code-complexity-in-swift
https://github.com/imaizume/code-complexity-in-swift

循環的複雑度でフローの複雑性を把握する- マイナー・マイナー
https://minor.hatenablog.com/entry/20110207/1297083896

「サイクロマティック複雑度」の計測方法が全くわからなかったので調べてみたら超簡単だった件 (@uhooi)
https://qiita.com/uhooi/items/c77a53a4c7ac232a1ba1

lizard
http://www.lizard.ws/

diverse-inc/docker-lizard
https://github.com/diverse-inc/docker-lizard)

あなたはどう書きなぜそう書くのか? (@shiz)
https://speakerdeck.com/shiz/anatahadoushu-ki-nazesoushu-kufalseka

Protocol-Oriented Programming in Swift - WWDC 2015
https://developer.apple.com/videos/play/wwdc2015/408/

Tomohiro Imaizumi

April 10, 2019
Tweet

More Decks by Tomohiro Imaizumi

Other Decks in Programming

Transcript

  1. 循環的複雑度を上げないための
    SwiftプログラミングTips
    Otemachi.swift #3 at 日本経済新聞社
    @imaizume

    View Slide

  2. 今泉 智博 (@imaizume)
    株式会社Diverse / 株式会社Uzumaki
    マッチングアプリPoiboy iOS版開発担当
    ⚖ CoconalaでGitコンサルタントやってます
    近況:レーシックを受けて免許の限定解除しました
    ⾃⼰紹介

    View Slide

  3. & 循環的複雑度(CCN)について
    ' CCNを上げないためのTips x3
    ( まとめ
    本⽇のINDEX

    View Slide

  4. 循環的複雑度(CCN)について
    • 「良いコード」を⽰す1つの指標
    • 数え⽅: ifなどの条件⽂で閉じた領域数
    • 別解: 条件判定の数に+1した数
    • ⼀般的にはCCN≦10が理想
    • swiftlintのcyclomatic_complexityで検出可
    IF
    IF
    B C
    D
    A ① ②

    ←の例では
    CCN = 3

    View Slide

  5. CCNを算出するツール : lizard
    • 静的解析でメソッド毎のCCNを算出
    • Swiftをはじめとした複数⾔語に対応
    • -CオプションでCCNでのフィルタが可能
    • 任意環境で実⾏できるDocker Image: docker-lizard
    https://github.com/terryyin/lizard

    View Slide

  6. SwiftでCCNを下げるための⼩技を共有します
    (昨年の個⼈OKRでプロダクトのCCNを減らした経験から)
    本⽇のサンプルコードはこちらで公開してます

    View Slide

  7. TIPS1. Optional Bindingの削減
    (if letやguardによる分岐を減らす)

    View Slide

  8. Optional Bindingは便利…だけど
    • ⾒直すこと
    • Optionalの必要性
    • 値をbindする必要性
    • Optionalを使うなら✅
    • Binding処理を集約
    • Optional Chaining
    public func someComplexFunc(x: String?, y: String?) -> String {
    if let x = x, let y = y {
    return "\(x) and \(y)"
    } else if let x = x {
    return "\(x) only"
    } else if let y = y {
    return "\(y) only"
    } else {
    return "both is nil"
    }
    }
    public func someSimpleFunc(x: String, y: String) -> String {
    return "\(x) and \(y)"
    }

    View Slide

  9. Binding処理を集約する
    Bindingを1箇所で⾏いCCNが⾼い処理を集約
    class A {

    if let x = x, let y = y {
    someSimpleFunc(x: x, y: x)
    } else {

    }
    }
    class B {

    if let x = x, let y = y {
    someSimpleFunc(x: x, y: x)
    } else {

    }
    }
    public func someSimpleFunc(
    x: String, y: String
    ) -> String {…}
    class A {

    someComplexFunc(x: x, y: x)
    }
    class B {

    someComplexFunc(x: x, y: x)
    }
    public func someComplexFunc(
    x: String?, y: String?
    ) -> String {…}
    呼び出し側でBinding
    定義側でBinding

    View Slide

  10. 様々なUnwrap⽅法とCCNの⽐較
    // 非オプショナル引数 → CCN:1
    func nonOptionalArg(x: String) { print(x) }
    // オプショナル引数(Binding) → CCN:2
    func optionalArg(x: String?) {
    if let x = x {
    print(x)
    } else {
    print("nilです")
    }
    }
    // オプショナル引数(Chaining) → CCN:1
    func optionalChain(x: String?) { print(x ?? "nilです") }
    // オプショナル引数(三項演算子) → CCN:2
    func ternaryOperator(x: String?) { print(x != nil ? x! : "nilです") }
    ================================================
    NLOC CCN token PARAM length location
    ------------------------------------------------
    3 1 12 1 3 nonOptionalArg@27-29@Sources/AvoidUnncessaryOptional.swift
    7 2 26 1 7 optionalArg@31-37@Sources/AvoidUnncessaryOptional.swift
    3 1 14 1 3 optionalChain@39-41@Sources/AvoidUnncessaryOptional.swift
    3 2 18 1 3 ternaryOperator@43-45@Sources/AvoidUnncessaryOptional.swift
    1 file analyzed.
    Optional Chaining: CCN=1
    他の⽅法: CCN=2

    View Slide

  11. TIPS2. enumのcaseを肥⼤化させない
    (enumの整理と他の選択肢を考える)

    View Slide

  12. Swiftのenumは便利…しかし
    switch selfのcaseが増える
    caseが肥⼤化する
    CCNが増える
    不要caseの定期的な⾒直しが必要
    …しかしそもそもなぜ、switch内のcaseが増えるのか
    public enum SushiEnum: String {
    case マグロ
    case サバ
    case サケ
    case …
    public var enName: String {
    switch self {
    case .マグロ:
    return "tuna"
    case .サバ:
    return "mackerel"
    case .サケ:
    return "salmon"
    case …:
    return "…"
    }
    }
    }

    View Slide

  13. case:value=1:n→structとstatic定数を使う
    (クラスのような)複雑な情報をenumで持つとswitch caseが増加
    そこでenumではなくstructで持つことを検討する
    enumだとswitch caseが発⽣
    + jaName: String
    + enName: String
    + price: Int
    SushiStruct
    public struct SushiStruct {
    public let jaName: String
    public let enName: String
    public let price: Int
    private init(
    jaName: String,
    enName: String,
    price: Int
    ) {…}
    }
    extension SushiStruct {
    public static let tuna: SushiStruct = .init(
    jaName: "マグロ", enName: "tuna", price: 380)
    }
    }
    static定数で返すことで呼び出し⽅をenumと同じにできる

    View Slide

  14. structとstatic定数でのCCN
    メリット: extensionで分離できる
    デメリット: CaseIteratable.allCasesが使えない
    ※この実装についてより詳しく知りたい⽅は@shizさんのスライドをCHECK
    ================================================
    NLOC CCN token PARAM length location
    ------------------------------------------------
    10 4 29 0 10 enName@[email protected]
    10 4 29 0 10 price@[email protected]
    9 1 31 3 9 init@[email protected]
    3 1 23 0 3 tuna@[email protected]
    3 1 23 0 3 mackerel@[email protected]
    3 1 23 0 3 salmon@[email protected]
    1 file analyzed.
    struct + static定数: CCN = 1
    enum: CCN = N+1
    (N: enum内のcaseの数)

    View Slide

  15. TIPS3. Protocol Oriented Programming
    (引数ではなく適合型を増やす)

    View Slide

  16. Protocol Oriented Programmingとは
    必要な性質をprotocolに定義し適合させていくことで
    処理の共通化を図る考え⽅ (WWDC 2015)
    protocolの活⽤で分岐の減少やスタブの作成が可能
    + jaName: String
    + enName: String
    + price: Int
    <>
    FoodContract
    Sushi Sake Ramen
    System
    +fetchName(FoodContract)
    +displayPrice(FoodContract)
    protocol
    protocolに適合する型
    利⽤側はprotocolのみを知れば良い

    View Slide

  17. 男⼥で返り値を変えたいenum(タブメニュー)
    public enum
    ConditionalTabType: String {
    // 女性用
    case femaleMyPage
    case femaleTop
    case femaleMessage
    // 男性用
    case maleTop
    case questionBox
    case maleMessage
    case maleMypage

    public func index() -> Int {
    // 実際はアプリ内フラグから取得したりする
    if isFemale {
    switch self {
    case .femaleMyPage: return 0
    case .femaleTop: return 1
    case .femaleMessage: return 2
    default: return 1
    }
    } else {
    switch self {
    case .maleTop: return 0

    }
    }
    }
    public func name() -> String {
    switch self {
    case .femaleMessage, .maleMessage: return "メッセージ"
    case .femaleMyPage, .maleMypage: return "マイページ"
    case .femaleTop: return "女性トップ"
    case .maleTop: return "アピール"
    case .questionBox: return "質問箱"
    }
    }
    }

    View Slide

  18. 男⼥で返り値を変えたいenum(タブメニュー)
    public enum
    ConditionalTabType: String {
    // 女性用
    case femaleMyPage
    case femaleTop
    case femaleMessage
    // 男性用
    case maleTop
    case questionBox
    case maleMessage
    case maleMypage

    public func index() -> Int {
    // 実際はアプリ内フラグから取得したりする
    if isFemale {
    switch self {
    case .femaleMyPage: return 0
    case .femaleTop: return 1
    case .femaleMessage: return 2
    default: return 1
    }
    } else {
    switch self {
    case .maleTop: return 0

    }
    }
    }
    public func name() -> String {
    switch self {
    case .femaleMessage, .maleMessage: return "メッセージ"
    case .femaleMyPage, .maleMypage: return "マイページ"
    case .femaleTop: return "女性トップ"
    case .maleTop: return "アピール"
    case .questionBox: return "質問箱"
    }
    }
    }
    内部で条件分岐しちゃってる
    name = rawValueにできない

    View Slide

  19. コードの声を聞く
    男⼥でenumを分け
    protocolに準拠❗
    .oO(本当にほしいものはenumなのか❓)
    タブ切り替えで必要なインターフェイスがあれば良い
    public protocol TabTypeContract {
    func index() -> Int { get }
    func name() -> String { get }
    }
    public enum MaleTabType: String, TabTypeContract {
    case maleTop = "アピール"
    case questionBox = "質問箱"
    case maleMessage = "メッセージ"
    case maleMypage = "マイページ"
    public func index() -> Int {
    switch self {
    case .maleTop: return 0
    case .questionBox: return 1
    case .maleMessage: return 2
    case .maleMypage: return 3
    }
    }
    public func name() -> String {
    return self.rawValue
    }
    }

    View Slide

  20. コードの声を聞く
    男⼥でenumを分け
    protocolに準拠❗
    .oO(本当にほしいものはenumなのか❓)
    タブ切り替えで必要なインターフェイスがあれば良い
    public protocol TabTypeContract {
    func index() -> Int { get }
    func name() -> String { get }
    }
    public enum MaleTabType: String, TabTypeContract {
    case maleTop = "アピール"
    case questionBox = "質問箱"
    case maleMessage = "メッセージ"
    case maleMypage = "マイページ"
    public func index() -> Int {
    switch self {
    case .maleTop: return 0
    case .questionBox: return 1
    case .maleMessage: return 2
    case .maleMypage: return 3
    }
    }
    public func name() -> String {
    return self.rawValue
    }
    }
    name = rawValue
    型の分離と
    外部での分岐に成功

    View Slide

  21. CCNの⽐較
    ================================================
    NLOC CCN token PARAM length location
    ------------------------------------------------
    18 9 72 0 19 index@17-35@Sources/MakeModuleProtocolOriented.swift
    9 6 47 0 9 name@37-45@Sources/MakeModuleProtocolOriented.swift
    8 5 35 0 8 index@60-67@Sources/MakeModuleProtocolOriented.swift
    3 1 11 0 3 name@69-71@Sources/MakeModuleProtocolOriented.swift
    7 4 29 0 7 index@80-86@Sources/MakeModuleProtocolOriented.swift
    3 1 11 0 3 name@88-90@Sources/MakeModuleProtocolOriented.swift
    1 file analyzed.

    1メソッドあたりのCCNが⼤きく下がった✌

    View Slide

  22. CCNの⽐較
    ================================================
    NLOC CCN token PARAM length location
    ------------------------------------------------
    18 9 72 0 19 index@17-35@Sources/MakeModuleProtocolOriented.swift
    9 6 47 0 9 name@37-45@Sources/MakeModuleProtocolOriented.swift
    8 5 35 0 8 index@60-67@Sources/MakeModuleProtocolOriented.swift
    3 1 11 0 3 name@69-71@Sources/MakeModuleProtocolOriented.swift
    7 4 29 0 7 index@80-86@Sources/MakeModuleProtocolOriented.swift
    3 1 11 0 3 name@88-90@Sources/MakeModuleProtocolOriented.swift
    1 file analyzed.
    分割後(男性)
    分割後(⼥性)
    分割前(男⼥)

    1メソッドあたりのCCNが⼤きく下がった✌

    View Slide

  23. まとめ
    TIPS1. Optional Bindingの削減
    ✅ 不要Optionalの回避とOptional Chainingを活⽤
    TIPS2. enumのcaseを肥⼤化させない
    ✅ 複雑な構造の定義はstruct + static定数で
    TIPS3. Protocol Oriented Programming
    ✅ 必要なインターフェイスを定義しモジュールを分割

    View Slide

  24. 参考リンク
    • imaizume/code-complexity-in-swift
    https://github.com/imaizume/code-complexity-in-swift
    • 循環的複雑度でフローの複雑性を把握する- マイナー・マイナー
    https://minor.hatenablog.com/entry/20110207/1297083896
    • 「サイクロマティック複雑度」の計測⽅法が全くわからなかったので調べてみたら
    超簡単だった件 (@uhooi)
    https://qiita.com/uhooi/items/c77a53a4c7ac232a1ba1
    • lizard
    http://www.lizard.ws/
    • diverse-inc/docker-lizard
    https://github.com/diverse-inc/docker-lizard)
    • あなたはどう書き なぜそう書くのか? (@shiz)
    https://speakerdeck.com/shiz/anatahadoushu-ki-nazesoushu-kufalseka
    • Protocol-Oriented Programming in Swift - WWDC 2015
    https://developer.apple.com/videos/play/wwdc2015/408/

    View Slide