LoginSignup
30
23

More than 5 years have passed since last update.

[Ruby]他のクラスを拡張するいろんな方法

Last updated at Posted at 2016-04-25

Rubyで他のクラスを拡張したい場合

いろんな言語で既存のクラスを機能拡張することはあると思いますが、Rubyでももちろん存在します。
ですが、Rubyの場合比較的自由度が高いのでいろんな方法で拡張できます。

昔からある方法を使うのもいいですが、Ruby2以降で入った機能なども考慮するとなかなかパターンが合ったので自分用に整理したかったのと、きっと他の方法もあるんだろうとおもってそういうのがあれば是非教えてもらいたくて記事を書こうと思いました。
そもそもいい、わるいは別として同じ結果を取得する方式でもいろいろあって面白いのでそういう意味でも他の方の意見も聞きたいなぁと。

あと、それぞれの方法のメリット・デメリットなど一般的にいわれているものがあればそれも教えてほしいです。

さて、それでは今回拡張するクラスです。

拡張対象のクラス

m.rb
class C1
  def m(act, *arg)
    case act
    when :sym1
      p [:sym1, arg]
    when :sym2
      p [:sym2, arg]
    else
      raise StandardError.new("unknown action : #{act}")
    end
  end
end

mというメソッドが「第一引数actの値によって、いろいろ異なる振る舞いをする」として与えられていて、それを使う側がactのパターンを増やしたいという場合です。

だいぶ恣意的な感じですが、実は最近触っていたseleniumwebdriverクラスのfind_elementがこんな引数になってるんですね(実装はもちろん違いますが)

find_element(:xpath, "xpathの式"), find_element(:css, "cssのセレクタ式")みたいな感じでどんな方式でエレメントを取得するか第一引数で指定して、その実際のパスを第二引数で渡す、というような処理です。

さて、ここで、act:sym3を渡して何か処理したい場合、クラスC1を拡張したいですね。
特に今回の元ネタの処理、webdriverでいうと、実際はactsym1,sym2で事足りるんだけど、実際に使う上でのショートカットとしてsym1sym2を使って別のsym3を使えるようにしたいというケースです。

まったくC1クラスとは独立したユーティリティクラスを作る

単純ですね。どこのクラスのメソッドとかは関係なく、C1クラスのオブジェクトを受け取って目的の処理をする処理を作ればいいわけです。

def m_ext(c1_obj)
  c1_obj.find_element(:sym1, :hogehoge_arg)
end

ただし、この方法にしてしまうと、sym3の場合のみ別のクラスのメソッドになってしまうので元のC1クラスのメソッドと合わせて使うとなんか見た目がおかしくなります。

c1 = C1.new

c1.m(:sym1, :hoge)
c1.m(:sym2, :piyo)
m_ext(c1)  # <= これだけ書き方が違う

特異メソッドを定義する

上のが嫌な場合(嫌じゃなければそれでもいいんですが)、次に簡単なのは、作ったc1オブジェクトに対して特異メソッドを定義してしまうことでしょうか。

c1 = C1.new
def c1.m(act, arg)
  case act
  when :sym3
    p [:sym3, arg]
  else
    super
  end
end

c1.m(:sym1, :arg1)
c1.m(:sym3, :arg1)

特異メソッドを直接定義するんではなく、特異クラスに専用のモジュールを取り込む

特異メソッドを定義してもいいんですが、じゃぁ、この他にもいろいろ拡張したいメソッドが出てきたらそれごとにdef c1.xxxを追加していくことになりますね。それでいいことも多いですが、なんかまとまりが無いですね。拡張した部分だけをまとめて定義してc1に注入できるといい感じです。

module C1Ex
  def m(act, arg)
    case act
    when :sym3
      p [:sym3, arg]
    else
      super
    end
  end
end

c1 = C1.new
c1.extend(C1Ex)
c1.m(:sym1, :arg1)
c1.m(:sym3, :arg1)

拡張したいところがmoduleにまとまったので個人的にはさっきまでのより綺麗な気がします。

C1クラスのサブクラスを作る

c1オブジェクトだけでなく、C1クラスから作られたすべてのオブジェクトでそういう振る舞いにしたい場合(というか多くの場合そうでしょう)C1自体を拡張したいですね。というか普通のオブジェクト指向言語ならこの方式ですかね。

class MyC1 < C1
  def m(act, arg)
    case act
    when :sym3
      p [:sym3, arg]
    else
      super
    end
  end
end

MyC1.new.m(:sym1, :arg1)
MyC1.new.m(:sym3, :arg1)

C1クラス自体を変更する

他の多くの言語と異なりRubyはオープンクラスという仕組みがあり、既存のクラスを再度定義(追加定義?)することができます。
その方法を使えば、拡張というよりもそもそも元のクラスを書き換えられます。いわゆるモンキーパッチというやつですね。

class C1
  def m(act, arg)
    case act
    when :sym1
      p [:sym1, arg]
    when :sym2
      p [:sym2, arg]
    when :sym3
      p [:sym3, arg]
    else
      raise StandardError.new("unknown action : #{act}")
    end
  end
