ロコガイド テックブログ

「地域のくらしを、かしこく、たのしく」する、株式会社ロコガイドの社員がいろいろな記事を書いています。

「地域のくらしを、かしこく、たのしく」する、株式会社ロコガイドの社員がいろいろな記事を書いています。

モジュラモノリスを試験運用している話

ビジネス開発部のバックエンドエンジニアの伊藤です。主にトクバイのビジネスサイドの開発を担当しています。

Shopify記事の影響もありしばらく前からモジュラモノリスが注目されるようになりました。 我々が開発しているトクバイではモノリシックに構築されており、各機能が密結合になりメンテナンスしづらくなっています。 今回、新機能プロジェクトを担当する上で各機能をモジュール分割・コンポーネント化することを試してみました。

Packwerk

PackwerkはShopifyが開発しているGemで、Railsアプリケーションをモジュール分割する手助けをしてくれます。 USAGEに書かれていることを要約すると、大規模なアプリケーションは境界を作り境界間の依存関係をコードレベルで最小にしようということだと考えています。 後述しますが幾つかのエコシステム導入する事で、よりモジュラモノリス化を促すことができます。

Packwerkを導入することで次のことができます。 - パッケージの定義 - パッケージの依存関係の管理 - 依存関係の検証

Packwerkではパッケージ化のベストプラクティスが記載されています。これについても少し触れます。

  • 機能的凝集性の高いものをパッケージ化する
  • パッケージは相互に緩い結合をする必要がある(疎結合)

結合度

※ 図はhttps://github.com/Shopify/packwerkより引用

パッケージ間の結合度を低くすることにより、モジュール・機能の依存度を下げることができそうです。 本来ならパッケージ設計の原則についてもしっかり触れたいところではありますが今回は省きます。

一緒に入れると便利なgem

packwerk-extensions

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

Packwerk単体ではパッケージの依存関係はチェックできるものの、それ以外のルールを課すことができません。 packwer-extensionsを導入することで以下のチェッカーを導入することができます。

Privacy Checker

Privacy Checkerではパッケージ内のモジュール・クラスの公開・非公開を明示的にし、依存関係を制限できます。 このチェッカーを導入した上でpublicディレクトリに配置した機能を公開し、それ以外を非公開にできます。 こうすることで不要な外部依存を検出することができます。 今回のプロジェクトでは有効にしました。

Visibility Checker

Visibility Checkerは他のパッケージのプライベート実装として定義することができます。 定義した依存関係外から直接参照があればエラーになるイメージです。 今回のプロジェクトでは利用していません。

Architecture Checker

Architecture Checkerではパッケージにアーキテクチャレイヤーを付与し、依存関係の順序を定義することができます。 Clean ArchitectureやDDDを意識するような依存チェックをしたい場合は有用そうです。 今回のプロジェクトでは利用していません。

packs-rails

https://github.com/rubyatscale/packs-rails

packs-railsを導入するとautoloadやテストなど分割した際に微妙に面倒になりそうな設定を省略することができます。 その代わり指定の構成にする必要が出てきますが大きな問題は無いかなと思います。

package.yml # root level pack
app/ # Unpackaged code
  models/
  ...
packs/
  my_domain/
    package.yml # See the packwerk docs for more info
    deprecated_references.yml # See the packwerk docs for more info
    app/
      public/ # Recommended location for public API
        my_domain.rb # creates the primary namespaces
        my_domain/
          my_subdomain.rb
      services/ # Private services
        my_domain/
          some_private_class.rb
      models/ # Private models
        some_other_non_namespaced_private_model.rb # this works too
        my_domain/
          my_private_namespaced_model.rb
      controllers/
      views/
    config/
      initializers/ # Initializers can live in packs and load as expected
    lib/
      tasks/
    spec/ # With packs-rails, specs for a pack live next to the pack
      public/
        my_domain_spec.rb
        my_domain/
          my_subdomain_spec.rb
      services/
        my_domain/
          some_private_class_spec.rb
      models/
        some_other_non_namespaced_private_model_spec.rb
        my_domain/
          my_private_namespaced_model_spec.rb
      factories/ # packs-rails will automatically load pack factories into FactoryBot
        my_domain/
          my_private_namespaced_model_factory.rb
  my_other_domain/
    ... # other packs have a similar structure
  my_other_other_domain/
    ...

各パッケージはpacks配下に設置し、各パッケージ内ではRailsアプリケーションと同様の構成を取ることができます。 前述したpackwerk-extensionsのPrivacy Checkerを導入した場合はpublicディレクトリも登場しますがそれ以外に大きな違いは無いと思います。

graphwerk

https://github.com/samuelgiles/graphwerk

