packwerkを検証してみての雑感

2023-12-24
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-erdgemを思い出せば、何ら意味のない図に成り果てる未来が想像できるので、個人的には期待していない。

そして、三度目だが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は利用できないこともないというのはわかった。まあ、個人的には使わんけど。