Developer's Blog

Ruby 2.1 の新機能 Refinements でクラス拡張をしてみよう

WebDevBlogTitle

こんにちは。ウェブ開発担当の清水です。

Ruby 2.0 で実験的に導入されていた Refinements が、バージョン 2.1 より正式に導入となりました。

今回はこの機能を利用してクラスの拡張をしてみたいと思います。

オープンクラス

Refinements とはクラスの拡張の範囲を限定する機能です。使用の前に、まず Ruby のオープンクラスについて説明します。

オープンクラスとは、既存クラスを再定義(再オープン)することで、メソッドの追加・上書きなどが容易にできる機能です。
この機能により組み込みクラスであっても自由に拡張できるため、使い道次第で強力な効果を発揮します。

以下の例では、 Array クラスへ bogo_sort! メソッドを追加することで、全ての Array インスタンスから呼び出せるようにしています。

class Array
  def bogo_sort!
    shuffle! until sorted?
  end

  private

  def sorted?
    each_cons(2).all? { |a, b| a <= b }
  end 
end

&#91;3, 1, 2&#93;.bogo_sort!
&#91;/ruby&#93;
<p>なお、組み込みクラスに対する拡張をモンキーパッチと呼びます。</p>
<h3 class="blue">モンキーパッチの危険性</h3>
<p>モンキーパッチは強力である反面、危険性も持ち合わせています。<br>極端な例ですが、以下の処理を見てみましょう。</p>
[ruby]
10 + 20 # => 30

class Fixnum
  def +(args)
    self * args
  end
end

10 + 20 # => 200

もしプログラム全体に適用されていたとしたら…恐ろしいですね。
モンキーパッチはその影響範囲をよく考える必要があります。

拡張範囲の限定

前置きが長くなりましたが、上記のような問題の対処法として Refinements を使ってみましょう。
以下のように、拡張したいクラスを refine の引数として定義します。

module ArrayEx
  refine Array do
    def bogo_sort!
      shuffle! until sorted?
    end

    private

    def sorted?
      each_cons(2).all? { |a, b| a <= b }
    end 
  end
end
&#91;/ruby&#93;
<p>有効にする場合は using [モジュール名] とします。</p>
[ruby]
using ArrayEx

[3, 1, 2].bogo_sort!

拡張は using を記述したファイルの記述位置以降で有効になるため、不要な箇所に影響を与える可能性は低くなります。

スコープに注意

Refinements のスコープはやや特殊です。

上記の通り using はファイルをまたがないため、記述したファイルを require や load しても拡張は有効になりません。

string_ex.rb
module StringEx
  refine String do
    def string_ex
      self + ' extension'
    end 
  end
end

using StringEx

'string'.string_ex # => "string extension"
refine_test.rb
require 'string_ex'

'string'.string_ex # => NoMethodError

include などに比べ、より限定的なスコープであることが分かります。

Ruby 2.1 ではトップレベル以外にも using を記述できます。(2.0 では例外となるため注意してください)

module HelloWorld
  refine String do
    def hello
      puts "#{self} says : Hello, world"
    end
  end
end
 
class User
  using HelloWorld
  attr_reader :user
 
  def initialize(user)
    @user = user
  end
 
  def say
    user.hello
  end
end

User.new('User1').say # => "User1 says : Hello, world"
'User2'.hello # => NoMethodError

まとめ

これまでクラスの拡張、まして影響の大きい組み込みクラスを弄るなんて恐ろしい!とお思いだった方も、Refinements で拡張範囲を限定的にしながらモンキーパッチを活用してみてはいかがでしょうか。

フェンリルのオフィシャル Twitter アカウントでは、フェンリルプロダクトの最新情報などをつぶやいています。よろしければフォローしてください!

 

Copyright © 2019 Fenrir Inc. All rights reserved.