循環的複雑度を上げないための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/

1a74617b91d2757b839b9cf3614648ce?s=128

Tomohiro Imaizumi

April 10, 2019
Tweet

Transcript

  1. 4.

    循環的複雑度(CCN)について • 「良いコード」を⽰す1つの指標 • 数え⽅: ifなどの条件⽂で閉じた領域数 • 別解: 条件判定の数に+1した数 •

    ⼀般的にはCCN≦10が理想 • swiftlintのcyclomatic_complexityで検出可 IF IF B C D A ① ② ③ ←の例では CCN = 3
  2. 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)" }
  3. 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
  4. 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
  5. 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 "…" } } }
  6. 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と同じにできる
  7. 14.

    structとstatic定数でのCCN メリット: extensionで分離できる デメリット: CaseIteratable.allCasesが使えない ※この実装についてより詳しく知りたい⽅は@shizさんのスライドをCHECK ================================================ NLOC CCN token

    PARAM length location ------------------------------------------------ 10 4 29 0 10 enName@13-22@UserStructInSteadOfEnum.swift 10 4 29 0 10 price@24-33@UserStructInSteadOfEnum.swift 9 1 31 3 9 init@43-51@UserStructInSteadOfEnum.swift 3 1 23 0 3 tuna@55-57@UserStructInSteadOfEnum.swift 3 1 23 0 3 mackerel@59-61@UserStructInSteadOfEnum.swift 3 1 23 0 3 salmon@63-65@UserStructInSteadOfEnum.swift 1 file analyzed. struct + static定数: CCN = 1 enum: CCN = N+1 (N: enum内のcaseの数)
  8. 16.

    Protocol Oriented Programmingとは 必要な性質をprotocolに定義し適合させていくことで 処理の共通化を図る考え⽅ (WWDC 2015) protocolの活⽤で分岐の減少やスタブの作成が可能 + jaName:

    String + enName: String + price: Int <<interface>> FoodContract Sushi Sake Ramen System +fetchName(FoodContract) +displayPrice(FoodContract) protocol protocolに適合する型 利⽤側はprotocolのみを知れば良い
  9. 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 "質問箱" } } }
  10. 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にできない
  11. 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 } }
  12. 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 型の分離と 外部での分岐に成功
  13. 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が⼤きく下がった✌
  14. 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が⼤きく下がった✌
  15. 23.

    まとめ TIPS1. Optional Bindingの削減 ✅ 不要Optionalの回避とOptional Chainingを活⽤ TIPS2. enumのcaseを肥⼤化させない ✅

    複雑な構造の定義はstruct + static定数で TIPS3. Protocol Oriented Programming ✅ 必要なインターフェイスを定義しモジュールを分割
  16. 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/