🦕

packwerkに入門してみた

2023/06/11に公開

はじめに

packwerkは、Shopifyがオープンソースで提供しているモジュラモノリスを支援するgemです。

https://github.com/Shopify/packwerk

Shpifyがモジュラモノリスに移行した理由やpackwerkが作成された背景については、下記の記事で紹介されています。

https://shopify.engineering/enforcing-modularity-rails-apps-packwerk

packwerkのUSAGE.mdを読むだけでは中々理解が進まなかったのですが、下記の「Gradual Modularization for Ruby and Rails」という本が非常に参考になりました。

https://leanpub.com/package-based-rails-applications

この記事では、packwerkの基本的な使い方について確認します。
簡易なサンプルアプリケーションをモジュール化する、という流れでイメージを掴んでいきたいと思います。

この記事で作成したコードは下記のリポジトリで確認することができます。

https://github.com/kondo97/sample_packwerk

事前準備

scaffoldを用いて、簡易なサンプルアプリケーションを作成します。

rails new sample_packwerk

rails g scaffold User name:string room_id:integer

rails g scaffold Room name:string user_id:integer

rails db:migrate

以下が作成されたアプリケーションのディレクトリ構成です。

→ tree
.
├── assets
│   ├── config
│   │   └── manifest.js
│   ├── images
│   └── stylesheets
│       ├── application.css
│       ├── rooms.scss
│       ├── scaffolds.scss
│       └── users.scss
├── channels
│   └── application_cable
│       ├── channel.rb
│       └── connection.rb
├── controllers
│   ├── application_controller.rb
│   ├── concerns
│   ├── rooms_controller.rb
│   └── users_controller.rb
├── helpers
│   ├── application_helper.rb
│   ├── rooms_helper.rb
│   └── users_helper.rb
├── javascript
│   ├── channels
│   │   ├── consumer.js
│   │   └── index.js
│   └── packs
│       └── application.js
├── jobs
│   └── application_job.rb
├── mailers
│   └── application_mailer.rb
├── models
│   ├── application_record.rb
│   ├── concerns
│   ├── room.rb
│   └── user.rb
└── views
    ├── layouts
    │   ├── application.html.erb
    │   ├── mailer.html.erb
    │   └── mailer.text.erb
    ├── rooms
    │   ├── _form.html.erb
    │   ├── _room.json.jbuilder
    │   ├── edit.html.erb
    │   ├── index.html.erb
    │   ├── index.json.jbuilder
    │   ├── new.html.erb
    │   ├── show.html.erb
    │   └── show.json.jbuilder
    └── users
        ├── _form.html.erb
        ├── _user.json.jbuilder
        ├── edit.html.erb
        ├── index.html.erb
        ├── index.json.jbuilder
        ├── new.html.erb
        ├── show.html.erb
        └── show.json.jbuilder

上記の構成をpackwerkを用いて、モジュール化していきます。

packwerkの導入

以下の手順でpackwerkを導入します。

# Gemfile
gem 'packwerk' # versionは、3.0.1を使用
bundle install

bundle exec packwerk init

パッケージの作成

ディレクトリ構成を変更します。
新たにpackageというディレクトリを作成し、機能単位でファイルをまとめることにします。
各パッケージ(rails_shims、rooms、users)には、package.ymlというファイルを配置することで、それらはパッケージと定義されます。

→ tree
.
├── assets
│   ├── config
│   │   └── manifest.js
│   ├── images
│   └── stylesheets
│       ├── application.css
│       ├── rooms.scss
│       ├── scaffolds.scss
│       └── users.scss
├── javascript
│   ├── channels
│   │   ├── consumer.js
│   │   └── index.js
│   └── packs
│       └── application.js
├── packages
│   ├── rails_shims
│   │   ├── channels
│   │   │   └── application_cable
│   │   │       ├── channel.rb
│   │   │       └── connection.rb
│   │   ├── controllers
│   │   │   ├── application_controller.rb
│   │   │   └── concerns
│   │   ├── helpers
│   │   │   ├── application_helper.rb
│   │   │   ├── rooms_helper.rb
│   │   │   └── users_helper.rb
│   │   ├── jobs
│   │   │   └── application_job.rb
│   │   ├── mailers
│   │   │   └── application_mailer.rb
│   │   ├── models
│   │   │   ├── application_record.rb
│   │   │   └── concerns
│   │   └── package.yml 👈
│   ├── rooms
│   │   ├── controllers
│   │   │   └── rooms_controller.rb
│   │   ├── models
│   │   │   └── room.rb
│   │   ├── package.yml 👈
│   │   └── views
│   │       └── rooms
│   │           ├── _form.html.erb
│   │           ├── _room.json.jbuilder
│   │           ├── edit.html.erb
│   │           ├── index.html.erb
│   │           ├── index.json.jbuilder
│   │           ├── new.html.erb
│   │           ├── show.html.erb
│   │           └── show.json.jbuilder
│   └── users
│       ├── controllers
│       │   └── users_controller.rb
│       ├── models
│       │   └── user.rb
│       ├── package.yml 👈
│       └── views
│           └── users
│               ├── _form.html.erb
│               ├── _user.json.jbuilder
│               ├── edit.html.erb
│               ├── index.html.erb
│               ├── index.json.jbuilder
│               ├── new.html.erb
│               ├── show.html.erb
│               └── show.json.jbuilder
└── views
    └── layouts
        ├── application.html.erb
        ├── mailer.html.erb
        └── mailer.text.erb

