LoginSignup
112
77

More than 3 years have passed since last update.

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

Last updated at Posted at 2017-12-20

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

112
77
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
112
77