『SRP原則違反指数』を計測して、技術的負債に立ち向かう

# はじめに

現在、弊組織全体で単体テストとリファクタリングの波がきておりコードをよくしていこうという機運の高まりを感じています。しかし、自分を含め多くの若手は『価値あるリファクタリング』の前段階で止まっていて、本質的に価値を生む品質改善に取り組めていないと感じています。（今は実力をつけている段階なので仕方がないと思っている）

一方で、メンバーそれぞれが『このリファクタリングは価値を生むのか？』を考えるための土壌を組織として醸成できているかは、長期的な視点で重要になります。そのためには、考えるきっかけとして 「**なにかしらの観点から定量的な数字を定期的に見てもらうこと**」が一番手っとり早いかなと思います。

https://mtx2s.hatenablog.com/entry/2023/04/26/230917

> コード品質の問題が組織の中で可視化されず、エンジニアしか認識できない状態に置かれているのだ。

組織の現状を考えると「コード品質をエンジニアメンバーみんな把握できているのか...？という状態」だと感じており（違ったら大変恐縮なのですが）、状況としては『**みんなぼんやりと技術的負債を日々の業務から感じてはいるけど、その先の具体的な改善に向かうための情報、つまり次のアクションの判断基準になる数字が不足している**』気がしています。

ということで、今回はSRP原則の観点からコード品質を可視化できるように簡単なシェルを書いてみました。

（いろいろ言ってるけど本当は『へ～簡単にSRP違反指数出せるんかやってみよ～』がモチベです...。）

# もくじ

