动态生成代码

Dynamically Generating Code

我有一大堆结构相似的方法,每个方法看起来都像这样:

def my_method_1
  if params[:user_id]
    #code that stays the same across my_method_1, 2, 3, 4, etc.
    #code that varies across my_method_1, 2, 3, 4, etc.
  elsif params[:tag_id]
    #code that stays the same across my_method_1, 2, 3, 4, etc.
    #code that varies across my_method_1, 2, 3, 4, etc.
  else
    #code that stays the same across my_method_1, 2, 3, 4, etc.
    #code that varies across my_method_1, 2, 3, 4, etc.
  end
end

我有 my_method_2、3、4 等。我想做的是避免为我拥有的每个方法输入所有这些,因为大部分代码都是重复的。我只想输入在方法 1、2、3、4 等中实际不同的代码位

我在这方面的尝试使用了 eval(),它有效,并且肯定会耗尽我所有的个人方法,但让我感到不舒服。基本上,我有一个接受键值对的辅助方法,键是 "context",值是 "statement",指定为字符串:

def helper_method
  hash.each do |context, statement|
    if params[eval(":#{context}_id")]
      #code that stays the same
      eval(statement)
      return
    end
  end
  eval(hash[:none])
end

现在我的个人方法可以超级枯燥,只需调用辅助方法并传入代码字符串:

def my_method_1
  helper_method(
    user:   '#code that varies',
    tag:    '#code that varies',
    none:   '#code that varies'
  )
end

再次强调,在字符串中输入大块代码让我感到不舒服。任何以另一种方式解决这个问题的帮助将不胜感激!

您需要使用动态方法:

def method_missing(method_name)
    if method_name.to_s =~ /context_(.*)/
       #Some code here that you want
       # ...
    end
end

def respond_to_missing?(method_name, include_private = false)
   method_name.to_s.start_with?('context_') || super
end

您可以通过使用 instance_eval 来改进这一点,它只是在您传递的块内更改 self(同时 String#to_sym 避免了哈希键的评估)。您还可以让辅助方法定义方法本身,这样使用起来会更短一些。

def self.define_structured_method(name, hash)
  define_method(name) do
    hash.each do |context, block|
      if params["#{context}_id".to_sym]
        #code that stays the same
        instance_eval &block
        return
      end
    end
    instance_eval &hash[:none]
  end
end

define_structured_method(:my_method_1,
    user:   proc { puts "user code" },
    tag:    proc { puts "tag code"  },
    none:   proc { puts "else code" }
)

代码中的重复分支告诉我您的 class 可以使用一点重构来消除对多个 if 语句的需要。听起来您的 class 需要委派给另一个 class 以实现特定功能。虽然我不知道您的 class 看起来像,或者它的意图,但以下是一个示例,您可以将其应用于您的代码,这样您 就不需要 完全生成动态方法。

具有重复 if 语句的假设 Order class

考虑这个 Order class 和多个看起来相似的 if 语句:

class Order
  attr_accessor :order_type

  def discount_amount
    if order_type == 1
      .2
    elsif order_type == 2
      .5
    else
      0
    end
  end

  def discount_end_date
    if order_type == 1
      DateTime.new(2014, 12, 31)
    elsif order_type == 2
      DateTime.new(2014, 3, 31)
    else
      # Always expires 100 years from now
      DateTime.new(DateTime.now.year + 100, 1, 1)
    end
  end
end

我们有三种折扣:2014 年底到期的 20% 折扣; 50%,将于 2014 年 3 月底到期。最后,0% 的默认折扣始终在 100 年后到期。让我们清理一下以删除 if 语句,并将这些计算委托给 Discount class。

重构 Order class 以利用委托方法

首先,让我们清理一下 Order class,然后我们将执行一个 Discount class:

class Order
  attr_accessor :order_type

  def discount
    @discount ||=
      if order_type == 1
        Discount.twenty_percent_off
      elsif order_type == 2
        Discount.half_off
      else
        Discount.default_discount
      end
  end

  def discount_amount
    discount.amount
  end

  def discount_end_date
    discount.end_date
  end
end

干净整洁。 Order 对象需要 Discount 对象来获取折扣金额和结束日期。 Order class 现在几乎可以无限扩展,因为计算折扣的逻辑完全卸载到另一个 class。 Order#order_type 值决定折扣。现在,让我们定义 Discount class.

实施 Discount class

根据我们的(假)商业规则,只有三种折扣:

  1. 20% 折扣,2014 年底到期
  2. 50% 折扣,2014 年 3 月底到期
  3. 0%-off(无折扣)从今天起 100 年后总是过期,本质上意味着它永远不会过期

我们不希望人们创建任意折扣,所以让我们将 Discount 实例限制为仅使用私有构造函数定义的实例,然后为每种折扣声明静态方法:

class Discount
  private_class_method :new

  def self.default_discount
    @@default_discount ||= new(0)
  end

  def self.half_off
    @@half_off_discount ||= new(.5, DateTime.new(2014, 3, 31))
  end

  def self.twenty_percent_off
    @@twenty_percent_off ||= new(.2, DateTime.new(2014, 12, 31))
  end

  def initialize(amount, end_date = nil)
    @amount = amount
    @end_date = end_date
  end

  def amount
    @amount
  end

  def end_date
    @end_date ||= DateTime.new(DateTime.now.year + 100, 1, 1)
  end
end

尝试 运行 Discount.new(...) 应该会引发错误。我们只有三个折扣实例可用:

Discount.half_off
Discount.twenty_percent_off
Discount.default_discount

鉴于 Order#order_type 用于确定折扣,我们使用 Order#discount 模拟此方法,返回基于 Order#order_type 的正确 Discount 实例。此外,我们通过定义自己的折扣来防止人们玩弄系统,并且所有折扣的逻辑都在一个 class.

order = Order.new
order.order_type = 1
puts order.discount_amount # -> .2

order = Order.new
order.order_type = 2
puts order.discount_amount # -> .5

您可以使用 sub classing 创建更具体的业务逻辑,例如 "random" 折扣:

class Discount
  protected_class_method :new

  ...

  def self.random
    @random_discount ||= RandomDiscount.new(nil)
  end

  class RandomDiscount < Discount
    def amount
      rand / 2
    end
  end
end

现在Discount.random.amount每次输出不同的折扣。可能性变得无穷无尽。

这如何适用于您的问题

重复 if 语句的存在意味着您的 class 做得太多了。它应该委托给另一个 class,专门研究这些代码分支之一。您不必在 运行 时操作 Ruby 中的方法来实现此目的。它太多 "magic" 并且让新开发人员感到困惑。使用我上面概述的方法,您可以获得这些折扣的强类型定义,并且您让每个 class 专注于一项任务(不,"strongly typed" 不是 Ruby 正确使用时)。您将获得对象之间明确定义的关系、更易于测试的代码,并且可以强有力地执行业务规则。全部没有"magic."