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

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

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

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

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

## 拡張対象のクラス
```rb: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のパターンを増やしたいという場合です。

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

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


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

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

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

```rb
def m_ext(c1_obj)
  c1_obj.find_element(:sym1, :hogehoge_arg)
end
```
  
ただし、この方法にしてしまうと、`sym3`の場合のみ別のクラスのメソッドになってしまうので元の`C1`クラスのメソッドと合わせて使うとなんか見た目がおかしくなります。

```rb
c1 = C1.new

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

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

```rb
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`に注入できるといい感じです。

```rb
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自体を拡張したいですね。というか普通のオブジェクト指向言語ならこの方式ですかね。

```rb
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はオープンクラスという仕組みがあり、既存のクラスを再度定義（追加定義？）することができます。
その方法を使えば、拡張というよりもそもそも元のクラスを書き換えられます。いわゆるモンキーパッチというやつですね。

```rb
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`でよかったんですが、
既存のクラスに有るメソッド自体を拡張するのはいろいろトリッキーなことをする必要がありました（`rails`の`alias_method_chain`とかですね）

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

```rb
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`を使えばその箇所を限定できます。

```rb
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`定義の中でも使えます。ご注意くださいませ。

```rb
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 追記

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

[Method Lookup](http://docs.ruby-lang.org/en/trunk/syntax/refinements_rdoc.html#label-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.

---------------------------------------

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

```rb: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
```

実行結果

```sh
$ 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`の中で`include`や`prepend`が使えるのは知りませんでした。
`refine`の中の分は`acestors`には現れないんですね。


確かにこの動きを見ると

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

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