Slide 1

Slide 1 text

After Party iOSDC Japan 2022 toya108 MaintainabilityIndexを 計測しながらコードの保守性を 改善している話 1

Slide 2

Slide 2 text

● 大体toya108という名前でやってます ● Cookpad Inc. iOS Developer ● クライミングが好き ● 3週間前に父になりました 自己紹介 2

Slide 3

Slide 3 text

● MaintainabilityIndexが何か分かる ● MaintainabilityIndexを計測するメリットが分かる ● MaintainabilityIndexの計測方法が分かる 今日のゴール 3

Slide 4

Slide 4 text

● クイズ ● MaintainabilityIndexとは ● MaintainabilityIndexを測るメリット ● どうやってMaintainabilityIndexを計測しているか ● まとめ アジェンダ 4

Slide 5

Slide 5 text

クイズ 5

Slide 6

Slide 6 text

Q. ios-cookpadのレポジトリの中で特にメンテナンスが難 しい箇所はどこでしょうか? (file、またはmodule単位で回答) 突然ですがクイズです 6

Slide 7

Slide 7 text

A.多分みんな分からない 7

Slide 8

Slide 8 text

Q.なぜ分からないか A.読んだことがないから 8

Slide 9

Slide 9 text

でもそれって結構困るのでは? ● 読まれずに放置されてたコードは保守しやすい状態になってるのか分からな い ● 放置されていたコードに突然の変更が必要になる ● 辛い ということが起きるかも。。。 読んだことがないコードは保守しやすいかどうか分か らない 9

Slide 10

Slide 10 text

● どこから読んでいくのか ● 巨大なコード群が日々更新される ● メンバーも定期的に入れ替わる ● 読んだ箇所も解像度が人によって違う ○ 実装者しか把握していない難しさがあったりする じゃあ全部読みます!は実質不可能 10

Slide 11

Slide 11 text

コードを読まなくても 保守しやすい状態かどうか 知る方法があればいいのでは? MaintainabiltyIndexを 測ってみよう! 11

Slide 12

Slide 12 text

MaintainabilityIndexとは 12

Slide 13

Slide 13 text

● 保守容易性指数、つまりコードがどれくらい保守しやすいかを表す指標の こと ● VSCodeで一部の言語で計測可能 ○ https://learn.microsoft.com/ja-jp/visualstudio/code-quality/code-me trics-maintainability-index-range-and-meaning?view=vs-2022 ● Dartでも計測できる ○ https://dartcodemetrics.dev/docs/metrics/maintainability-index MaintainabiltyIndexとは 13

Slide 14

Slide 14 text

● 0~100の数値で保守容易性を出す ● 3段階でコードのヤバさを知ることができる MaintainabiltyIndexとは 14 0~9 ヤバい 10~19 やや危険 20~100 大丈夫そう

Slide 15

Slide 15 text

MaintainabilityIndexの計算式 Maintainability Index = MAX(0,(171 - 5.2 * ln(Halstead Volume) - 0.23 * (Cyclomatic Complexity) - 16.2 * ln(Lines of Code))*100 / 171) ※詳細は「どうやって MaintainabilityIndexの計測しているか」で説明するよ。 15

Slide 16

Slide 16 text

複雑そうだと思っていた 箇所は実際に赤になって おり、体感と一致してる! swiftのfileごとのMaintainabilityIndexを スプレッドシートで確認できるようにしている 16

Slide 17

Slide 17 text

MaintainabilityIndexを 計測するメリット 17

Slide 18

Slide 18 text

隔週の技術改善MTGにて どこか改善したい箇所 ありますか? ● 読んだことない箇所は改善す べきか分からないなあ ● どこから読めばいいかも検討 つかないしなあ 思いつかないですね。 同僚氏 わい 18

Slide 19

Slide 19 text

MaintainabiltyIndexを見て 改善すべき箇所にあたりつける Legacy配下がヤバいな。 特にマイレシピ画面とか 0(最低値) になってる。 わい 19

Slide 20

Slide 20 text

読んだことがないコードでも手を入れるのが難しそうか ざっと把握できる。 MaintainabiltyIndexを測るメリット① 20

Slide 21

Slide 21 text

