Ruby 学习笔记簿:Metaprogramming Workshop - The Legacy System

本篇要分享的是此书(在第三章)我蛮喜欢的范例之一,作者以说故事的方式讲解本章节所介绍的题目,假设的情境是新进员工被会计部门赋予一项任务,目标是找出花费大於美金$99元的电脑配件。

我做了此范例可使用的 DS(搭配CSV档),这样修改起来比较有感觉。大家如有有兴趣可以在我的 GitHub The Legacy System Demo下载。

主角所面临的旧系统中存在着许多重复及冗长的程序码,达成任务的过程就是以本章学习到的技巧来重构更简洁、易维护的系统。

还未修改的程序码如下:

class DS
  def initialize                           # connect to data source...
  def get_cpu_info(workstation_id)         # ...
  def get_cpu_price(workstation_id)        # ...
  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_display_info(workstation_id)     # ...
  def get_display_price(workstation_id)    # ...
  # ...and so on

当建立新的 DS 实例物件时,DS#initializ 方法就会连接到资料系统里。如需要询问特定电脑的配件资讯,可以将该电脑ID带入到对应的实例方法中,就可以得到相关讯息。

ds = DS.new
ds.get_cpu_info(42)      # => "2.9 Ghz quad-core"
ds.get_cpu_price(42)     # => 120
ds.get_mouse_info(42)    # => "Wireless Touch"
ds.get_mouse_price(42)   # => 60

#  这代表了电脑ID 42的:
#  CPU 是 "2.9 Ghz quad-core", 价格是美元 $120
#  滑鼠 是 "Wireless Touch", 价格是美元 $60...(这滑鼠也太贵了吧)

这位新进员工马上就写了初步的解决方案如下:

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

不久,他就发现自己的程序码存在了重复编码的情形,於是请教了资深的同事帮忙。这位同事建议两个方案来解决问题:(1) Dynamic Methods、 (2) Method Missing

方案1:Dynamic Methods

Step 1: Adding Dynamic Dispatches

首先注意到在 Computer 内的每个方法都有类似的程序码,先把它抓出来再想办法修改。

  info = @data_source.get_mouse_info(@id)
  price = @data_source.get_mouse_price(@id)
  result = "Mouse: #{info} ($#{price})"
  return "* #{result}" if price >= 100 result

还记得 send 方法吗? 我们可以让方法名称变成只是字串变数"get_#{name}_info",经由#{name}变数的改变,便可以用 send() 来『 动态呼叫 』原本重复的方法。

class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
  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
    result
  end

  def mouse
    component :mouse
  end

  def cpu
    component :cpu
  end

  def keyboard
    component :keyboard
  end
end

Step 2: Generating Methods Dynamically

接者我们利用 define_method 可 『 动态定义 』方法的特性,把 mouse()、cpu()、及 keyboard() 再做简化。作者建立 define_component 的类别方法,并将定义动态方法的程序码放在里面。然後以呼叫 define_component() 来建立实例方法,最终的输出都是相同的。

这里我提供另一个写法,其实也可直接将方法名称放在同ㄧ个集合中,再用 each do 转出来就好。

class Computer
  def initialize(computer_id, data_source)
    @id = computer_id
    @data_source = data_source
  end

  #----------------- 以集合(Array)方式来做 ---------------------
  titles = ['cpu', 'mouse', 'keyboard']

  titles.each do |title|
      define_method(title) do
      info = @data_source.send("get_#{title}_info", "#{@id}")
      price = @data_source.send("get_#{title}_price", "#{@id}")
      result = "#{title.capitalize}: #{info} ($#{price})"
      return "* #{result}" if price >= 100
      result
    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.capitalize}: #{info} ($#{price})"
      return "* #{result}" if price >= 100
      result
    end
  end

  define_component :mouse
  define_component :cpu
  define_component :keyboard
  #-----------------------------------------------------------
end

Step 3: Sprinkling the Code with Introspection

修改到目前为止,程序码重复的情况其实几乎没有了。不过你还是可以利用 Regular Expression 让上步骤最後三行程序码都省略掉。 注意在 initialize() 多了ㄧ行使用正规表示法的程序码,当 block 依附在 Enumerable#grep()方法时,这个 block 将会被与 grep 里的 regular expression 做对比,而符合的结果会被储存在**$1**全域变数里。

换句话说,如果在 data_source 中有 get_cup_info() 以及 get_mouse_info() Computer.define_component(cpu)Computer.define_component(mouse) 就会被呼叫和执行。

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
      result
    end
  end
end

方案2:Methods Missing

使用 Methods Missing 就简单许多了。如果有看过之前 Chapter 3:Methods - Part III 的文章,应该就不需要多作解释了。


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
    result
  end
end

如果有任何问题,都欢迎留言询问!


<<:  JS 如何运行 DAY45

>>:  第 54 天 - 学习 PHP CLI

GCP IAP

GCP IAP 今天再来了解一下什麽事IAP?他的全名即是dentity-Aware Proxy简称...

第3天~调查表

2021/01/15再用https://developer.android.com/studio 的...

Day 7 Dart语言-资料型态

资料型态 内建资料型态是构成整个程序的最小型态单位,是程序中不可或缺的元素,而Dart的内建类型主要...

DAY30:赛後心得检讨

完赛检讨 资料处理 虽然我们有大致上把红框等杂讯去除掉,但我们还是没有完全把照杂讯清除乾净。 Yol...

JavaScript Day 24. DOM API 节点

之前我们有提到,JavaScript 如何与浏览器沟通,於是我们讨论到透过 JavaScript 取...