end

c1 = C1.new
c1.m(:sym1, :arg1)
c1.m(:sym3, :arg1)

これやれば何でもありですね。ですが、もともとのC1#mメソッドが複雑だったりするとなかなか大変ですし、C1を使う他のクラスがいたりするとそこに影響もいろいろ出てきます。

c1クラス自体を変更するが、モジュールで既存メソッドを拡張する

既存のクラスに無いメソッドを既存のモジュールを使って拡張するのは昔からあるincludeでよかったんですが、
既存のクラスに有るメソッド自体を拡張するのはいろいろトリッキーなことをする必要がありました(railsalias_method_chainとかですね)

そういったことをするために、prependという処理が加えられました。これを使えば、モジュールでをprependすることで既存のメソッドを変更することができます。

module C1Ex
  def m(act, arg)
    case act
    when :sym3
      p [:sym3, arg]
    else
      super
    end
  end
end

class C1
  prepend C1Ex
end

C1.new.m(:sym1, :arg1)
C1.new.m(:sym3, :arg1)

ちなみに、これをprepend C1Exでなく、昔ながらのinclude C1Exにすると、m(:sym3, :arg1)は実行できるのですが、m(:sym1, :arg1)が動かなくなります。理由は、includeの場合C1Exが継承階層でいうと、C1より親に来るからです.

というかそうならないように継承階層に組み込もう!というのがprependということですね。

c1クラス自体を変更するが、影響範囲はrefineで抑える

モンキーパッチでもprependでもC1クラスを使うすべての箇所に影響しますが、refineを使えばその箇所を限定できます。

module C1Ex
  refine C1 do
    def m(act, arg)
      case act
      when :sym3
        p [:sym3, arg]
      else
        super
      end
    end
  end
end


using C1Ex
C1.new.m(:sym1, :arg1)
C1.new.m(:sym3, :arg1)

refineは比較的最近の機能で2.0ではトップレベルにしかかけませんでした。2.1以降はclass定義の中でも使えます。ご注意くださいませ。

class UserClass
  using C1Ex #あるクラスの中だけでC1クラスを拡張する

  def initialize
    C1.new.m(:sym1, :arg1)
    C1.new.m(:sym3, :arg1)
  end
end

UserClass.new

一律既存クラスの動きを変えたいのではなく、特定のクラス内だけ動きを変えたい場合はruby 2.1以降はこの方法が一番いいんじゃないかと思っています。

ちなみによく分かってないのですが、refine内で定義したメソッドたちの検索順序はこれを見る限り、prependのようにC1よりも先にくるんですかね??

 まとめ

いやぁ、いろいろありますね。

2016/4/27 追記

コメントでrefinementprependなどを使った時のメソッドの検索順序の資料を教えてもらいました。こんなの有るんですね。

Method Lookup


When looking up a method for an instance of class C Ruby checks:

  • If refinements are active for C, in the reverse order they were activated:

    • The prepended modules from the refinement for C
    • The refinement for C
    • The included modules from the refinement for C
  • The prepended modules of C

  • C

  • The included modules of C

  • If no method was found at any point this repeats with the superclass of C.


だそうです。こんなの日常使うプログラムで全部同時につかったら混乱してしまいそうですが、試してみます。

search_order.rb
class B
  def m
    p :B_m
  end
end

module PrependedModuleOfC
  def m
    p :PrependedModuleOfC_m
    super
  end
end

module IncludedModuleOfC
  def m
    p :IncludedModuleOfC
    super
  end
end

class C < B
  prepend PrependedModuleOfC
  include IncludedModuleOfC
  def m
    p :C_m
    super
  end
end

module IncludedModuleFromRefinementForC
  def m
    p :IncludedModuleFromRefinementForC_m
    super
  end
end

module PrependeModuleFromRefinementForC
  def m
    p :PrependeModuleFromRefinementForC_m
    super
  end
end

module RefinementForC
  refine C do
    include IncludedModuleFromRefinementForC
    prepend PrependeModuleFromRefinementForC
    def m
      p :RefinementForC_m
      super
    end
  end
end

using RefinementForC
p C.ancestors
C.new.m

実行結果

$ ruby -v search_order.rb
ruby 2.3.0p0 (2015-12-25 revision 53290) [x86_64-darwin15]
[PrependedModuleOfC, C, IncludedModuleOfC, B, Object, Kernel, BasicObject]
:PrependeModuleFromRefinementForC_m
:RefinementForC_m
:IncludedModuleFromRefinementForC_m
:PrependedModuleOfC_m
:C_m
:IncludedModuleOfC
:B_m

おぉ、資料のとおりに呼ばれてますね。
refineの中でincludeprependが使えるのは知りませんでした。
refineの中の分はacestorsには現れないんですね。

確かにこの動きを見ると

ちなみによく分かってないのですが、refine内で定義したメソッドたちの検索順序はこれを見る限り、prependのようにC1よりも先にくるんですかね??

と疑問に思っていたところは:RefinementForC_m:C_mより先に表示されているので、そうなっているようですね。

30
23
5

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
30
23