Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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)" }

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

様々な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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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 "…" } } }

Slide 13

Slide 13 text

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と同じにできる

Slide 14

Slide 14 text

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の数)

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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のみを知れば良い

Slide 17

Slide 17 text

男⼥で返り値を変えたい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 "質問箱" } } }

Slide 18

Slide 18 text

男⼥で返り値を変えたい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にできない

Slide 19

Slide 19 text

コードの声を聞く 男⼥で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 } }

Slide 20

Slide 20 text

コードの声を聞く 男⼥で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 型の分離と 外部での分岐に成功

Slide 21

Slide 21 text

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が⼤きく下がった✌

Slide 22

Slide 22 text

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が⼤きく下がった✌

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

参考リンク • 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/