Packwerk でスパゲッティな Rails のコードをソーメンぐらいにさっぱりさせる

9 min読了の目安(約5700字TECH技術記事

はじめに

Shopify から面白そうな gem がオープンソースとして公開されました。

Shopify/packwerk: Good things come in small packages

ドキュメントを読みながら動かしたところ、結構便利に思えたので紹介します。

課題と Packwerk による解決方法

理想的なコードの条件として、一般的に高凝集 (high cohesion) と疎結合 (loose coupling) が挙げられます。

こちらは Packwerk のリポジトリにあったで、視覚的にとても分かりやすいと思ったので転載しました。

(※ここでの package とは、オートロードされたコードを含むフォルダー)

a) は package 内が high cohesion (高凝集) で、package 間は public な api を使って loose coupling (疎結合) を実現しています。

反対に b) は package 内が low cohesion (低凝集) で、package 間のやりとりがバラバラに行われてしまっています。これだとコードの理解が困難になったり、1つの修正が様々な箇所に波及してしまい、保守や再利用が難しいコードになる可能性が非常に高くなります。いわゆるスパゲッティコードですね。

そして b) の状態のまま巨大なコードベースとなった場合の影響は顕著で、提供しているサービスのスケール速度を落としかねません。

そこで、Shopify では high hoheision と low coupling にはコードの境界を確立させる必要があると考えて、様々な検討を行います。その結果、dependencyprivacy の2つの観点で静的にコードをチェックする Packwerk を作成して、この問題に立ち向かう事にしたようです。

コードレビューだと見落としがあったり、既存のコードに対する検知が漏れてしまいがちなので、このようにツールで機械的に検知して強制するのは望ましいアプローチだと思います。

Packwerk の使い方

インストール

まず、Gemfile に次の内容を追記します。

gem 'packwerk'

次にインストールを行います。

$ bundle install

初回の実行前に設定ファイルを生成します。

$ bundle exec packwerk init

インストールはこれだけです。

実行

まずは package の設定を行います。

先ほどの packwerk init でルートディレクトリに package.yml が作成されています。package.yml が package を定義するファイルになります。

# This file represents the root package of the application
# Please validate the configuration using `bin/packwerk validate` (for Rails applications) or running the auto generated
# test case (for non-Rails projects). You can then use `packwerk check` to check your code.

# Turn on dependency checks for this package
enforce_dependencies: true

# Turn on privacy checks for this package
# enforcing privacy is often not useful for the root package, because it would require defining a public interface
# for something that should only be a thin wrapper in the first place.
# We recommend enabling this for any new packages you create to aid with encapsulation.
enforce_privacy: false

# A list of this package's dependencies
# Note that packages in this list require their own `package.yml` file
# dependencies:
# - "packages/billing"

デフォルトの上記の状態だと、privacy チェックを行わないので必要に応じて enforce_privacy: true に変更します。その後、チェックを実行します。

$ bundle exec packwerk check

チェックの結果、違反がなければ次のような出力になります。

$ bundle exec packwerk check
📦 Packwerk is inspecting 34 files
..................................
No offenses detected 🎉

📦 Finished in 0.23 seconds

違反があると次のようなエラーが出力されます。

% bundle exec packwerk check
📦 Packwerk is inspecting 34 files
...E..............................
/Users/katsuhiko.yoshida/sandbox/packwerk-sample/app/models/building/house.rb:5:17
Dependency violation: ::Vehicle::Truck belongs to 'app/models/vehicle', but 'app/models/building' does not specify a dependency on 'app/models/vehicle'.
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 ::Vehicle::Truck which seems to be defined in app/models/vehicle/truck.rb.
To receive help interpreting or resolving this error message, see: https://github.com/Shopify/packwerk/blob/main/TROUBLESHOOT.md#Troubleshooting-violations

1 offense detected

📦 Finished in 0.25 seconds

エラーメッセージから Dependency の違反があった事が分かります。具体的には、::Vehicle::Truckapp/models/vehicle package に存在し、 app/models/building から参照していますが、その依存関係が package.yml に宣言されていません。

また、特定の package のみチェックすることも可能です。次は app/model/vehicle をチェックする例です。

$ bundle exec packwerk check app/models/vehicle

既存コードへの適用

すでに存在するコードに対して packwerk check を行うと大量の違反が検出される事があります。その場合、次のコマンドを実行する事で、いったん現状の違反検知をスルーする設定ファイルを作成することが出来ます。

$ bundle exec packwerk update

次は実行した結果で、違反があっても検知はされません。

$ bundle exec packwerk update
📦 Packwerk is inspecting 34 files
..................................
No offenses detected 🎉

📦 Finished in 0.23 seconds

コマンドの実行が成功すると package.yml と同じディレクトリに deprecated_references.yml が自動で作成されます。次は deprecated_references.yml の内容です。

# This file contains a list of dependencies that are not part of the long term plan for app/models/building.
# We should generally work to reduce this list, but not at the expense of actually getting work done.
#
# You can regenerate this file using the following command:
#
# bundle exec packwerk update app/models/building
---
app/models/vehicle:
  "::Vehicle::Truck":
    violations:
    - dependency
    - privacy
    files:
    - app/models/building/house.rb

上記の違反はスルーされますが、もし新たに別の違反が行われると packwerk check を実行した時に検知されます。

これによって、新しい違反が作られないようにしつつ、既存の違反を解消するアプローチを取る事ができます。チーム開発や違反の解消を緩やかに行いたいケースでうれしいですね。

違反があった時はどう対応するか

次のいずれかになると思います。

  • 参照およびプライバシーの違反を根本的に解消する
  • package.yml に依存関係を宣言する
  • 一旦deprecated_references.yml に追記してやりすごしつつ、あとで解消する

CI

Packwerk は CLI による静的チェックツールなので CI に組み込んで、違反を検出するのも良さそうです。

Shopify による Webinar あります

2020/09/30 1:00 pm (EST) より Shopify 主催の webinar が行われます。日本時間だと2020/10/01 (木) 03:00 am と早朝なので、自分はちょっと参加が難しそうではあります。

Packwerk のデモや Shopify で Packwerk がどの様に使われているか、などの話を聞くことが出来るようなので興味のある方は登録して参加してみてください。

ShipIt! Presents: Packwerk by Shopify

参考