マイレシピ画面の実態 ● VIPER化されていないFatViewController ● TableViewと旧時代のDataSourceの使用 ● Legacyモジュール内の様々なコードへの依存 実際にコードを読んでみて実態を把握する ヤバすぎるっぴ! わい 21

Slide 22

Slide 22 text

コード生成を用いたiOSアプリマルチモジュール化のための依存解決 
 ※ios-cookpadのアーキテクチャについては以下 を参照ください。 22

Slide 23

Slide 23 text

次の技術改善MTGにて どこか改善したい箇所 ありますか? マイレシピ画面をリファクタしたいです。 同僚氏 わい お、ええで 23

Slide 24

Slide 24 text

● FatViewControllerをVIPERSceneに分割する ○ 他のコードとアーキテクチャが揃うので読みやすくなる ○ コードが分割されてMaintainabilityIndexの数値も改善するはず ● 元はLegacyの配下にあったが、独立したモジュールに切り出す ○ 必要なものはCoreに移動し、それ以外はマイレシピのためのモジュールに閉じ込 める ○ コードが分割されてMaintainabilityIndexの数値も改善するはず ○ 副次的にSandboxビルドで動作確認がしやすくなる ● SwiftUIにする ○ TableViewのIndexPath管理から脱却したい ○ 最近チームでもSwiftUI化を積極的に行っているので リファクタの方針決め 24

Slide 25

Slide 25 text

リファクタ後の計測結果 Before: 0 After: 14.57 file数は増えたが、その分 1fileあたりの コード量とロジック量が減らせている ※まだリリースはできてないけど こうなる予定 25

Slide 26

Slide 26 text

変更の前後で以前より保守しやすくなったかどうかを 数値で確認できる。 MaintainabiltyIndexを測るメリット② 26

Slide 27

Slide 27 text

● 読んだことがないコードでも手を入れるのが難しそうか ざっと把握できる。 ● 変更の前後で以前より保守しやすくなったかどうかを 数値で確認できる。 MaintainabiltyIndexを測るメリットまとめ 27

Slide 28

Slide 28 text

● 今回はfileごとに解析しているので、fileより小さい単位で保守が難しい コードを特定することはできない ● コード量、ロジックの複雑さに起因しない難しさを評価することはできな い。(背景知識が必要なコードや古いOSで実装したコードなど) できないこともある 28

Slide 29

Slide 29 text

29 ● Dartのdocumentでこのように明言されている ○ https://dartcodemetrics.dev/docs/metrics/maintainability -index ● 数値を過信して、コードを読まずに現在の状況を判断するのは良くなさそ う。 (注意) MaintainabilityIndexは実験的な 数値なため、過信することも禁物 注: 保守性指標はまだ非常に実験的な指標であり、   他の指標ほど真剣に考慮すべきではありません。

Slide 30

Slide 30 text

どうやって MaintainabilityIndexを 計測しているか 30

Slide 31

Slide 31 text

MaintainabilityIndexの計算式 Maintainability Index = MAX(0,(171 - 5.2 * ln(Halstead Volume) - 0.23 * (Cyclomatic Complexity) - 16.2 * ln(Lines of Code))*100 / 171) これをSwiftで計算している。 31

Slide 32

Slide 32 text

swiftでMaintainabilityIndexを計算する let rebasedHalsteadVolume = 5.2 * log(Double(halsteadVolume)) let rebasedCyclomaticComplexity = 0.23 * Double(cyclomaticComplexity) let rebasedLineOfCode = 16.2 * log(Double(lineOfCode)) let maitainabilityIndex = max(0, CGFloat(171 - rebasedHalsteadVolume - rebasedCyclomaticComplexity - rebasedLineOfCode) * 100 / 171) 32

Slide 33

Slide 33 text

①Halstead Volume Maintainability Index = MAX(0,(171 - 5.2 * ln(Halstead Volume) - 0.23 * (Cyclomatic Complexity) - 16.2 * ln(Lines of Code))*100 / 171) 33

Slide 34

Slide 34 text

Halsteadさんが提唱したコードの複雑度を測るための指標の一つ https://dartcodemetrics.dev/docs/metrics/halstead-volume Halstead Volumeとは η1 = the number of distinct operators η2 = the number of distinct operands N1 = the total number of operators N2 = the total number of operands Program vocabulary: η=η1+η2 Program length: N=N1+N2 Volume: V=Nlog2η 34

