Railsのリファクタリングの際に、クラスを細かく分ける手段として、Railsのdelegateを使ってみました。
直しづらい、クラスの長さ
プロジェクトにRubocopを入れて、ヤバそうなコードをチェックしているのですが、書式の問題はものによっては自動解決も可能ですし、長すぎるメソッドも、どうにか分けられるものです。
一方で、「長すぎるクラス」というのは、そう簡単に分割できるものではありません。クラスは最終的に1つものとして動作させることになるので、メソッドみたいに「一部切り出す」というのが、あまりスッキリしません。
Moduleは使える、けど
もちろん、一部を別なモジュールに分けてincludeする、という手段はあります。
Railsのモデルではhas_manyとかvalidatesとか、DSL的なメソッドを多用しますが、これに関しては「クラスに直接書く」あるいは「Concernのincludedで回す」ぐらいの選択肢しかないので、大量のDSLが必要になる巨大なモデルの場合は「リレーションだけ1つのモジュールに書く」ようなことも、仕方なく発生してしまったりします。
とはいえ、それでは単に「ファイルを分けただけ」であって、意味論的に全く切れていない(メソッドの名前空間も共通)ので、もっとくっきり分けたいものです。
delegateとは
具体例に入る前に、delegateの説明をしておきます。
詳細はAPIdockに譲りますが、だいたい以下のような構文を取ります。
delegate :foo, :bar, to: :something
こう書くことで、foo、barメソッドが、それぞれ、somethingメソッドにチェーンしたsomething.foo、something.bar相当のものとして動くようになります。他にも、to: :@instance_varとすればインスタンス変数へ、to: :CONSTANTとすれば定数につなげることができます。
特殊な処理をする部分だけ、別クラスに
一例として、ブログシステムのユーザー(User)について考えてみましょう。ユーザーには読者・編集者・査読者・管理者などいくつか権限が考えられて、それぞれにできることが違ってきます。
自然にやれば、User#can_edit?とかUser#can_delete?のような、権限に関するメソッドを作ることとなりますが、権限まわりはある程度ひとかたまりになるものですし、まとめて処理したいものです。
ということで、権限管理を集中処理するRoleManagerを作ってみましょう。まずはUserインスタンスを受け取っておきます。
class RoleManager
def initialize(user)
@user = user
end
end
そして、権限判定にrole_idが必要という状況ということにして、それをRoleManagerの中でも使えるようにしておきます。
delegate :role_id, to: :@user
# あとの都合もあるので、private化
private :role_id
# 上の行があることで、こんな書き方ができる
def can_create?
role_id > 200
end
def can_delete?(article)
# 後略
end
このようにしてRoleManagerが出来上がったら、Userのほうから呼び出します。
def role_manager
@role_manager ||= RoleManager.new(self)
end
delegate(*RoleManager.public_instance_methods(false), to: :role_manager)
もちろん、1つ1つ指定してdelegateするのもありですが、publicメソッドすべてをdelegateしたいのなら、Module#public_instance_methodsを使って一気に済ませてしまうこともできます(ただし、やりすぎて循環参照を起こさないようには要注意です)。あと、delegate(*としていますが、カッコは掛け算と誤認しないようにしている措置です(正しくスペースを入れれば、いちおうなしでも動きますが)。
クラスを分けるメリット
メソッドの中では全体がローカル変数のスコープとなるように、privateメソッドやインスタンス変数も、クラスやincludeしたモジュール全体がスコープとなります。別にクラスを立てれば、それらのスコープも区切れて、理解するときに一度に把握しないといけない範囲も縮むことになります。
また、1つの意味を1つのクラスで担う、というのもコードを調べる上でわかりやすくなります。