package.ymlは、下記のように記述します。

# package.yml
enforce_dependencies: true

ディレクトリ構成を変更したので、各ファイルの読み込みパスを変更する必要があります。

# application.rb
module SamplePackwerk
  class Application < Rails::Application
# - 
    config.paths.add 'app/packages', glob: '*/{*,*/concerns}', eager_load: true # 追記
  end
end
# application_controller.rb
class ApplicationController < ActionController::Base
  append_view_path(Dir.glob(Rails.root.join('app/packages/*/views'))) # 追記
end

違反を記録する

packwerkを用いることで、他のパッケージへの依存をチェックすることができます。
検証のため、以下のようにusersroomsに依存するように処理を追加しておきます。

class UsersController < ApplicationController
# - 
  def index
    @users = User.all
    @room = Room.new # 依存を発生させるために追記。この処理自体に特に意味はない。
  end
# - 
end

以下のコマンドを実行します。

bundle exec packwerk update

すると、各パッケージにpackage_todo.ymlが生成されます。package_todo.ymlには、そのパッケージの依存関係が示されます。

# app/packages/users/package_todo.yml
app/packages/rails_shims:
  "::ApplicationController":
    violations:
    - dependency
    files:
    - app/packages/users/controllers/users_controller.rb
  "::ApplicationRecord":
    violations:
    - dependency
    files:
    - app/packages/users/models/user.rb
app/packages/rooms:
  "::Room":
    violations:
    - dependency
    files:
    - app/packages/users/controllers/users_controller.rb

package_todo.ymlで示された依存関係は、大きく次のようなアプローチを取ることになります。

  1. 違反を記録する
  2. 違反を排除する

RESOLVING_VIOLATIONS.md

まずは違反を記録するというアプローチを確認します。
package.ymlを次のように変更します。

# app/packages/users/package.yml
dependencies:
- app/packages/rails_shims
- app/packages/rooms # 依存しているパッケージを記述する

再度bundle exec packwerk updateを実行すると、全ての依存が解決された(明示された)ので、package_todo.ymlが削除されました。
packwerkでパッケージの依存をチェックして、それを明示するのが1つ目のアプローチです。

依存関係の視覚化

packwerkのパッケージの依存関係をビジュアル化することもできます。
ここではpockyというgemを用いることにしました。

https://github.com/mquan/pocky

rake pocky:generate"[app/packages]"を実行すると、packwerk.pngが生成されました。

モジュール化を進めるにあたっては、こうしたビジュル化を行うgemも便利なツールになりそうです。

違反を排除する

違反の記録ではなく、リファクタリングを行うことで実際に依存関係自体を解消するアプローチには様々な方法があると思います。
冒頭に紹介した「Gradual Modularization for Ruby and Rails」には、例えば以下のような手法などが紹介されています。

  • Merge the two packages
  • Split the violated package
  • Move the code between packages
  • Duplicate the functionality
  • Abstract away the dependency
  • Dependency injection
  • Dependency Location - The service locator pattern
  • Emit and listen to events

この記事の中で全てを扱うのは難しいので、1つだけその手法を確認したいと思います。

検証のため、room_uiパッケージを追加して、roomsパッケージから参照する(依存する)ことにします。

# app/packages/room_ui
→ tree
.
├── models
│   └── room_ui.rb
└── package.yml
# app/packages/rooms/rooms_controller.rb
class RoomsController < ApplicationController
# - 
  def index
    @rooms = Room.all
    @room_ui = RoomUi.new # 依存を発生させるために追記。この処理自体に特に意味はない。
  end
end
# -