Slide 35

Slide 35 text

なんか色々書いてるけど、コードの中のoperatorとoperandの総数、そして それぞれの重複を弾いた個数が分かれば、あとは式に突っ込むだけ。 ※operator = 演算子のこと。+ とか - とか ※operand = 被演算子のこと。1 + 1 の 1はoperandになる Halstead Volumeとは 35

Slide 36

Slide 36 text

SwiftでHalsteadVolumeを計算する import Accelerate // logの使用に必要 let operators: [String] let distinctOperators: Set let operands: [String] let distinctOperands: Set let vocabulary= Float(distinctOperators.count + distinctOperands.count) let length = Float(operators.count + operands.count) let halsteadVolume = length * log2(vocabulary) 36

Slide 37

Slide 37 text

Q. operatorとoperandの数は どうやってカウントするの? 37

Slide 38

Slide 38 text

https://github.com/apple/swift-syntax ● Swiftのソースコードを解析したり変換できるライブラリ ● これを使ってコードの中のoperatorやoperandの数をカウントする SwiftSyntaxを利用する 38

Slide 39

Slide 39 text

operatorとoperandの数をカウントする import SwiftSyntax import SwiftSyntaxParser // 計測用のVisitorを用意する class MaintainabilityIndexVisitor: SyntaxVisitor { var operators: [String] = [] var distinctOperators: Set { Set(operators) } var operands: [String] = [] var distinctOperands: Set { Set(operands) } … } 39

Slide 40

Slide 40 text

