《Ruby元编程》读书笔记-方法
静态类型检查
有些语言比如Java,对象间的每一次方法调用,编译器都会检查接受对象是否有一个匹配的方法,称之为静态类型检查(static type checking),这类语言被称为静态语言(static language)
动态语言———比如 Python 和 Ruby,直到方法被真正执行,对象无法理解调用时才会报错
静态类型检查的好处:在代码运行前编译器就可以发现其中的一些错误 静态语言强迫你写很多误区和重复的代码
Ruby中,契约方法不再是问题
历史遗留系统 The Legacy System
程序需要加载的数据存储在一个历史遗留系统中
class DS
def initialize # connect to data source ...
def get_mouse_info(workstation_id) ...
def get_mouse_price(workstation_id) ...
def get_keyboard_info(workstation_id) ...
def get_keyboard_price(workstation_id) ...
def get_cpu_info(workstation_id) ...
def get_cpu_price(workstation_id) ...
def get_display_info(workstation_id) ...
def get_display_price(workstation_id) ...
...
end
需要检测出计算机各配件的信息与价格,将价格高于99的设备信息打印出来
事不过三 Double, Treble…Trouble
抽象出一个类型Computer
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
result
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
result
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
result
end
...
end
写到这里,你发现自己陷入了不断拷贝、粘贴代码的泥潭。你不仅有一大堆方法要写,而且每个方法还要写单元测试,否则这些重复性代码就很容易出错。很快你就感到乏味了———更别提有多痛苦了。
我们怎么来重构它?
动态方法
那时我还是个年轻的开发者,正在学习C++,我的导师告诉我,当你调用一个方法时,实际上是给一个对象发送了一条消息。
class MyClass
def my_method(my_arg)
my_arg * 2
end
end
obj = MyClass.new
obj.my_method(3) # => 6
使用Object#send()取代点标记符来调用MyClass#my_method()方法
obj.send(:my_mehtod, 3) # => 6
来自Test::Unit的例子
Test::Unit标准库使用一个命名管理来判定哪些方法时测试方法
一个TestCase对象会查找自己的公开方法,并选择其中名字以test开头的方法:
method_names = public_instance_methods(true)
tests = method_names.delete_if {|mehtod_name| method_name !~ /^test./}
得到测试方法数组,使用send()方法来调用数组中的每个方法
符号
符号与字符串没有关系,并且它们属于完全不同的类
不同的人有不同的答案。有些人会告诉你,符号和字符串的区别在于:符号是不可变的,而你可以修改字符串中的字符。另外,一些操作针对符号运行得更快些。
绝大多数情况下,符号用于表示事物的名字,尤其是跟元编程相关的名字,比如方法名
符号与字符串的转换
# 字符串转符号 String#to_sym() 或 String#intern()
# 符号转字符串 Symbol#to_s() 或 Symbol#id2name()
定义动态方法
Module#define_method()
class MyClass
define_method :my_method do |my_arg|
my_arg * 2
end
end
obj = MyClass.new
obj.my_method(3) # => 6
重构
第一步:添加动态派发
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 display
component :display
end
def component(name)
info = @data_source.send "get_#{name}_info", @id
price = @data_source.send "get_#{name}_price", @id
result = "#{name.to_s.capitalize}: #{info} ($#{price})"
return " * #{result}" if price >= 100
result
end
end
第二步:动态创建方法
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.to_s.capitalize}: #{info} ($#{price})"
return " * #{result}" if price >= 100
result
end
end
define_component :mouse
define_component :cpu
define_component :keyboard
define_component :display
end
第三步:用内省(Introspection)方式缩减代码
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
data_source.methods.grep(/^get_(.+)_info$/) do
Computer.define_component $1
end
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.to_s.capitalize}: #{info} ($#{price})"
return " * #{result}" if price >= 100
result
end
end
end
method_missing()方法
class A
end
a = A.new
a.hello
由于A类中不存在hello实例方法,报错如下
in <top (required)>': undefined method
hello’ for #
沿着祖先链查找hello方法
A > Object > Kernel > BasicObject
找不到hello()方法,则调用method_missing()方法
覆写method_missing()方法
class A
def method_missing(method, *args)
puts "You called: #{method}(#{args.join(', ')})"
end
end
a = A.new
a.hello
输出
You called: hello()
幽灵方法
来自Ruport的例子
require 'ruport'
table = Ruport::Data::Table.new column_names: ['country', 'wine'],
data: [['France', 'Bordeaux'],
['Italy', 'Chianti'],
['France', 'Chablis']]
found = table.rows_with_country('France')
found.each do |row|
puts row.to_csv
end
输出
France,Bordeaux France,Chablis
rows_with_country与to_csv是幽灵方法,Table中method_missing实现如下
class Table
...
def method_missing(id,*args,&block)
return as($1.to_sym,*args,&block) if id.to_s =~ /^to_(.*)/
return rows_with($1.to_sym => args[0]) if id.to_s =~ /^rows_with_(.*)/
super
end
...
end
来自OpenStruct的例子
OpenStruct来自于Ruby标准库。一个OpenStruct对象,如果需要一个新属性只需要直接它赋个值即可
require 'ostruct'
user = OpenStruct.new
user.id = 123
user.name = 'Ant'
puts "#{user.id} #{user.name}"
其内部使用了method_missing,属性其实是幽灵方法
实现一个简化版的开放结构类
class MyOpenStruct
def initialize
@attributes = {}
end
def method_missing(name, *args)
attribute = name.to_s
if attribute =~ /=$/
@attributes[attribute.chop] = args[0]
else
@attributes[attribute]
end
end
end
user = MyOpenStruct.new
user.id = 123
user.name = 'Ant'
puts "#{user.id} #{user.name}"
再一次重构Computer类
class Computer
def initialize(computer_id, data_source)
@id = computer_id
@data_source = data_source
end
def method_missing(name, *args)
info = @data_source.send "get_#{name}_info", @id
price = @data_source.send "get_#{name}_price", @id
result = "#{name.to_s.capitalize}: #{info} ($#{price})"
return " * #{result}" if price >= 100
result
end
end
computer = Computer.new(1, DS.new)
puts computer.mouse
puts computer.cpu
puts computer.keyboard
# puts computer.display
狩猎bug
class Roulette
def method_missing(name, *args)
person = name.to_s.capitalize
3.times do
number = rand(10) + 1
puts "#{number}"
end
"#{person} got a #{number}"
end
end
number_of = Roulette.new
puts number_of.bob
puts number_of.frank
方法冲突
代码
puts computer.display # => #
通过irb执行如下语句,可以看到Object中已经存在有display方法
irb(main):007:0> Object.instance_methods.grep /^d/ => [:dup, :display, :define_singleton_method]
将Computer类重构为白板类
方案一
class Computer
instance_methods.each do |m|
undef_method m unless m.to_s =~ /^__|method_missing|respond_to?/
end
方案二
从Ruby1.9开始,白板技术被集成到语言自身中,Object类有一个名叫BasicObject的超类
class Computer < BasicObject
性能方面的忧虑
幽灵方法,命名冲突和神秘的bug,一些人还会加上一个缺点:总体而言,使用幽灵方法比使用普通该方法要慢,因为调用幽灵方法查账的路径一般要更长些
某些情况下,你需要意识到这种性能问题,但这通常不是什么大问题。为了避免猜来猜去,在你过于担心是否需要优化之前,最好先用性能分析器测试你的代码。
小结
在其他语言中,习惯于在方法内部发现并消除其重复性,在Ruby中,发现并消除方法之间的重复性
其他语言中,通过常规面向对象思想来消除重复,Ruby中,则同时可以借助元编程的力量来消除重复