packwerkを検証してみての雑感
- 研鑽Rubyプログラミング 実践的なコードのための原則とトレードオフ
- 海皇紀(1) (月刊少年マガジンコミックス) 本記事とは関係ないけど、最近読んでる漫画
packwerkのリポジトリ:https://github.com/Shopify/packwerk
packwerkはどういうgemなのか
セットアップ方法とかは公式とかをみてもらいたい。
一言でいえば、パッケージ同士の依存関係に制限ないし明示義務を課すgem。
例えば、以下のようなコードの場合、
# app/components/a1/a2/a3.rb
class A1::A2::A3
def aaa
B1::B2.new
end
end
app/components/a1/a2/package.yml
を作って以下のようにすることで、依存関係を明示できる。
enforce_dependencies: true
dependencies:
- app/components/b1
仮に、enforce_dependencies
がfalseならチェックされないし、package.yml
がなくてもチェックされない。ただ、親ディレクトリを遡ってpackage.yml
を探すので、パス上のどこにもない場合に限られる。
package.yml
がありenforce_dependencies
がtrueの状態で、dependencies
に指定のないパッケージを参照した場合にbin/packwerk check
コマンドを実行すると以下のような感じでエラーになる。
........E...................
📦 Finished in 1.0 seconds
app/components/c/y/hoge.rb:6:8
Dependency violation: ::B::Z belongs to '.', but 'app/components/c/y' does not specify a dependency on '.'.
Are we missing an abstraction?
Is the code making the reference, and the referenced constant, in the right packages?
Inference details: this is a reference to ::B::Z which seems to be defined in app/components/b/z.rb.
packwerkが解決すること、解決しないこと
繰り返すが、packwerkはパッケージ同士の依存関係に制約ないし明示義務を課す。
また、あるパッケージが、他のどのパッケージに依存しているかの明示義務を課すが、あるパッケージが、他のどのパッケージから依存されているかについてはなんら関知していない。
ただ、依存関係を図示するツールはあるようなので、それを見れば誰から依存されているのかはわかるようではある。が、そういったツールについてはrails-erd
gemを思い出せば、何ら意味のない図に成り果てる未来が想像できるので、個人的には期待していない。
そして、三度目だがpackwerkはパッケージ同士の依存関係に対するもので、クラスに対する依存関係に関しては何ら関知しない。
したがって、一見目を引くPackwerk usageにあるpackageの依存関係に関する図(2023/12/04時点のusage)に対して何ら解決策を提供していない。つまり「ちゃんとパッケージに公開APIを用意して疎結合にできるよね? 僕たちはpackwerkを提供することで公開APIを使った結合を管理しやすくしたよ!」ってこと。少なくとも、自分は動作確認して上記usageを読んでそう理解した。
すみませんが、自分たちは利口じゃないので…
明確に言えるのは、そんなパッケージを綺麗に切ることなんてできないってこと。原義の技術的負債の通り、そのタイミングで最良と思っても結局後からみると不恰好になる。そうなった時に、リファクタリングというか、再構築レベルの書き換えが必要になるわけだけど、そんなことする暇あるのかっていう。最初は疎結合だったものが、あれも必要これも必要と密結合になって、パッケージの分かれた泥団子になるだけってのがオチじゃないだろうか。
自分は、パッケージ同士が依存関係を持つのは、サービス同士がREST APIなどで繋がっているのと同じだと理解している。相手のAPIのことをよく理解している必要があり、なおかつ同期的な依存関係になる。依存先の問題は依存元も被ることになる。
なので、パッケージ同士の直接の依存関係は避けるべきで、パッケージを跨いで参照できるユースケースかコントローラーでその接続を担うべきだと考えている。これは、ユースケースやコントローラーがメッセージをバイパスする形であり、Kafkaとかのメッセージブローカーを介したサービス同士の接続と同じ形になると理解している。
つまり、メッセージブローカーを介したマイクロサービス化を視野に入れているなら、パッケージ同士の依存関係を持つのは避けたほうがいいということだ。
その上でpackwerkの使い所
- 境界づけられたコンテキストやDDDにおけるモジュールなどが枯れて安定している
- モジュラモノリスのままでマイクロサービス化は考えていない
- マイクロサービス化したとしてもREST APIなど直接的な依存関係になることが仕様的に確定している
- チームメンバのスキルが高く、パッケージの公開APIの設計やその重要性について理解し実践できる
といった状況が揃っているなら有効な気はする。特に一番目と四番目が満たされていないなら、張子の虎にしかならないだろう。
じゃあどうすればいいのよ!
対応は二択で以下の二つ(プロダクト全体で排他的選択肢ではない。レイヤが異なるので同じパッケージに対して両方適用することは普通にある)
- ユースケースやコントローラーで接続する
- 依存したいものを親のモジュールに定義する
前者はすでに書いている通りなので省略するとして、後者はデータモデリング - 要求分析駆動設計でも少し書いているが、雑な例をあげると以下の通りである。
# ※極めて雑な例
# 商品の値段を表現するのに 経理/money.rb を使いたい場合
app/domains/ECサイト/経理/money.rb
app/domains/ECサイト/商品管理/商品.rb
# こうする
app/domains/ECサイト/money.rb
app/domains/ECサイト/経理/... # 他のファイル
app/domains/ECサイト/商品管理/商品.rb
こうすることで、money.rbというファイルがECサイトモジュール内で広範囲に依存されていることがわかりやすくなる。
また、別ルールとして、依存できる(この例なら商品->money
)のは、同じモジュールか親モジュールに限る(つまり、モジュールを下ることは禁止)というルールを適用する。
このルールを適用することでmoney.rbというファイルはECサイトモジュール内だけしか依存されていないというのが確約される。
締め
自分はパッケージ同士の依存は避けるべき派閥なので、最初から別方向をみている感じで、完全にnot for meだった。
ただ、「じゃあどうすればいいのよ!」で書いた後者を厳守させるためにpackwerkは利用できないこともないというのはわかった。まあ、個人的には使わんけど。