酒と泪とRubyとRailsと

Ruby on Rails と Objective-C は酒の肴です!

メタプログラミング Ruby 第2版 / 第6章 コードを記述するコード[勉強メモ]

メタプログラミングRubyを読んでいます。完全自分用のメモ記事です。 第5章の「クラス定義」で特に参考になった部分を中心に書いています。


6章 クラス定義

オブジェクトの汚染

外部から来た安全ではないをブジェクを「オブジェクトの汚染」という。 この汚染を判定するメソッドが#tainted?である。

1
2
3
4
5
6
7
8
9
10
user_input = "User input: #{gets()}"
puts "user_input.tainted? => #{ user_input.tainted? }"

only_code_object = 1
puts "only_code_object.tainted? => #{ only_code_object.tainted? }"

# ruby 6.2.4.tainted_code.rb
# <= 1
# user_input.tainted? => true
# only_code_object.tainted? => false

ERB内のRubyコードの評価メソッド

ERBの中でRubyのコードを書くとそのコードがevalで評価される。

1
2
3
4
5
6
7
8
9
10
11
12
class ERB
  def result(b=new_toplevel)
    if @safe_level
      proc {
        $SAFE = @safe_level
        eval(@src, b, (@filename || '(erb)'), 0)
      }.call
    else
      eval(@scr, b, (@filename || '(erb)'), 0)
    end
  end
end

ユーザーが@safe_levelを設定していれば、サンドボックスの中で、コードを評価する。 また、$SAFEはprocの中だけで有効になっており、全体の設定を変更しないようにしている。

フックメソッド

Rubyにはいくつかのイベントが発生した時にフックするメソッドが存在する。

継承にフック

1
2
3
4
5
6
7
8
class String
  def self.inherited(subclass)
    puts "#{self}#{subclass} に継承されたよ!"
  end
end

class MyString < String; end
#=> "String は MyString に継承されたよ!"

include にフック

1
2
3
4
5
6
7
8
9
10
module M1
  def self.included(othermod)
    puts "#{self}#{othermod} にincludeされたよ!"
  end
end

class C
  include M1
end
#=> M1 は C にincludeされたよ!

prepend にフック

1
2
3
4
5
6
7
8
9
10
11
module M2
  def self.prepended(othermod)
    puts "#{self}#{othermod} にprependされたよ!"
  end
end

class C
  prepend M2
end

#=> M2 は C にprependされたよ!

その他以下のようなメソッドもある。

Module#method_added - メソッドを追加した時に呼ばれる
Module#method_removed - メソッドがModule#remove_method により削除された時に呼ばれる
Module#method_undefined - メソッドがModule#undef_method によって削除されるか、 undef 文により未定義になったら呼ばれる

# 特異メソッドのイベントをキャッチする
Kernel#singleton_method_added - 特異メソッドが追加された時に呼ばれる
Karnel#singleton_method_removed - 特異メソッドが削除された時に呼ばれる
Karnel#singleton_method_undefined - 特異メソッドがundefinedになった時に呼ばれる

アトリビュートのチェック

全Classで attr_checked を使えるようにする

classやmoduleのアトリビュートをチェックするようなDSL attr_checkedを追加する例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
require 'test/unit'

class Class
  def attr_checked(attribute, &validation)
    define_method "#{attribute}=" do |value|
      raise 'Invalid attribute' if value.nil? || !value || !validation.call(value)
      instance_variable_set("@#{attribute}", value)
    end

    define_method attribute do
      instance_variable_get("@#{attribute}")
    end
  end
end

class Person
  attr_checked :age do |v|
    v >= 18
  end
end

class TestCheckedAttribute < Test::Unit::TestCase
  def setup
    @bob = Person.new
  end

  def test_accepts_valid_values
    @bob.age = 18
    assert_equal 18, @bob.age
  end

  def test_refuses_invalid_values
    assert_raises RuntimeError, 'Invalid attribute' do
      @bob.age = 17
    end
  end

  def test_refuses_nil_values
    assert_raises RuntimeError, 'Invalid attribute' do
      @bob.age = nil
    end
  end

  def test_refuses_false_values
    assert_raises RuntimeError, 'Invalid attribute' do
      @bob.age = false
    end
  end
end

なるほど、これでできるのかとちょっと感激!

include した時だけ使えるようにする

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
require 'test/unit'

module CheckedAttributes
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def attr_checked(attribute, &validation)
      define_method "#{attribute}=" do |value|
        raise 'Invalid attribute' if value.nil? || !value || !validation.call(value)
        instance_variable_set("@#{attribute}", value)
      end

      define_method attribute do
        instance_variable_get("@#{attribute}")
      end
    end
  end
end

class Person
  include CheckedAttributes

  attr_checked :age do |v|
    v >= 18
  end
end

class TestCheckedAttribute < Test::Unit::TestCase
  def setup
    @bob = Person.new
  end

  def test_accepts_valid_values
    @bob.age = 18
    assert_equal 18, @bob.age
  end

  def test_refuses_invalid_values
    assert_raises RuntimeError, 'Invalid attribute' do
      @bob.age = 17
    end
  end

  def test_refuses_nil_values
    assert_raises RuntimeError, 'Invalid attribute' do
      @bob.age = nil
    end
  end

  def test_refuses_false_values
    assert_raises RuntimeError, 'Invalid attribute' do
      @bob.age = false
    end
  end
end

サンプルソース

何かの役に立つこともあるかもなので、リポジトリも公開しておきます。

morizyun/meta_programming_ruby2 - GitHub


おすすめの書籍