bundle exec packwerk updateを実行すると、roomsパッケージに違反が見つかりました。

# app/packages/rooms/package_todo.yml
app/packages/room_ui:
  "::RoomUi":
    violations:
    - dependency
    files:
    - app/packages/rooms/controllers/rooms_controller.rb

この違反をDependency injection(依存性の注入)によって解決していきます。
roomパッケージにService層を導入します。

# app/packages/rooms/services/room_service.rb
module RoomService
  def self.configure(room_ui)
    @room_ui = room_ui
    freeze
  end

  def self.room_ui
    @room_ui
  end
end

そして、RoomUiを初期化時にインスタンス化するために、config/initializers/configure_room_ui.rbを作成します。

# config/initializers/configure_room_ui.rb
Rails.application.config.to_prepare do
  RoomService.configure(RoomUi.new)
end

RoomsControllerを変更します。

class RoomsController < ApplicationController
# -
  def index
    @rooms = Room.all
    @room_ui = RoomService.room_ui
  end
end
# -

再度bundle exec packwerk updateを実行すると、roomsパッケージの違反が解消されていることが確認できました。
このようなリファクタリングを行うことで、違反自体を解消することが可能になります。

拡張機能

この記事で使用しているpackwerkのバージョンは3.0.1です。
2.x から 3.0へのアップグレード時にいくつか大きな変更がありました。

UPGRADING.md

「Gradual Modularization for Ruby and Rails」は、2.x系の説明になっているため、読み替えが必要になります。
例えば、違反を記録するファイル名がdeprecated_references.ymlからpackage_todo.ymlに変更されています。

「privacy checking」という機能も、packwerk-extensionsという別のgemに分離されています。

https://github.com/rubyatscale/packwerk-extensions

プライバシーチェック

以下の手順でpackwerk-extensionsを導入します。

# Gemfile
gem 'packwerk-extensions'
bundle install
# packwerk.yml
require:
  - packwerk/privacy/checker

roomsパッケージのpackage.ymlenforce_privacy: trueを追加します。

# app/packages/rooms/package_todo.yml
enforce_dependencies: true
enforce_privacy: true # 追加
dependencies:
- app/packages/rails_shims

bundle exec packwerk updateを実行すると、usersパッケージに違反が見つかりました。

# app/packages/users/package_todo.yml
app/packages/rooms:
  "::Room":
    violations:
    - privacy # プライバシーチェックに違反
    files:
    - app/packages/users/controllers/users_controller.rb

usersパッケージは、以下のようにroomsパッケージに依存しています。

# app/packages/rooms/rooms_controller.rb
class UsersController < ApplicationController
# - 
  def index
    @users = User.all
    @room = Room.new # 依存を発生させるために追記。この処理自体に特に意味はない。
  end
# - 
end

roomsパッケージは、enforce_privacy: trueの追加によって、プライベートなパッケージと定義されることになります。
依存自体はdependenciesに明記しました。一方でプライバシーチェックにおいては、roomsパッケージ自身がパブリックなインターフェースを公開するという制約が設定されます。
パッケージを公開するには、公開対象をpublicディレクトリに移動します。

.
├── controllers
│   └── rooms_controller.rb
├── package.yml
├── public
│   └── room.rb
├── services
│   └── room_service.rb
└── views
    └── rooms
        ├── _form.html.erb
        ├── _room.json.jbuilder
        ├── edit.html.erb
        ├── index.html.erb
        ├── index.json.jbuilder
        ├── new.html.erb
        ├── show.html.erb
        └── show.json.jbuilder

またpublicディレクトリを公開対象のディレクトリと定義するためにpackage.ymlに追記を行います。

# app/packages/rooms/package.yml
enforce_dependencies: true
enforce_privacy: true
public_path: public/
dependencies:
- app/packages/rails_shims

bundle exec packwerk updateを実行すると、違反が消えていることが確認できました。

おわりに

packwerkを用いたモジュール化の全体感を掴むことができました。しかし、実際の開発に導入していくとなると、パッケージをどのような単位で定義するか、依存関係を解消するためのリファクタリングなど、まだまだイメージできない部分が多くあります。
一方で業務で携わっているプロダクトにおいては、機能数も日々増えており、低凝集・密結合なコード、依存の方向性が複雑化しているなど、開発の難しさを感じる場面があります。
packwerkを用いたモジュラモノリスへの移行は、大規模なRailsアプリケーション開発で有用な手段になるのではないか、と感じているので引き続きウォッチしていきたいと思います。

GitHubで編集を提案
株式会社スタメン

Discussion