Help us understand the problem. What is going on with this article?

delegateを使って、別クラスへの切り出し

Railsのリファクタリングの際に、クラスを細かく分ける手段として、Railsのdelegateを使ってみました。

直しづらい、クラスの長さ

プロジェクトにRubocopを入れて、ヤバそうなコードをチェックしているのですが、書式の問題はものによっては自動解決も可能ですし、長すぎるメソッドも、どうにか分けられるものです。

一方で、「長すぎるクラス」というのは、そう簡単に分割できるものではありません。クラスは最終的に1つものとして動作させることになるので、メソッドみたいに「一部切り出す」というのが、あまりスッキリしません。

Moduleは使える、けど

もちろん、一部を別なモジュールに分けてincludeする、という手段はあります。

Railsのモデルではhas_manyとかvalidatesとか、DSL的なメソッドを多用しますが、これに関しては「クラスに直接書く」あるいは「Concernのincludedで回す」ぐらいの選択肢しかないので、大量のDSLが必要になる巨大なモデルの場合は「リレーションだけ1つのモジュールに書く」ようなことも、仕方なく発生してしまったりします。

とはいえ、それでは単に「ファイルを分けただけ」であって、意味論的に全く切れていない(メソッドの名前空間も共通)ので、もっとくっきり分けたいものです。

delegateとは

具体例に入る前に、delegateの説明をしておきます。

詳細はAPIdockに譲りますが、だいたい以下のような構文を取ります。

delegate :foo, :bar, to: :something

こう書くことで、foobarメソッドが、それぞれ、somethingメソッドにチェーンしたsomething.foosomething.bar相当のものとして動くようになります。他にも、to: :@instance_varとすればインスタンス変数へ、to: :CONSTANTとすれば定数につなげることができます。

特殊な処理をする部分だけ、別クラスに

一例として、ブログシステムのユーザー(User)について考えてみましょう。ユーザーには読者・編集者・査読者・管理者などいくつか権限が考えられて、それぞれにできることが違ってきます。

自然にやれば、User#can_edit?とかUser#can_delete?のような、権限に関するメソッドを作ることとなりますが、権限まわりはある程度ひとかたまりになるものですし、まとめて処理したいものです。

ということで、権限管理を集中処理するRoleManagerを作ってみましょう。まずはUserインスタンスを受け取っておきます。

app/services/role_manager.rb
class RoleManager
  def initialize(user)
    @user = user
  end
end

そして、権限判定にrole_idが必要という状況ということにして、それをRoleManagerの中でも使えるようにしておきます。

RoleManagerの中身
delegate :role_id, to: :@user

# あとの都合もあるので、private化
private :role_id

# 上の行があることで、こんな書き方ができる
def can_create?
  role_id > 200
end

def can_delete?(article)
 # 後略
end

このようにしてRoleManagerが出来上がったら、Userのほうから呼び出します。

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away