せらぴんブログ

サークル「せらぴん」のうのはな透です。やっぱり眼鏡っ娘が好き!!

Sinatraで肥大化したファイルを分割する

Sinatraを使ったPJ*1で、あまりにも一つのファイルに色々突っ込みすぎていたので、ファイル分割をメインとしたリファクタリングを実施した。
その際の備忘録。

ディレクトリ構成は長くなるので割愛。

やったこと

バッチファイル

  • cronで実行するバッチファイルがappディレクトリ直下に置かれまくっていたので、app/batchディレクトリを作ってそこに全部つっこむ
  • バッチファイル用の独立したライブラリファイルがあったので、新設するapp/src/modelsディレクトリに統合
  • バッチファイル内に独立したクラスが書かれていた💢ので、それもapp/src/modelsディレクトリに移行(バッチファイルは処理のみを書く)

モデルファイル

  • appディレクトリ内に2~3のモデルファイルが散逸していたため、app/src/modelsディレクトリを作ってそこに全部つっこむ
  • モデルファイルが肥大化(一番ひどいもので80KB!)していたため、1ファイル1クラス形式になるようファイル分割
  • 複数のサブクラスを抱えている肥大クラスがあったので、サブクラスをすべてファイル分割
  • それでも肥大だったため、さらに一部機能をモジュール化して別ファイルに退避

ヘルパー

  • app/src/helperディレクトリに移動
  • helper do~endを用いていたので、ヘルパークラス化

コントローラ

  • コントローラファイルが肥大化(合わせて45KB!)していたため、サブコントローラとしてファイル分割し、app/src/controllerディレクトリに移動

工夫した点

ディレクトリ一括読込

今回のファイル分割により、モデルファイル数が約10個→約90個まで増えてしまった。
ここまで来るといちいちrequireしていたらキリがないため、ディレクトリ以下のファイルを一括読込するような処理を組み込んだ。

def req(path)
  Dir[path + '*.rb'].each {|file| require file}
  Dir[path + '*/'].each {|dir| req(dir)}
end
req(File.dirname(__FILE__) + "/src/models")

これをトップレベルに書いておけば、/src/models配下に置いたファイルすべてをrequireできる。
この際の注意点としては、サブクラスはスーパークラスが先にrequireされていないとエラーになる点がある。
これは、サブクラスのファイル内でスーパークラスをrequireするか、あるいはサブクラスは全てスーパークラスがあるディレクトリよりも下位のディレクトリに置くことで解決できる*2

Sinatraでのサブコントローラ導入

モデル群はただファイルして一括requireすればいいが、コントローラ群はどうするのか?
これは使っているSinatraのスタイルによって変わってくるため、そこから説明する。

ラシックスタイルの場合

Sinatraの「始めよう」のソース、このようにトップレベルにルーティングをそのまま書くことをラシックスタイルという。
ラシックスタイルの場合、ファイル分割はそのままルーティングを別ファイルに分割してrequireするだけで正しく動作する。

モジュラースタイルの場合

Sinatra::Base(あるいはSinatra::Application)を継承したクラス内にルーティングを記述していくと、モジュラースタイルになる。
モジュラースタイルの場合、ファイル分割はクラシックスタイルほど容易ではない。

まず、なにも継承しないモジュールにルーティングを書くと、当たり前だが怒られる。
ルーティングを書く場所はSinatra::Baseを継承したクラスでなければならない。
ここで問題になるのは、複数のサブコントローラ(=Sinatra::Baseを継承したクラス)を作ったとして、その統合をどうするか。
モジュールでないためincludeは使えないし、rubyは多重継承をサポートしてないので継承でも解決できない。

解決策は主に2つ。
1つは、本家の「Sinatraミドルウェアとしての利用」という章にある通り
メインクラスで使いたいサブコントローラをuseするというもの。
もう1つは、Rack::URLMapを利用するというものである。
studio3104.hatenablog.com

今回は

今まではコントローラファイルが2個のみだったため、クラシックスタイルで押し通していたのだが、
今後のことを考え、この期にすべてモジュラースタイルに変更している。
結合にはuse(上記1番目の方法)を使った。
また、サブコントローラもディレクトリ以下のものを一括読込するようにしている。

追記(2018/01/28)

モジュラースタイルで分割した際に問題が頻発したので、ひとまずクラシックスタイルに差し戻しました。
モジュラースタイルでの分割は今後の課題とさせていただきたく。

共通のsetをサブコントローラ間で共有する

上述のように、モジュラースタイルであっても、”一つのメインクラスと複数のサブコントローラクラス”という形でアプリケーションを分割することが可能である。
しかし、setで定義した条件は、大抵複数のサブコントローラクラスで必要となるもの。
これらはサブコントローラ間で共有できなければならない。

結論から言うと、そのような共通メソッドを集めた共通コントローラクラスを作り、サブコントローラはそれを継承すればよい。

qiita.com

ここで注意しなければいけないのは、beforeフィルタ、afterフィルタは共通コントローラに入れてはいけない。
useを使ってサブコントローラを結合している場合、1回のリクエストで複数回beforeフィルタ/afterフィルタが呼ばれうるためだ。
beforeフィルタでDBへのアクセスをしていたりすると、これが原因でDBのTimeoutが頻発する羽目に遭う(ていうか遭った)。
この場合、beforeフィルタ/afterフィルタは、メインクラスに定義するとうまくいく。
追記(2018/01/28):確かに「フィルタが複数呼ばれる」という問題はこれで解決しましたが、私の場合はキャッシュの辺りで問題が発生しました。これについては今後余力のある時に調査したいです。

「じゃあsetメソッドは複数回呼ばれないのか?」と言われると、軽く調べた範囲ではrackup直後にはサブコントローラの数だけ実行されているようだ。
なぜそういうことが起きるのかは不明。

ちなみに、Rack::URLMapを使えば上記問題は起こらない(らしい)ので、気になるのであればそっちを使うといい。

感想

  • ファイルを分割するだけだが、これだけでも若干見通しが良くなる
  • そもそも1000行以上あるファイルの管理をしたくない、というのが正直な心理(例えクラス分割自体はされていたとしても!)
  • とっつきやすいSinatraで始めていったPJだが、ここまで肥大化するとわかっていたら、Railsを使っていたかもしれない……
  • だからといって「わざわざファイル分割したらRailsと同様なことを自分で書いてるだけじゃん…」とか「それもう○○じゃん」論理で押し通してもあまり得しないので、肥大化したら素直にファイル分割した方がいいと思う
  • サブコントローラを作った場合はRack::URLMapを使うほうがよさそう。ただuseも使えないわけじゃない

*1:まぁ要するに同人秘書なんだけど

*2:reqメソッド内で、自身のディレクトリのファイルをすべて読み込んだ後で、各サブディレクトリを再帰的に処理しているため