alighters

程序、写作、人生

Ruby 代码重构

| Comments

在《 Ruby 元编程》一书的第二章 method 中,通过一段代码的重构,来展示 Ruby 的特性,如何以很少的代码来达到我们最终的效果。

示例 Demo

源代码:data_source.rb,其通过传入工作站点的 id,用来获取工作站点信息,如下:

1
2
3
4
5
6
7
8
9
10
11
12
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) # ...
   # ...and so on
end

调用代码如下:

1
2
3
4
5
ds = DS.new
ds.get_cpu_info(42) # => 2.16 Ghz
ds.get_cpu_price(42) # => 150
ds.get_mouse_info(42) # => Dual Optical
ds.get_mouse_price(42) # => 40

重构

可以看到在 DS 类中,有很多重复的信息。第一步,首先将其抽象成一个 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
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

但是,抽象成这个类中,可以看到方法中对 data_source 的使用还有点信息的冗余。这里可以对方法再以参数的形式调用,如下。

1.使用 Object 类的 send 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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.to_s.capitalize}: #{info} ($#{price})"
    return "* #{result}" if price >= 100
    result
  end
end

send 方法的参数指定一个方法的名称和参数,这样对方法的调用就可以抽象在 component 方法中。 调用代码:

1
2
my_computer = Computer.new(42, DS.new)
my_computer.cpu # => * Cpu: 2.16 Ghz ($220)

PS: 这种动态派发的这种特殊用法有时被称为**模式派发 (Pattern Dispatch),因为它基于方法名的某种模式来过滤方法。

2.方法 define_method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
  end
  def self.define_component(name)
    define_method(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
  define_component :mouse
  define_component :cpu
  define_component :keyboard
end

使用 define_method 方法,来在运行时动态地定义方法,也称 动态方法 (Dynamic Method)

3. 内省代码的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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) {
      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
      result
    }
  end
end

这里通过使用正则表达式,来进一步简化方法的定义。使用 grep,当满足之后的正则表达式,则会定义相应的方法。

4.method_missing 的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
  end
  def method_missing(name, *args)
    super if !@data_source.respond_to?("get_#{name}_info")
    info = @data_source.send("get_#{name}_info", args[0])
    price = @data_source.send("get_#{name}_price", args[0])
    result = "#{name.to_s.capitalize}: #{info} ($#{price})"
    return " * #{result}" if price >= 100
    result
  end

end

method_missing 方法又称 ghost 方法(幽灵方法),是指在方法的调用过程中,若是在其类型中及其祖先链上找不到相应的方法,则会在实例上调用 method_missing 方法(其属于 Kernel 的一个实例方法,而所有的对象都继承自 kernel 模块)。这里通过重写 method_missing 方法,来达到对 data_source 中相应的方法的动态调用。

参考资料

Metaprogramming Ruby

版权归作者所有,转载请注明原文链接:/blog/2016/09/26/ruby-code-refactor/

给 Ta 个打赏吧...

Comments