当たり前と言えば当たり前なのですが Gem を作る時とかに使えそうなので。
普通に拡張すると何が問題なのか?
Ruby 自体は組み込みのライブラリに含まれるクラスも簡単に拡張できます。
class Array
def <<(obj)
nil
end
end
簡単に Array
の <<
メソッドを壊す事ができました。これは極端な例ですが、こうした衝突があちこちで起こるのを避けたいというのが本題です。
やり方
継承を使う
class MyArray < Array
def <<(obj)
2.times { super(obj) }
end
end
実行。
# 普通に Array の initialize が呼べる
my_array = MyArray.new(2, "hoge")
# 上書きしたメソッドを実行してみる
my_array << 1
p my_array
結果。
["hoge", "hoge", 1, 1]
もちろん元の Array クラスでは何ら変更は起こりません。
class_eval を用いたメソッド実装
class_eval
の第一引数に文字列を渡すと、その中身が直接クラス内に定義されます。
また第二引数と第三引数はスタックトレースで利用されるので、このまま書いておくとデバッグの時に便利です。
おまじないにしたくない人は公式で詳しい動作をチェックしましょう。
これの何が嬉しいかと言うと、人が作った拡張と名前が被った時などに衝突をコンフィグで回避できる点です。
# このようにコンフィグで設定したメソッド名を…
METHOD_NAME = "cut".freeze
class MyArray < Array
# class_eval で直接定義する
class_eval <<-RUBY, __FILE__, __LINE__ +1
def #{METHOD_NAME}(count = 2)
self.class.new self.slice(0, count)
end
RUBY
end
実行。
my_array = MyArray.new(10, "hoge")
p my_array
p my_array.cut
p my_array.cut(1)
結果。
["hoge", "hoge", "hoge", "hoge", "hoge", "hoge", "hoge", "hoge", "hoge", "hoge"]
["hoge", "hoge"]
["hoge"]
この場合、テストコードなどもコンフィグに合わせて実行されるように注意する必要があります(そちらは send
を使えば簡単にできると思います)。
余談
このケースでも全てのメソッド名が被る事を避ける事はできませんが、そのためにネームスペースが切られています。そこまで被ってしまうケースではどうしようもありませんが、変わった名前の Gem はその辺の被りを嫌ってネーミングしているのかもしれません。
サードパーティが自社コンテンツ向け Gem でネーミングする際にかなり一般的な名前を使っている意図など考慮すると少し面白い気がします。
実装の例
Kaminari はこの両方の方式を使っていました。というより、自分で Gem を作った方が良さそうな要件が発生したので Clean and Easy to Use を謳っている Kaminari の実装を参考にさせて頂きました。
継承を使う
元々の配列を引数にして new
できるようになっています。
class_eval
を使う
中身の実装は意外とシンプルです。