method_missing をいじる

NoMethodError というエラーがあるが、method_missingメソッドを使うと、これをコントロールできる。
試したのでメモ。

参考

NoMethodError を拾う

とりあえず、NoMethodError を起こしてみる。

NoMethodErrorを起こす
1
2
3
4
5
6
7
8
9
10
11
12
13
class MyMath
attr_reader :val

def initialize
@val = 0
end
end

math = MyMath.new

math.plus
# => undefined method `plus' for #<MyMath:0x0000557adb648308 @val=0> (NoMethodError)
# まあ当然

インスタンスに無いメソッドを呼べば、NoMethodErrorを起こせる。

NoMethodErrorを拾う
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyMath
attr_reader :val

def initialize
@val = 0
end

def method_missing(name)
puts "#{name} => method_missingを呼び出した"
end
end

math = MyMath.new

math.plus
# => plus => method_missingを呼び出した
# クラスのインスタンスが持っていないメソッドを呼び出した結果method_missingが呼ばれた

math.plus(1)
# => wrong number of arguments (given 2, expected 1) (ArgumentError)
# 引数の数が合わないと相変わらずエラー

method_missing を用意することで、クラスのインスタンスが持っていないメソッドが呼ばれた時に、method_missing が呼ばれるようになった。

method_missing で NoMethodError を詳細に拾う

method_missing を使って、呼び出し時の引数まで拾ってみると以下のようになる。

method_missing で NoMethodError で詳細に拾う
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyMath
attr_reader :val

def initialize
@val = 0
end

def method_missing(name, *args)
puts "#{name} => method_missingを呼び出した"
puts "args = #{args.inspect}"
end
end

math = MyMath.new

math.plus()
# => plus => method_missingを呼び出した
# args = []

math.plus(1, 2, 3, 4)
# => plus => method_missingを呼び出した
# args = [1, 2, 3, 4]

method_missing を使って処理を組み立てる

method_missing で実装されていないメソッドの呼び出しを拾うことができた。
メソッド名を処理して処理自体を組み立てることができる。

method_missing を使って処理を組み立てる
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
58
59
60
61
62
63
64
65
66
67
68
69
class MyMath
attr_reader :val

def initialize
@val = 0
end

def plus(number)
@val += number
end

def sub(number)
@val -= number
end

def method_missing(name)
# 呼び出したメソッド名が calc_ で始まらなければ super で親クラスにおまかせ
return super unless name.start_with?("calc_")

# calc_ 呼び出し時の名称を取得
options_str = name.to_s.gsub("calc_", "")

# 文字列を_で分割し、命令とパラメータを切り出す
options = options_str.split"_"

index = 0
while options.length > index do
# 命令部分、パラメータ部分が両方ない場合エラー
raise NoMethodError if options[index].nil? || options[index + 1].nil?

# 命令部分が plus か sub 以外の文字列の場合エラー
raise NoMethodError if !["plus","sub"].include?(options[index])

# パラメータ部分が数字として解釈でき無ければエラー
# 数字に変換できない文字列に.to_iを実行すると 0 になることで判断
# 特に、足し算引き算であれば 0 は考慮する必要がないだろう
raise NoMethodError if options[index + 1].to_i == 0

# 計算実行
plus(options[index + 1].to_i) if options[index] == "plus"
sub(options[index + 1].to_i) if options[index] == "sub"

index += 2
end

self
end

def respond_to_missing?(name, *args)
# calc_ から始まるメソッドか?だけを見ているざっくり仕様
return true if name.start_with?("calc_")
super
end
end

math = MyMath.new

puts math.respond_to?(:calc_calc_plus_1_plus_5_sub_2_plus_7)
# => true
# calc_calc_plus_1_plus_5_sub_2_plus_7 メソッドはある

puts math.respond_to?(:hoge)
# => false
# hoge メソッドはない

puts math.calc_plus_1_plus_5_sub_2_plus_7.val
# => 11
# 処理内容をメソッド名で渡して処理できる

calc_plus_1_plus_5_sub_2_plus_7 というように、メソッド名で詳細なパラメータを与えて処理できる。
結局のところ処理したのは、0 + 1 + 5 - 2 + 7です。

method_missing のオーバーライドをするときには、対象のメソッド名が Object#respond_to? に対して true を返すようにドキュメントに書いてある。
対応としてrespond_to_missing? で呼び出されたメソッド名が、calc_ から始まるかだけをチェックする。
(本当なら method_missing に用意した処理同様のメソッド名のチェックをするのがいいのでしょうが)


method_missing はおもしろい、しかし多用するとチームで進めるとき混乱を生まないかやや不安が残る。

method_missing の使用事例として、proxy パターンと Builder パターンがある。
Qiita - Ruby によるデザインパターンまとめ

うまく付き合いたいものです。

ではでは。