operatorとoperandの数をカウントする override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind { switch node.tokenKind { case .spacedBinaryOperator, .unspacedBinaryOperator, .prefixOperator, .postfixOperator: operators.append(node.text) default: // Add non-reserved words to operands if !node.tokenKind.isKeyword && !node.text.isEmpty { operands.append(node.text) } } } 40

Slide 41

Slide 41 text

visitorにsourceを食わせる let sourceFile: SourceFileSyntax = try SyntaxParser.parse(source: source) let visitor = MaintainabilityIndexVisitor() visitor.walk(sourceFile) 41

Slide 42

Slide 42 text

visitorにsourceを食わせる // walk()の実行後にここに入ってくる。 override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind { switch node.tokenKind { case .spacedBinaryOperator, .unspacedBinaryOperator, .prefixOperator, .postfixOperator: operators.append(node.text) default: // Add non-reserved words to operands if !node.tokenKind.isKeyword && !node.text.isEmpty { operands.append(node.text) } } } 42

Slide 43

Slide 43 text

②Cyclomatic Complexity Maintainability Index = MAX(0,(171 - 5.2 * ln(Halstead Volume) - 0.23 * (Cyclomatic Complexity) - 16.2 * ln(Lines of Code))*100 / 171) 43

Slide 44

Slide 44 text

● コードの分岐の数を合計し、コードの複雑度を測る指標のこと ○ 循環的複雑度と訳される ● 1からスタートし、分岐(if・for・switch・whileなど)の度に「1」を加算する ● swiftlintのruleにも追加されている ○ https://realm.github.io/SwiftLint/cyclomatic_complexity.h tml Cyclomatic Complexityとは 44

Slide 45

Slide 45 text

Cyclomatic Complexityを計算する var cyclomaticComplecity = 1 override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind { switch node.tokenKind { case .ifKeyword, .switchKeyword, .forKeyword, .whileKeyword, .guardKeyword, .caseKeyword: cyclomaticComplecity += 1 case .identifier(let identifier): if identifier == "forEach" { cyclomaticComplecity += 1 } … 45

Slide 46

Slide 46 text

● 加算する時のstatementは大体swiftlintと揃えている ○ https://github.com/realm/SwiftLint/blob/4382ef49b9af08cfc1 3cc145e206a63e89e03367/Source/SwiftLintFramework/Rules /RuleConfigurations/CyclomaticComplexityConfiguration.swift #L15 ● forEachだけ別にハンドリングしてるのはそのため ● repeatはwhileとセットで一つの分岐なので加算してない ● if case も一つの分岐なのでif と caseでそれぞれ加算したりは してない Cyclomatic Complexityを計算する 46

Slide 47

Slide 47 text

③Lines of Code Maintainability Index = MAX(0,(171 - 5.2 * ln(Halstead Volume) - 0.23 * (Cyclomatic Complexity) - 16.2 * ln(Lines of Code))*100 / 171) 47

Slide 48

Slide 48 text

● コードの行数のこと ● 今回は空行を除いた論理LOCを使っている Line of Codeとは 48

Slide 49

Slide 49 text

try CommandLine.arguments.forEach { argument in let url = URL(fileURLWithPath: argument) let fileData = try Data(contentsOf: url) let source = fileData.withUnsafeBytes { buf in return String(decoding: buf.bindMemory(to: UInt8.self), as: UTF8.self) } let lineOfCode = source.split(separator: "\n").count Line of Codeの出し方 ● sourceを改行ごとに分割してcountする 49

Slide 50

Slide 50 text

swiftでMaintainabilityIndexを計算する let rebasedHalsteadVolume = 5.2 * log(Double(halsteadVolume)) let rebasedCyclomaticComplexity = 0.23 * Double(cyclomaticComplexity) let rebasedLineOfCode = 16.2 * log(Double(lineOfCode)) let maitainabilityIndex = max(0, CGFloat(171 - rebasedHalsteadVolume - rebasedCyclomaticComplexity - rebasedLineOfCode) * 100 / 171) print("\(sourcePath), \(halsteadVolume), \(cyclomaticComplexity), \(lineOfCode), \(maitainabilityIndex)") 50

Slide 51

Slide 51 text

51 https://github.com/toya108/MaintainabilityIndexCalculator 実装が気になる方はこちらをどうぞ

Slide 52

Slide 52 text

Q. 出力したMaintainabilityIndexは どうやって取り回してるの? 52

Slide 53

Slide 53 text

● コンマ区切りで出力した数値をス プレッドシートにコピペする ● コンマごとに列を分割する ● スプレッドシートの数式で MaintainabilityIndexごとに行 を色分けする 出力したMaintainabilityIndexの取り回し 53

Slide 54

Slide 54 text

● コンマ区切りで出力した数値をス プレッドシートにコピペする ● コンマごとに列を分割する ● スプレッドシートの数式でファイル ごとに色分けする 出力したMaintainabilityIndexの取り回し 温かみのある手作業! 54

Slide 55

Slide 55 text

● リリース用のビルドを作るコマンドと合体させて、リリースバージョンごとに S3かどこかにアップロードしておいて、ブラウザから確認できるようにする とか ● やっていきが必要 (課題) 毎回手作業でスプレッドシートに移すのしんど いので何とかしたい 55

Slide 56

Slide 56 text

● 前述したフローを行うハードルが高いので定期的に値を計測し、改善する 運用フローまでは作れていない ● 計測した値を元にコードを改善することが目的のため、計測のハードルは 下げていきたい その他課題 56

Slide 57

Slide 57 text

まとめ 57

Slide 58

Slide 58 text

コードを読みにいかなくても 保守しやすい状態かどうか知りたい? MaintainabiltyIndexを 測ってみよう! 58

Slide 59

Slide 59 text

● 読んだことがないコードでも手を入れるのが難しそうか ざっと把握できる。 ● 変更の前後で以前より保守しやすくなったかどうかを 数値で確認できる。 MaintainabiltyIndexを測るメリットまとめ 59

Slide 60

Slide 60 text

● コマンドラインからswiftのfileのpathを渡す ● SwiftSyntaxで解析する ● 式に従って計算して出力する ● 温かみのある手作業によりスプレッドシートへまとめる MaintainabilityIndexの計測方法 60

Slide 61

Slide 61 text

● https://learn.microsoft.com/en-us/visualstudio/code-quality/code-metric s-maintainability-index-range-and-meaning?view=vs-2022 ● https://realm.github.io/SwiftLint/rule-directory.html ● https://github.com/apple/swift-syntax ● https://monoist.itmedia.co.jp/mn/articles/1010/19/news124.html ● https://dartcodemetrics.dev/docs/metrics/maintainability-index ● https://blog.yagipy.me/analyze-maintainability-index 参考資料 61