パッケージの依存関係を図にすることができます。 依存関係を図にすることで直感的に機能のありかや複雑さが分かるようになります。

依存関係

※ 図はhttps://github.com/samuelgiles/graphwerkより引用

導入

紹介したGemすべてを導入方法について説明します。 尚、Railsアプリケーションであることを前提とします。

# Gemfile

gem 'packwerk'
gem 'packwerk-extensions'
gem 'packs-rails'
gem 'graphwerk', group: :development

graphwerkはGraphvizを利用します。 macOSであればbrew install graphvizでインストールしておくと良いでしょう。

コマンド実行を楽にする為にbinstubを生成してきます。

bundle binstub packwerk

packwerkの設定とパッケージ定義を生成します。

bin/packwerk init

bin/packwerk initすると以下のファイルが生成されます。

  • packwerk.yml
  • package.yml

packwer.ymlはpackwerkの設定ファイルです。packwerk-extensionsで導入したいチェッカーがあればこのファイルに追記します。 package.ymlはパッケージを宣言するファイルです。packwerkではpackage.ymlが設置されたディレクトリ配下を1つのパッケージとして扱います。 ネストしている場合は単純にパッケージ境界として認識します。なのでサブパッケージのような扱いにはなりません。

工夫していること

名前空間を切る

当然と言えば当然ですがパッケージ毎に名前空間を切っています。 公開APIに関しては{Package}::Publicと言う形で切り出しています。 呼び出す時に少し冗長でわずわらしさもありますが、内部のAPIなのか外部のAPIなのかが意識しやすくなります。

各パッケージをなるべく小さく保つ

大きくなったモジュールを小く分離する方がコストが高いという考えで、 パッケージとして切り出しすぎかも?ぐらい小さく保つことを心掛けています。

循環依存関係を持たない

循環依存を持つと全体像が把握が難しくなりがちです。依存関係をツリー状にすることで循環依存を回避するよう心掛けています。

Architecture Checkerを導入してレイヤーの階層を作っても良かったのですが、初期段階では決め切れないと判断し導入しませんでした。

テスト

RSpecを使っている場合はpacks/rails/rspecをrequireすることで各パッケージのテストをワンコマンドで実行できるようになります。 .rspecに記載しておくと便利です。

--require spec_helper --require packs/rails/rspec

モデルを共有しない

同一のテーブルを参照する場合でも必要に応じてパッケージ毎にモデルを用意するようにしました。 パッケージに閉じ込めたビジネスロジックに直接参照するとパッケージ間の依存度が強くなるからです。

DDDでCreatePostUpdatePostのようにエンティティを分けて実装する場合がありますが、この思想に似ています。

CI

パッケージの依存関係をCIで常にチェックし違反があった場合に検出できるようにしました。

感想

良かったこと

境界を意識できる

今から実装するもの・利用するものは公開APIなのか非公開APIなのかを意識するようになります。 それによって影響範囲が事前に把握しやすくなりました。 また、コメントを記述する際も特に公開APIであれば利用する際の注意点や副作用なども気にするようになりました。

他の言語では標準機能として搭載している場合もあるので良いなと思いました。

複雑度が下がる

これは依存関係をどう持たせるかによりますが、前に上げた「境界を意識できる」レベルで分解することが習慣化され、1つ1つのパッケージ・モジュールの複雑度が下げやすくなった印象があります。 各名前空間のクラス数など数えると明かに数が少なくなっているので効果が得られていそうです。

気になること

設定やルーティング

パッケージをmountable engineのように振る舞わせることができるのですが、その機能を活用してconfig配下をパッケージ毎に配置しています。 パッケージ内のコンテキストに絞って見ている内はいいのですがアプリケーション全体に影響するような設定や、どこで追加されたか分かりづらいroutes.rbが今後出てきそうです。

この辺は設定メソッドやルーティングメソッドをパッケージの公開APIとして用意し、アプリケーション側で呼び出すように変更した方が良さそうだなと思いました。

モデルの共有

モデルを共有していなことでパッケージ毎にモデルやファクトリの定義をしなくてはならず少し大変でした。 共通のバリデーションなどが出てきた場合にsharedパッケージのような微妙なパッケージが必要になりそうだなと思いました。

こういうシーンではインターフェイスのような概念がRubyにも実装されていると、パッケージ間の実装の齟齬などが発生を抑えらるなと実感しました。 型アノテーションも一緒に導入できれば良かったなと思いました。

まとめ

試験導入ということもありまだまだ勉強し工夫しなければならない段階だと思いますが、他のプロダクトにも導入したい感触がありました。 読者の皆さんも是非Packwerkでモジュラモノリスライフを始めてみてください。