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つのクラスで担う、というのもコードを調べる上でわかりやすくなります。