- [そもそもSRP原則とは](#そもそもsrp原則とは)
- [計算の方針](#計算の方針)
- [SRP違反指数の式](#srp違反指数の式)
  - [Revisions | 修正リビジョンのユニーク数](#revisions--修正リビジョンのユニーク数)
  - [Unique Contributers | 修正ユーザーのユニーク数](#unique-contributers--修正ユーザーのユニーク数)
  - [Lines of Code | モジュールのライン数](#lines-of-code--モジュールのライン数)
- [計算のスクリプト](#計算のスクリプト)
- [実際使うときに、より考慮すべき点](#実際使うときにより考慮すべき点)
  - [REVISIONS / UNIQUE_CONTRIBUTERS の取得について](#revisions---unique_contributers-の取得について)
  - [bcコマンドの利用](#bcコマンドの利用)
  - [進捗率は消してもいい](#進捗率は消してもいい)
  - [処理速度がめちゃ遅い](#処理速度がめちゃ遅い)
- [おわりに](#おわりに)

# そもそもSRP原則とは

『単一責任の原則（`Single Responsibility Principle` / `SRP`）』は、ソフトウェア設計の原則のひとつで、ソフトウェアのモジュール（クラス、関数など）は、**１つのアクターに対してのみ責任を持つべきである** という考えです。SRP に従って設計されたシステムは、変更がしやすく、メンテナンス性が高いとされます。

https://www.ogis-ri.co.jp/otc/hiroba/others/OOcolumn/single-responsibility-principle.html

https://qiita.com/yoshinori_hisakawa/items/bbe258f7745d949a6bca

# 計算の方針

以下記事を参考にします。

https://gihyo.jp/dev/serial/01/perl-hackers-hub/000803

> - SRP＝R＋U＋((L/100)－5)
> - R：修正リビジョンのユニーク数
> - U：修正ユーザのユニーク数
> - L：モジュールのライン数
>
> この値が大きければ大きいほど単一の「大きな」モジュールが「何度も多くの人に」触られている状態であると言えます。これはコンポーネントの変更の多さに対してモジュールの分割が不十分であることを証明しているという判断によるものです。

# SRP違反指数の式

今回は、SRP違反指数を導出する式を以下のように定義してみました。

$$SRP原則違反指数 ＝ クラスの修正回数 + クラスの修正者数 +((クラス行数/100)－5)$$

これから SRP 原則違反の大犯罪クラスに手を入れていくことを見込んで、「モジュール」についてはクラスと定義しています。その他３つのパラメータについての説明は以下になります。

## Revisions | 修正リビジョンのユニーク数

この指標は、過去から現在までのコード変更回数を測定しています。

[リビジョン](https://e-words.jp/w/%E3%83%AA%E3%83%93%E3%82%B8%E3%83%A7%E3%83%B3.html) とは、ソースコード管理システムにおいて、ある時点でのプロジェクトのコードの状態を指します（ざっくり）。ゆえに「修正リビジョンのユニーク数」とは、そのクラスが過去に何度修正されたかを意味します。

Git でのコミットはそれぞれ個別のコミットIDによって識別されますが、あるクラスがそれぞれ独自のコミットIDで修正された回数を数えることによって「修正コミットのユニーク数」を定めます。

SRP の原則に則ると、1つのファイルに多数の修正コミットが存在することは「複数の責務がそのクラスに与えられている」ことを意味し、単一責任の原則が破られている可能性を示唆しています。

## Unique Contributers | 修正ユーザーのユニーク数

この指標は、クラスに変更を加えた個別のユーザーの数を測定しています。

各サブドメインが専門的なチームによってメンテナンスされるという文脈（前提）を踏まえた上で、１つのクラスが多くの異なるユーザーによって手が加えられている状況は、さまざまな人々がそのクラスに対して理解を深め、変更を加える動機を見出していると言えそうです。つまり、そのクラスが複数の異なる責任を背負い過ぎていることを示唆しています。

これはクラスの変更に関わる人が多いという事実から SRP の原則に逆らっている根拠たりえるはずです。クラスが持つべき単一の責務が定義から逸脱している可能性が高く、コードの改善を判断する１つのアラートとなると思います。

## Lines of Code | モジュールのライン数

この指標は、文字通りそのクラスが含むコード行の総数を測定します。

クラス内のコード行が増加することは、認知的・循環的複雑度の増大、変更容易性の低下、それゆえにバグの温床となるリスクを指し示します。

コード行数が極端に増えたクラスはやがて、その全知全能の存在感により『神クラス』と畏怖される名を冠することになるでしょう。その偉大なる存在はプロダクトコードに鎮座し、並みの開発者では手を出すことができない、圧倒的な力を宿したままあり続けます。（そういう意味で弊プロダクトは多神教！）

ゆえに、クラスの行数が極端に多い場合、それは１つのクラスが様々な機能や責任を一手に担っていることを暗示しており、SRPの原則に反します。適切なリファクタリングによって明確に責務の分割を行い、より健全なコードベースへと繋げる必要性を感じます。


# 計算のスクリプト

以下が実行シェルスクリプトです。こんなに長くなる予定じゃなかったのですが...やってることは単純です。

```bash:srp_violation_analyzer.sh
#!/bin/bash

DIRECTORY_PATH=$1
OUTPUT_FILE="srp_violation_score.csv"
FILE_LIST=($(find "$DIRECTORY_PATH" -type f)) # 指定されたディレクトリ内の全ファイルを取得
TOTAL_FILES=${#FILE_LIST[@]}
CURRENT_COUNT=0
PROGRESS_INTERVAL=$((TOTAL_FILES / 20)) # 全ファイルの5%ごとに進捗表示を更新する

# 出力ファイルが既に存在する場合は空にする
> "$OUTPUT_FILE"

echo -ne "Progress: $CURRENT_COUNT/$TOTAL_FILES (0%)\r" # 進捗の初期表示（0%）

# 各ファイルごとにSRP違反指数を計測する
for FILE_PATH in "${FILE_LIST[@]}"; do

  FILE_NAME=$(basename "$FILE_PATH") 
  GIT_LOG=$(git log --pretty=format:"%H %an" -- "$FILE_PATH")

  # 変更数、コントリビュータ数、コード行数を取得
  REVISIONS=$(echo "$GIT_LOG" | cut -d ' ' -f 1 | sort | uniq | wc -l)
  UNIQUE_CONTRIBUTERS=$(echo "$GIT_LOG" | cut -d ' ' -f 2- | sort | uniq | wc -l)
  LINES_OF_CODE=$(wc -l < $FILE_PATH)

  # SRP指数を計算するための一時変数を計算（浮動小数点演算を避けるために100倍しておく）
  TEMP_SRP_SCORE=$((REVISIONS * 100 + UNIQUE_CONTRIBUTERS * 100 + LINES_OF_CODE - 500))

  # 実際のSRP指数を計算（元に戻すために100で割る）
  SRP_VIOLATION_SCORE=$(($TEMP_SRP_SCORE / 100))

  # 結果をCSV形式で出力ファイルに追記する
  echo "$SRP_VIOLATION_SCORE,$REVISIONS,$UNIQUE_CONTRIBUTERS,$LINES_OF_CODE,$FILE_NAME,$FILE_PATH" >> "$OUTPUT_FILE"

  # 進行カウントを増やす
  ((CURRENT_COUNT++))
  if [ $((CURRENT_COUNT % PROGRESS_INTERVAL)) -eq 0 ]; then
    PROGRESS=$((CURRENT_COUNT * 10000 / TOTAL_FILES)) 
    PROGRESS=$(printf "%.2f%%" "$((PROGRESS))e-2")
    # 進捗を表示
    echo -ne "PROGRESS: $CURRENT_COUNT/$TOTAL_FILES ($PROGRESS)\r"
  fi

done

echo -e "\nInitial Processing complete. Now sorting the results..."

# データを降順にソートして一時ファイルに出力
sort -t, -k1,1nr "$OUTPUT_FILE" > "sorted_$OUTPUT_FILE"

# ソートしたデータにヘッダー行を追加して元のファイルに上書き保存
{
  echo "SRP_VIOLATION_SCORE,REVISIONS,UNIQUE_CONTRIBUTERS,LINES_OF_CODE,FILE_NAME,FILE_PATH"
  cat "sorted_$OUTPUT_FILE"
} > "$OUTPUT_FILE"

# 一時ファイルを削除
rm "sorted_$OUTPUT_FILE"

echo "All processing, including sorting, is complete."
```

実行時は以下のような形でコマンド叩けば、あとはいい感じにCSV形式で出力されます。

```terminal
$ ./srp_violation_analyzer.sh <ディレクトリのパス>
PROGRESS: XXX/XXX (XX.XX%)
Initial Processing complete. Now sorting the results...
All processing, including sorting, is complete.
```

# 実際使うときに、より考慮すべき点

今回は上記のスクリプトで作ってはみたものの、組織の文脈や欲しい情報などの要件によってコマンドを変える必要があります。

## REVISIONS  / UNIQUE_CONTRIBUTERS の取得について

- **REVISIONS について**
  - コミットハッシュでカウントしている
    - １つのPRの中に複数回修正のコミットがある場合、都度カウントされる
    - １つのPRで生まれる差分は１つの目的を持つ、という前提で同じPRに含まれるのであれば１とカウントしたほうが正確かも
      - その場合は `--first-parent` オプションを `git log` に追加することで対処できるぽい（未検証）
- **UNIQUE_CONTRIBUTERS について**
  - ファイル変更の歴史に関するユーザーの範囲が広い
    - `git log` では、ファイルの全変更履歴にわたるユニークな貢献者をカウントしているので、ファイルの初期作成者から最新の編集者まで含まれている
    - ファイルの " 現在の内容 " に貢献しているユーザーのみを知りたい場合は、`git blame` を使用してカウントすべき


## bcコマンドの利用

今回は演算するときに `bash` の組み込み機能で処理してますが、 `bc` コマンドを使用して簡略化できると思います。また、より複雑な浮動小数点演算やより精密な計算を実施したい場合は、`bc` コマンド使った方がいいかもです。

```bash
# しゃーなし一時変数を使っている
TEMP_SRP_SCORE=$((REVISIONS * 100 + UNIQUE_CONTRIBUTERS * 100 + LINES_OF_CODE - 500))
あとSRP_VIOLATION_SCORE=$(($TEMP_SRP_SCORE / 100))

# ここも簡略化できる
PROGRESS=$((CURRENT_COUNT * 10000 / TOTAL_FILES))
PROGRESS=$(printf "%.2f%%" "$((PROGRESS))e-2")
```

## 進捗率は消してもいい

シェルを実行させたときに、処理が長いとプロンプトが返ってこなくて不安になるので差し込んでます。

進捗率を表示させるのは言ってしまえば不要な部分なので、処理速度の観点から消してしまっても問題ないです。

## 処理速度がめちゃ遅い

まぁ～遅い。PCでローカル実行させたとき、20000ファイルくらいを対象にしたらだいたい８時間くらいかかりました。

forループの中で各ファイルごとに `git log` しているのと毎回ファイルに書き込みしているのが最大の原因。ファイルの書き込みについては全データを変数に保持させて最後に１度だけファイルに書き込むとかすれば割と早くなりそう。

# おわりに

以下イベントに参加した際に「SRP違反指数が出せるよ！」てのを知って、やってみたくて、良い感じに組織とこじつけて記事にしてみました。たのしかったです。

https://findy.connpass.com/event/313471/

数字は出して終わり、じゃ意味ないので改善活動につなげたいところですね。