Refactor Ruby Code by Meta Programming

最近重構了一些自動化部署的程式碼,因為發現程式碼裡面大部份的邏輯是一樣的。 決定使用meta programming來重構這些程式碼順便學習ruby的meta programming。

dynamic define method

看看底下的程式碼,想取得使用者輸入的版號來進行自動部署。

before_refactor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  def get_gc_version_from_user
    puts "Type a version d.d.d or leave blank to get the latest gc:"
    @gc_version = get_user_input
  end

  def get_gv_version_from_user
    return unless @config[:import_gv]
    puts "Type a version d.d.d or leave blank to get the latest gv:"
    @gv_version = get_user_input
  end

  def get_game_combo_version_from_user
    return unless @config[:import_game_combo]
    puts "Type a version d.d.d or leave blank to get the latest game combo:"
    @game_combo_version = get_user_input
  end

After refactor, use array to create methods. define_method is used to define new methods dynamically. do |argument| means your could pass parameters in. instance_variable_set is used to set a instance varaible

after_refactor
1
2
3
4
5
6
7
  ['gc','gv','game_combo'].each do |item|
    define_method("get_#{item}_version_from_user") do |argument|
      return unless argument
      puts "Type a version d.d.d or leave blank to get the #{item}"
      instance_variable_set("@#{item}_version", get_user_input)
    end
  end

After refactor, how to invoke these methods?

how_to_invoke
1
2
3
4
5
def prefer_versions
  get_gc_version_from_user(true)
  get_gv_version_from_user(@config[:import_gv])
  get_game_combo_version_from_user(@config[:import_game_combo])
end

I found it’s unconvenient to pass parameter everytime. Methods should determine if it’s executed or not. So i did a little updated. Use instance_variable_get to access instance variable and determine return or not.

after_refactor2
1
2
3
4
5
6
7
  ['gc','gv','game_combo'].each do |item|
    define_method("get_#{item}_version_from_user")
      return unless instance_variable_get("@config")["import_#{item}".to_sym]
      puts "Type a version d.d.d or leave blank to get the #{item}"
      instance_variable_set("@#{item}_version", get_user_input)
    end
  end

How to invoke?

how_to_invoke2
1
2
3
4
5
def prefer_versions
  get_gc_version_from_user
  get_gv_version_from_user
  get_game_combo_version_from_user
end

dynamic define class method

I have a util class and most of methods are class method.

before_refactor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MvnUtils
  class << self
    def find_latest_gc_version
      find_latest_version 'http://xxx.yyy.zzz/folder_a/gc/maven-metadata.xml'
    end
    def find_latest_gv_version
      find_latest_version 'http://xxx.yyy.zzz/folder_b/gv/maven-metadata.xml'
    end
    def find_latest_game_combo_version
      find_latest_version 'http://xxx.yyy.zzz/folder_c/game_combo/maven-metadata.xml'
    end
  end
  ...
end

There’re few ways to create class methods but i think below are favroite.
Use define_method inside class << self

first way
1
2
3
4
5
6
7
8
9
10
class MvnUtils
  class << self
    [%w(gc folder_a),%w(gv folder_b),%w(game_combo folder_c)].each do |item|
      define_method("find_latest_#{item[0]}_version")
        find_latest_version "http://xxx.yyy.zzz/#{item[1]}/#{item[0]}/maven-metadata.xml"
      end
    end
  end
  ...
end

OR use define_singleton_method to create class method

second way
1
2
3
4
5
6
7
8
class MvnUtils
  [%w(gc folder_a),%w(gv folder_b),%w(game_combo folder_c)].each do |item|
    define_singleton_method("find_latest_#{item[0]}_version")
      find_latest_version "http://xxx.yyy.zzz/#{item[1]}/#{item[0]}/maven-metadata.xml"
    end
  end
  ...
end

dynamic call

Originally i will retrieve the latest version from mvn if user doens’t specific the target deployment version.

before_refactor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  def check_gc_version
    if @gc_version.nil?
      @gc_version = MvnUtils.find_latest_gc_version
      abort("Can't find the latest gc version.") if @version.nil?
    end
  end
  def check_gv_version
    return unless @config[:import_gv_game]
    if @gv_version.nil?
      @gv_version = MvnUtils.find_latest_gv_version
      abort("Can't find the latest gv version.") if @gv_version.nil?
    end
  end
  def check_game_combo_version
    return unless @config[:import_game_combo]
    if @game_comb_version.nil?
      @game_comb_version = MvnUtils.find_latest_game_combo_version
      abort("Can't find the latest game comb version.") if @game_comb_version.nil?
    end
  end

Use .send() to invoke object’s method

after_factor
1
2
3
4
5
6
7
8
9
  ['gc','gv','game_combo'].each do |item|
    define_method("check_#{item}_version")
      return unless instance_variable_get("@config")["import_#{item}".to_sym]
      if instance_variable_get("@#{item}_version").nil?
        instance_variable_set("@#{item}_version", MvnUtils.send("find_latest_#{item}_version"))
        abort("Can't find the latest #{item} version.") if instance_variable_get("@#{item}_version").nil?
      end
    end
  end

Comments