Qiita はずっとモノリスな Rails アプリケーションとして開発し続けているのですが、今後も開発し続けやすいように packs-rails + packwerk での、一部機能の package 分割を進めています。
まだ始めたばかりの取り組みではありますが、この記事ではそのへんについて触れていこうと思います。
なんで package 分割を始めているか
Qiita はサービス開始からかれこれ13年以上運営しており、Qiita の Advent Calendar も一般開放を開始した、 2012年 から数えて 13回目と、非常に長く運営させていただいております。その過程で Qiita の機能とコードの量も増えてきました。
これにより様々な機能のためのコードが入り混じることで、全体的にどこで何が行われているのかわかりにくい、関連あるコード群を探しにくい、特定の目的の実装が散らばってしまっているなどの問題が起こっていました。
一応、ドメインに応じた module を分ける等は行っていたのですが、時たま一貫しない場所にコードが置かれてしまったり、Rails のディレクトリ構成だとどうしてもコードがあちこちに散らばってしまったり、などの難しさがありました。
app/
controllers/
aaa/
bbb/
ccc/
ddd/
eee/
...
advent_calendar/
xxx_controller.rb
yyy_controller.rb
...
graphql/
aaa/
bbb/
ccc/
ddd/
...
advent_calendar/
models/
aaa/
bbb/
ccc/
ddd/
eee/
fff/
ggg/
...
advent_calendar/
xxx.rb
yyy.rb
...
views/
...
mailers/
...
lib/
tasks/
aaa.rake
bbb.rake
...
advent_calendar.rake
...
spec/
requests/
aaa/
bbb/
ccc/
...
advent_calendar/
xxx_spec.rb
...
models/
aaa/
bbb/
ccc/
...
advent_calendar/
xxx_spec.rb
...
こうしたコードを package に分けて整頓し、コード全体の見通しを改善するために、 Qiita では package 分離を少しずつ進めています。
(↑ のディレクトリ構成を ↓ のような構成に変えている)
app/
controllers/
graphql/
models/
views/
mailers/
...
lib/
spec/
packs/
advent_calendar/
app/
controllers/
advent_calendar/
xxx_controller.rb
yyy_controller.rb
...
graphql/
advent_calendar/
...
models/
advent_calendar/
xxx.rb
yyy.rb
...
views/
mailers/
lib/
tasks/
advent_calendar.rake
...
specs/
requests/
advent_calendar/
xxx_spec.rb
...
advent_calendar/
xxx_spec.rb
.../
app/
lib/
spec/
packwerk + packs-rails による package 分割とは
Qiita で一部機能のコードを切り離すにあたって、 packwerk + packs-rails の組み合わせを利用しています。
これらのライブラリについて詳しいことは、すでにいくつかの記事などで解説されているのでそちらに譲ることにして (特に Kaigi on Rails での発表などがおすすめ: https://kaigionrails.org/2024/talks/cc-kusama/ )、得られるものをざっくり書くと、
- 以下の様に、一部のコードを packs/ 配下の package として、ディレクトリを分けて管理することが出来る
- packs-rails により、これらのディレクトリでも自動読み込みするように設定され、コード内の位置 (クラス、モジュール名) などは変更せずに切り出せる
- package 間と root package (/app/, /lib/ などのこと) 内の定数アクセスを検知、制御できる
- package A からは package B で定義したクラスやモジュールが見れない、みたいなことが擬似的に可能
あたりです。
app/
models/
controllers/
views/
graphql/
lib/
spec/
models/
requests/
graphql/
lib/
package.yml
packs/
package_a/
app/
models/
controllers/
views/
lib/
spec/
package.yml
package_b/
app/
models/
controllers/
views/
lib/
spec/
package.yml
(余談ですが、フロントエンドなどで、機能毎のディレクトリ構成パターンだったり、import の制限 だったりの話題がありますが、それを Rails でやろうとするとこうなる、とも言えます。)
package 化する対象の選定
packs-rails + packwerk を導入するといっても、導入することの開発の進め方への影響はあります。 (マイクロサービス化するよりは遥かに手戻りはしやすいですが、そのコストが無視できる、というレベルではないため慎重に見ています。)
そこで、以下の流れで
- そもそもディレクトリを分けることの効果がありそうか検証
- 思いつく package 分離を手元で片っ端から試し、どういった package の分離のやり方が移行がスムーズかつ、効果的になりそうか試す
- 調べた中で、移行で悩む点が少なく、分離の効果が大きそうなものを実際に package 分離しながら開発
そもそもまず、 packs-rails + packwerk を導入せずとも試せる方法として、エディタの拡張などで、ファイルツリーで表示するファイルを絞った状態で開発してみるというのがあります。そもそも package 化するべきか悩んでいる方には手軽な検証方法としておすすめです。
また、 2 で色々試す過程では、コードの移動等を支援する packs CLI (packs-rails をインストールするとついでに使える) と packwerk によるコード間の依存の抽出が、非常に役立ちました。 行いたい package 切り出しが、実際には切り出しが容易か、他のコードとどのぐらい結合してしまっているかなどを、ツールの支援を受けて短時間で調査することが可能でした。
こうした検証のもとに、いきなり何でもかんでも package 化するのではなく、以下の部分から package 化を始めています。
- 社内ライブラリ的な実装 (スパム対策、外部サービスとの接続、特定ライブラリ向けの設定、など)
- イベント系の実装など、開発が特定のタイミングに集中しやすいもの (Advent Calendar、年末レポート、 Engineer Festa など)
- 独立性が高い機能 (管理画面など)
明らかに切り出せそうなものが多いですが、こうしたところから始めていくことで、package 分離のメリットを享受しつつ、 分離のノウハウを蓄積して、より入り組んでいるところの整理を進められるようにするのを想定しています。
今後
現在、こうした package 分離を進めながら機能開発を行っていますが、切り出した部分に関しては、機能への集中した開発が進めやすかったり、機能としての仕様が把握しやすくなったり、 package 分離のためのロジック整理によりファットすぎるモデルやテストの分割が進んだりなど、と様々な恩恵を感じています。また、分離検討の過程で依存関係を検査したおかげで、今後の改善点もいくつか見つけられています。
今後も Qiita が開発しやすい状態にしていけるように、packwerk を上手く活用していこうと考えています。