Railsの薄いコントローラー

More than 5 years have passed since last update.


はじめに

まず最初に、コードを見てほしい

これは Ruby on Railsで書かれた薄いコントローラーの例である。

※ 薄いコントローラー = ロジックをモデルに押し出しつけ、自分はほとんどコードを持たないコントローラー

module Api

class MaterialsController < BaseController
def index
render json: handler.find_all
end
def show
render json: handler.find_one(params[:id])
end
# ...
def update
render json: handler.update(params[:id], params[:material] || {})
end

private
def handler
MaterialHandler.new(current_user)
end
end
end

一見すると、「このコード薄いけど何か意味があるの?」「やり過ぎだろ」と思うかもしれない。

しかしこれが私の悩んだ末の結論であり、現在のプロジェクトで実際に動いてるコードである。


解説

先にMaterialHandlerが何をするクラスなのか説明しておこう。

このクラスはcurrent_userが素材を閲覧・作成する機能を提供している。具体的にはMaterialから複雑なリレーションをたどってcurrent_user.idで絞り込む、作成時にcreated_byにユーザIDを突っ込む、などの処理を行っている。

ではXxxHandlerというクラスはどのような役割を果たすのだろうか。

雑な言い方をしてしまうとHandlerは「ついついControllerにかいてしまいがちなロジックの受け皿になるクラス」である。

このようなクラスを用意した理由を綺麗に説明するのは難しいのだが


  • ControllerはHTTPに専念してほしい

  • Controllerはテストしづらい

  • Controllerは再利用できない

  • Modelが肥大化するのは嫌だ

  • あるModelに複数のモデルにまたがった感心事を書くのはなにか違う気がする

といったところだ。


ControllerはHTTPに専念

HandlerはRubyのオブジェクトを返す。ControllerはそれをJSONにしクライアントに返す。

Handlerが例外を投げた場合、Controllerはそれに応じたHTTPステータスを返す。(ちなみにそれはBaseController側でやっている)


Controllerはテストしづらい

RailsのControllerのテストを書いてみればわかるが、実際の感心事に入る前に、ログインやget :actionやJSONパースなどが入り、正直テストが読みづらい。テストはただでさえ冗長なコードになりやすいので、回避できるのならすべきである


Controllerは再利用できない

現在のプロジェクトでは、素材(Material)を複数つかってコンテンツを作成できる。このAPIを提供するのがContentsControllerだ。

class Api::ContentsController < Api::BaseController

def add_material
render json: handler.add_material(params[:id], params[:material_id])
end
end

ContentsController#add_material では、あるユーザがあるコンテンツを参照してよいかのチェックに加え、素材の権限チェックも行う必要がある。(でないと本来見れないはずの素材をコンテンツに追加できてしまうので)

もし、権限チェックのロジックをMaterialsControllerに書いたとすると、ContentsControllerからMaterialsControllerのメソッドを呼ぶことになる。やってみればわかるが、Controllerのアクションは paramsから情報を得る前提で書かれているので、再利用が極めて難しい。

ContentHandlerからMaterialHandlerを再利用するのは極めて容易だ。


Modelが肥大化するのは嫌だ

Railsでロジックをモデルに押し付けていくと、当然モデルは肥大化していく。1万を超えるRailsプロジェクトだと余裕で数百行の巨大モデルができてしまう。

なんらかのルールを決めてロジックは複数ファイルに分割する必要がある。このときルールを曖昧にするとRailsのCoC(設定より規約)つまり「〜が知りたいときは〜を見ればいいのか」という勘が働かなくなってくる。


あるModelに複数のモデルにまたがった感心事を書くのはなにか違う気がする

あるユーザがある素材を閲覧できるか確認する、このロジックはUserクラスに書くべきか、Materialクラスに書くべきか。この問題はOOPで常に起こる問題である。

経験則上、これはUserでもMaterialでもない新たなクラスを利用するのが良かった。今回はURLに合わせて(つまりController名を合わせて)MaterialHandlerにロジックを書くようにしている。

今回のプロジェクトでは、感心事がMaterialで閉じる場合はMaterialクラスに、他のクラスと協調して動作する場合はMaterialHandlerに書く、というルールで書いている。


Handlerという語

実際のところHandlerよりManagerのほうが良いように思っている。もっと良い名前があるのかもしれない。

この名前は補完ベースで考えており、cd a[TAB]ha[TAB]cd app/handlers/ まで補完されて欲しかった。managersにするとmails と競合しタイプ数が増えるので避けた。


おわりに

Railsは大変良いWebフレームワークだが、どんなフレームワークを利用してもコードの規模が大きくなるにつれて保守しにくいコードになる。

今回例示したHandlerをそのまま使う必要は無いが、あなたのコードを整理する何かの手助けになれば幸いである。


おまけ

ちなみに記事中ではいろいろカッコつけてるが、Handlerへの移行が悩んだ末の結果であるため、現在のプロジェクトではAPI部分こそ概ねHandlerだが、一部はModuleに書いてリフレクションとincludeで頑張り、一部はControllerにベタ書きする、といった残念なコードになっている。

時間と工数の兼ね合いもあるので、ゆるしてほしい。