パッケージ間の依存をチェックする静的解析ツール「go-depcheck」を作った
はじめに
こんにちは。株式会社バニッシュ・スタンダードのhidechaeです。普段はGoを書いています。
弊社では Clean Architecture を採用しています。さらに最近はプロダクトの拡大に伴いモジュラーモノリスを導入しました。
レイヤー間やモジュール間の依存ルールが増えてきたことで、依存のミスが増えてきました。すべてをコードレビューで検査するのは大変なので、CIでチェックできるように静的解析ツールを作成しました。
この記事では、このツールで何ができるのか、どのように使っているのかを紹介します。
go-depcheck
以下の設定を書くことができます。
- ブラックリスト形式で依存禁止ルールを定義できる
- ルールの中で例外的に許容する依存を設定できる
- 検査対象外とするファイルパターンを設定できる
使い方
インストール
go install github.com/v-standard/go-depcheck/cmd/depcheck@latest
depcheck.yml
の作成
設定ファイルを作成します。
例として、以下のような依存関係を考えてみます。
また、ディレクトリ構成は以下のようになっているとします。
.
├── domain
├── infrastructure
├── presentation
└── usecase
依存禁止ルールは以下のようになります。
- Presentation から Infrastructure への依存はできない
- Usecase から Infrastructure, Presentation への依存はできない
- Domain から Infrastructure, Presentation, Usecase への依存はできない
- Infrastructure から Presentation への依存はできない
これをymlで書くと以下のように書きます。
rules:
# Presentation から Infrastructure への依存はできない
- from: "^github.com/your-org/project/presentation.*$"
to:
- "^github.com/your-org/project/infrastructure.*$"
# Usecase から Infrastructure, Presentation への依存はできない
- from: "^github.com/your-org/project/usecase.*$"
to:
- "^github.com/your-org/project/infrastructure.*$"
- "^github.com/your-org/project/presentation.*$"
# Domain から Infrastructure, Presentation, Usecase への依存はできない
- from: "^github.com/your-org/project/domain.*$"
to:
- "^github.com/your-org/project/infrastructure.*$"
- "^github.com/your-org/project/presentation.*$"
- "^github.com/your-org/project/usecase.*$"
# Infrastructure から Presentation への依存はできない
- from: "^github.com/your-org/project/infrastructure.*$"
to:
- "^github.com/your-org/project/presentation.*$"
基本的なルールは上記の設定で良いのですが、以下のようなケースがあると思います。
- テストファイルやモックファイルは検査対象から除外したい
- 禁止ルールの中で、例外的に一部のパッケージの依存は許容したい
- 例えば、Infrastructure から Presentation の一部のパッケージへの依存を許容する
その場合、以下のように設定を追加します。
ignorePatterns: # 追加
# テストファイルやモックファイルは検査対象から除外する
- "mock_.*.go$"
- ".*_mock.go$"
- "_test.go$"
rules:
...
# Infrastructure から Presentation への依存はできない
- from: "^github.com/your-org/project/infrastructure.*$"
to:
- "^github.com/your-org/project/presentation.*$"
allowedDependencies: # 追加
# presentation/dependable への依存は許容する
- "^github.com/your-org/project/presentation/dependable.*$"
実行する
以下のように go vet
を使って実行します。
go vet -vettool=$(which depcheck) ./...
運用時の工夫
実際の運用では、 go.mod
でバージョンを固定したいので以下のように運用しています。
-
tools/tools.go
を作成し、以下のように記述します。
//go:build tools
package tools
import (
_ "github.com/v-standard/go-depcheck"
)
- Makefile にコマンド定義する
export GOBIN=$(shell pwd)/.bin
$(GOBIN)/depcheck: go.mod
go install github.com/v-standard/go-depcheck/cmd/depcheck
.PHONEY: depcheck
depcheck: $(GOBIN)/depcheck
go vet -vettool=$(GOBIN)/depcheck ./...
-
make depcheck
で実行する
こうすることで、 go.mod
でバージョンを固定して、installされていなければinstallしたうえで実行する、という一連の処理をまとめて定義することができます。
おまけ
さくっと動作確認できるサンプルを用意したので、よかったら御覧ください。Github Actions で動かす設定まで追加してあります。
最後に
実際プロダクトのコードに使ってみると、思っていたよりも依存のミスが見つかりました。まだ運用し始めたところですが、レビュー時に意識しなくて良くなって楽になりました。
ルールを書くこと自体が、コーディング規約を明文化できるという側面もあって、エンジニア内での共通認識を持つことにも寄与するのではないかと思っています。
同じような課題をお持ちの方がいたら、ぜひ使っていただいてご意見いただけると嬉しいです。
Discussion