Ruby元编程笔记—— 方法

在理解了方法查找的基础上,可以在运行时创建方法、插入方法调用、把调用转发给其他对象,甚至调用一个不存在的方法。接下来的元编程技术讲解了如何实现这些操作。

本章的主要内容:

通过元编程技术使代码更加简洁,更有利于扩展和维护。

先来看一段代码,通过重构的过程,逐步了解元编程怎样发挥作用。

  1. 这是一个连接数据库并查询相关信息的data_source类。为节省时间,可直接跳到第三步重构步骤,需要时再跳回来看。
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
class DS
# attr_accessor :workstation_id
def initialize
"...connect..."
end
def get_cpu_info(workstation_id)
cpu_info = if workstation_id == 1
"intel core i7"
else workstation_id == 2
"apple AM 11"
end
end
def get_cpu_price(workstation_id)
cpu_price = if workstation_id == 1
2500
else workstation_id == 2
2000
end
end
def get_mouse_info(workstation_id)
mouse_info = if workstation_id == 1
"雷蛇"
else workstation_id == 2
"牧马人"
end
end
def get_mouse_price(workstation_id)
mouse_price = if workstation_id == 1
300
else workstation_id == 2
500
end
end
def get_keyboard_info(workstation_id)
keyboard_info = if workstation_id == 1
"机械键盘"
else workstation_id == 2
"普通键盘"
end
end
def get_keyboard_price(workstation_id)
keyboard_price = if workstation_id == 1
500
else workstation_id == 2
90
end
end
end
  1. 这是一个computer类。
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
require './data_source.rb'
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def mouse
info = @data_source.get_mouse_info(@id)
price = @data_source.get_mouse_price(@id)
result = "Mouse: #{info} #{price}"
return "#{result}" if price >= 100
end
def cpu
info = @data_source.get_cpu_info(@id)
price = @data_source.get_cpu_price(@id)
result = "Cpu: #{info} #{price}"
return "#{result}" if price >= 100
end
def keyboard
info = @data_source.get_keyboard_info(@id)
price = @data_source.get_keyboard_price(@id)
result = "Keyboard: #{info} #{price}"
return "#{result}" if price >= 100
end
end
computer = Computer.new(1,DS.new)
puts computer.mouse
puts computer.cpu
puts computer.keyboard
  1. 现在我们使用元编程的技术对Computer类增加动态派发,进行第一次重构:
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
require './data_source.rb'
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def mouse
component :mouse
end
def cpu
component :cpu
end
def keyboard
component :keyboard
end
def component(name)
info = @data_source.send "get_#{name}_info", @id
price = @data_source.send "get_#{name}_price", @id
result = "#{name.capitalize}: #{info} #{price}"
return "#{result}" if price >= 100
end
end
computer = Computer.new(2,DS.new)
puts computer.mouse
puts computer.cpu
puts computer.keyboard

首先把重复的代码提取到一个方法中,然后把mousecpukeyboard方法代理到component方法上。component方法会接着调用DS类的get_XXX_infoget_XXX_price方法。然后执行相关操作。

现在重复代码被消灭了很多,但仍然可以进一步精简。

什么是动态派发?

在代码运行的最后一刻再决定调用哪个方法,被称为动态派发。比如@datasource.send “get#{name}_info”, @id的使用。

  1. 现在我们使用define_method方法来动态创建方法,以进行第二次重构:
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
require './data_source.rb'
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def self.define_component(name)
define_method(name) do
info = @data_source.send "get_#{name}_info", @id
price = @data_source.send "get_#{name}_price", @id
result = "#{name.capitalize}: #{info} #{price}"
return "#{result}" if price >= 100
end
end
define_component :mouse # 调用第一次创建mouse方法
define_component :cpu # 调用第二次创建cpu方法
define_component :keyboard # 调用第三次创建keyboard方法
end
computer = Computer.new(2,DS.new)
puts computer.mouse
puts computer.cpu
puts computer.keyboard

define_method可以代替def关键字定义方法,这次重构中,self.define_component(name)方法执行了三次,每一次又都调用define_method分别创建了mousecpukeyboard方法。然后执行相关操作。

这三个在运行时被定义的方法被称为动态方法。

接下来又有一个关于代码维护和拓展的问题:如果将来DS类中加入了get_display_info方法,而Computer类中并没有动态派发这个方法,能不能在不修改Computer代码的基础上,自动将get_display_info方法加入到Computer的动态派发中呢?

  1. 用内省的方式第三次重构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
require './data_source.rb'
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
data_source.methods.grep(/^get_(.*)_info$/){ Computer.define_component $1 }
end
def self.define_component(name)
define_method(name) do
info = @data_source.send "get_#{name}_info", @id
price = @data_source.send "get_#{name}_price", @id
result = "#{name.capitalize}: #{info} #{price}"
return "#{result}" if price >= 100
end
end
end
computer = Computer.new(2,DS.new)
puts computer.mouse
puts computer.cpu
puts computer.keyboard

这次重构中新加入的data_source.methods.grep(/^get_(.*)_info$/){ Computer.define_component $1 }的作用如下:

当程序执行到puts computer.mouse时,会去Computer类中去找mouse方法,在initialize中,data_source.methods.grep(/^get_(.*)_info$/)如果找到get_mouse_info方法就会用mouse作为参数传给Computer.define_component,随后Computer.define_component会创建mouse方法。

上面的三次重构用到了动态派发动态方法的技术。接下来是运用幽灵方法和动态代理来解决代码重构的问题。

  1. 第四次重构:
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
require './data_source.rb'
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def method_missing(name)
super if !@data_source.respond_to?("get_#{name}_info")
info = @data_source.send "get_#{name}_info", @id
price = @data_source.send "get_#{name}_price", @id
result = "#{name.capitalize}: #{info} #{price}"
return "#{result}" if price >= 100
end
def respond_to_missing?(method, include_private = false)
@data_source.respond_to?("get_#{method}_info") || super
end
end
computer = Computer.new(2,DS.new)
puts computer.mouse
puts computer.cpu
puts computer.keyboard
puts computer.respond_to?(:mouse)

当程序执行到puts computer.mouse时,因为computer中没有直接定义mouse方法,所以会调用method_missing方法,如果在@data_source中找到了get_mouse_info,就会用mouse作为参数动态派发get_mouse_infoget_mouse_price执行相应程序返回相应结果。

什么叫幽灵方法?

method_missing 被称作幽灵方法,它可以动态地创建方法。

什么叫动态代理?

一个捕获幽灵方法调用并把它们转发给另外一个对象的对象(有时也会在转发前后包装一些自己的逻辑,在这里指自己重写的method_missing方法),称为动态代理(Dynamic Proxy)。

当一个幽灵方法和一个真实方法发生名字冲突时

这个问题是动态代理技术的通病,当一个幽灵方法和一个真实方法发生名字冲突时,后者会胜出。为了解决这个问题,你可以通过继承白板类和删除重名方法的方法来定义方法。
继承白板类:

1
class XXX < BasicObject

删除重名方法:

可以使用Module#undef_method()方法,它会删除所有的(包括继承来的)方法;也可以使用Module#remove_method()方法,它只会删除接收者自己的方法,而保留继承来的方法。