在 Ruby 这种动态语言中,方法的调用是极为灵活的,并不会在编译器就爆出各种错误,比方说我定义一个 User 类,我想调用 hi 方法,我并没有定义 hi 方法,但这并不妨碍我编写代码,运行期我去找 hi 方法,如果发现没有这个方法,最终我可以向这个类中添加我想要的方法在运行期间。这给了我们更多的操作空间,这将是我们要学习的。
现在我们有一个老的系统需要我们重构,老板要求系统自动为超过 99 美金的开销天添加标记
class DS
def initialize ; end# 连接数据源
# 原先设计的访问方法
def get_cpu_info(id) ; end
def get_cpu_price(id) ; end
def get_mouse_info(id) ; end
def get_mouse_price(id) ; end
def get_keyBoard_info(id) ; end
def get_keyBoard_price(id) ; end
end
ds = DS.new
# 获取信息
ds.get_cpu_info(1)
# 获取价格
ds.get_cpu_price(1)
我们现在需要将数据源封装起来,每个 computer 为一个对象,并为每个组件定义通用的方法
class Computer
# data_source 就是上面的DS对象
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})"
result = "*" + result if price > 99
return result
end
def cpu
info = @data_source.get_cpu_info(@id)
price = @data_source.get_cpu_price(@id)
result = "Cpu: #{info} : (#{price})"
result = "*" + result if price > 99
return result
end
# ... 类似操作
end
我们可以看到 mouse 和 cpu 就有大量的代码重复,如果后面还需要加其他的,则会让代码臃肿且冗余。
我们有两种办法进行重构优化:动态方法 和 method_missing
class MyClass
def method(content)
end
end
obj = MyClass.new
obj.method "Hello World"
# 动态派发和上面普通调用的结果是一样的, 将 obj.method 替换为 obj.send(:method)
obj.send(:method,"Hello World")
为什么使用动态派发?
因为可以在运行最后才决定具体调用哪个方法。而不是硬编码决定
这里使用
:method
,而不是"method",实际上是一样的
obj.send("method","Hello World")
:method 表示的是一个 Symbol 符号
"method"则是一个 String 字符串,一般在元编程我们常常使用 Symbol,因为 Symbol 是不可变的。字符串是可变的。
符号与字符串是很容易相互转化的
"abc".to_sym # => :abc
:abc.to_s # => "abc"
class MyClass
# 这里就是定义了一个实例方法 将 def my_method 替换为 define_method :my_method,参数部分,通过Block传递
define_method :my_method do |my_arg|
my_arg * 3
end
end
obj = MyClass.new
obj.my_method 2
在运行时定义方法的技术称为动态方法。Module#define_method
为什么使用动态方法,而不是直接定义:def
因为这样可以在运行最后决定方法名叫什么,和上面动态调用方法类似,我们想在运行期间再决定一些事情
class Computer
def initialize(computer_id,data_source)
@id = computer_id
@data_source = data_source
end
def mouse
companent :mouse
end
def cpu
companent :cpu
end
def companent(name)
# 这里使用了动态调用方法
info = @data_source.send("get_#{name}_info",@id)
price = @data_source.send("get_#{name}_price",@id)
result = "#{name}: #{info} : (#{price})"
result = "*" + result if price > 99
return result
end
# ... 类似操作
end
我们使用动态派发的方式,抽离出一个公共组件,其他配件可以直接使用,代码量减少的多
我们再用动态定义方法去试着重构一下代码
class Computer
def initialize(computer_id,data_source)
@id = computer_id
@data_source = data_source
end
# 这里定义一个类方法,这里的self指向的是Computer类常量
def self.define_companent(name)
# 根据传入的Symbol,创建相应的方法
define_method(name) do
info = @data_source.send("get_#{name}_info",@id)
price = @data_source.send("get_#{name}_price",@id)
result = "#{name}: #{info} : (#{price})"
result = "*" + result if price > 99
return result
end
end
## 这里主动调用 并动态创建对应的方法
define_companent :mouse
define_companent :cpu
define_companent :keyboard
end
现在 Computer 已经剩不了多少代码了,我们使用内省方式缩减代码
class Computer
def initialize(computer_id,data_source)
@id = computer_id
@data_source = data_source
# 主动根据DS中给定的访问方法,创建访问方法,而不需要我们再去手动控制
data_source.methods.grep(/^get_(.*)_info$/ |) {
# 被正则表达式匹配到的方法,会依次调用这里传递的块儿,并将内容封装到 $1 全局变量中
Computer.define_companent $1
}
end
def self.define_companent(name)
define_method(name) do
info = @data_source.send("get_#{name}_info",@id)
price = @data_source.send("get_#{name}_price",@id)
result = "#{name}: #{info} : (#{price})"
result = "*" + result if price > 99
return result
end
end
end
我们在初始化方法中加入几场代码就可以让代码更加简洁。
你将学习幽灵方法和动态代理
class User
def method_missing(method,*args)
puts "You called: #{method} (#{args.join(',')})"
puts "(You alse passed it a block)" if block_given?
end
end
obj = User.new
obj.hi
在 Ruby 中我们可以随意调用一个方法,而这个方法可能根本不存在,当运行时在当前对象的继承链上都没有找到这个方法时,会去找当前对象的 method_missing 方法,它就好像每个无家可归的人最终的点,method_missing 是BasicObject
中定义的私有实例方法,所以每个子类都可以使用这个方法,而 BasicObject 中是直接抛出这个异常,所以需要我们自己去重写。method_missing
也叫做幽灵方法。
现在我们通过 method_missing 来重构我们的 Computer 类
class Computer
def initialize(computer_id,data_source)
@id = computer_id
@data_source = data_source
end
# name 为 调用的方法名,args 表示参数 ,*表示接受所有的参数,封装为一个数组
def method_miss(name,*args)
# 判断@data_source是否有这个方法?如果没有则调用super.method_miss,也就是未找到该方法
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}: #{info} : (#{price})"
result = "*" + result if price > 99
return result
end
end
现在发现我们不需要再定义额外的方法了,直接通过幽灵方法来做判断与返回,这里方法:respond_to?
表示该实例是否有目标方法,
如果方法返回值为 Boolean,一般会在方法名定义时使用?
,如:def is_black?()
表示。
如何问 Computer 对象是否响应幽灵方法?
cmp = Computer.new(0,DS.new)
cmp.respond_to?(:mouse) # => false
无法响应到,因为 :mouse 是一个幽灵方法,我们需要重新定义respond_to_missing()
class Computer
def respond_to_missing?(method,include_private=false)
@data_source.respond_to?("get_#{method}_info") || super
end
end
cmp = Computer.new(0,DS.new)
cmp.respond_to?(:mouse) # => true
所以正确的做法是每次覆写method_missing
时,同时也覆写respond_to_missing?
方法
通常,幽灵方法都是锦上添花的左右,不过有些对象的功能,几乎完全依赖于他,这些对象通常是一些封装对象,他们封装的可以是另一个对象,web 服务或者其他语言写成的代码,这些对象通过 method_missing 方法收集方法调用,并把这些调用转发到被封装的对象上
如果对于一个常量的引用发现找不到,则会默认调用const_missing
方法,将常量名作为一个符号进行传递。
在 Rake 中就有使用,为了兼容老版本的 Task 和新版本的 Rake::Task。
module Rake
class Task; end
class FileTask; end
end
class Module
def const_missing(const_name)
case const_name
when :Task
p "提示:原有的Task,已经移入,Rake名称空间,请使用 Rake::Task"
Rake::Task
when :FileTask
p "提示:原有的Task,已经移入,Rake名称空间,请使用 Rake::FileTask"
Rake::FileTask
end
end
end
# main是Object的实例,Object是Class的实例,Class的父类是Module,所以当我们使用猴子补丁修改
# Module#const_missing时,main对象是继承到这个方法的。
# 我们想要访问Task常量,发现并不存在,则触发了const_missing(const_name)
p Task.new
p FileTask.new
我们设计一个按照人名,抽号码的小程序
class Roulette
def method_missing(name,*args)
person = name.to_s.capitalize
3.times do
# 这里在块儿内定义了number
number = rand(10)+1
puts "#{number}..."
end
# 这里又使用了number,因为作用域的不同,运行时找不到这个变量,所以默认会找 number这个方法,因为也没有这个方法,所以调用了method_missing方法,导致不断的重入
"#{name} got #{number}"
end
end
number_of = Roulette.new
p number_of.bob
p number_of.Jack
不知道你是否可以看出来?当程序运行时会不断方法重入,直到栈溢出。所以我们需要进行改良。
class Roulette
def method_missing(name,*args)
person = name.to_s.capitalize
# 判断是否名字是否存在,如果不存在,直接报错
super unless %w[Bob Frank Bill].include? person
# 将局部变量作用域移出
number = 0
3.times do
number = rand(10)+1
puts "#{number}..."
end
"#{name} got #{number}"
end
end
number_of = Roulette.new
p number_of.Bob
p number_of.Frank
比如说上面的 number_of.display 我们希望实际调用 method_missing 方法,但实际上可能调用了 Object.display 方法,这是因为我们从 Object 类中继承了大量的方法,所以时常导致幽灵方法与继承方法的重复。
如果实例存在继承方法,则幽灵方法是失效的。我们有两个办法:
BasicObject 是 Object 的父类,其中定义的实例方法很少,所以我们可以让现有的类继承 BasicObject,从而可以避免继承 Object 类的方法,这是最简单的白板类实现方法
# [:__send__, :!, :instance_eval, :==, :instance_exec, :!=, :equal?, :__id__]
p BasicObject.instance_methods
所以我们最终可以选择让Roulette 继承 BasicObject
,或者删除指定方法
幽灵方法更容易出现隐性 Bug,所以能使用动态方法,尽量使用动态方法,除非不得不使用时,才去使用,记住如果重写response_to?
,也要重写response_to_missing?