Sinatraで肥大化したファイルを分割する
Sinatraを使ったPJ*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で定義した条件は、大抵複数のサブコントローラクラスで必要となるもの。
これらはサブコントローラ間で共有できなければならない。
結論から言うと、そのような共通メソッドを集めた共通コントローラクラスを作り、サブコントローラはそれを継承すればよい。
ここで注意しなければいけないのは、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も使えないわけじゃない