【问题标题】:Dynamically Generating Code动态生成代码
【发布时间】:2015-04-04 08:23:46
【问题描述】:

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

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(),它有效,并且肯定会耗尽我所有的个人方法,但让我感到不舒服。基本上,我有一个辅助方法,它接受键值对,键是“上下文”,值是指定为字符串的“语句”:

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

再一次,在字符串中输入大量代码让我感到不舒服。非常感谢以另一种方式进行此操作的任何帮助!

【问题讨论】:

  • 缺少信息。到目前为止,我不确定您为什么不只调用处理相同代码并接受参数的方法。或者创建一个为您执行此操作的实用程序类,并允许以某种方式(很可能)基于类型进行子类化或方法分派。到目前为止,我认为不需要任何过度动态的东西,这往往会使事情变得相当复杂。
  • 是的,如果不提供#code that stays the same across my_method_1, 2, 3, 4, etc. #code that varies across my_method_1, 2, 3, 4, etc.,不清楚如何清理它。
  • 我不会使用evalmethod_missing。我会非常谨慎地使用任何元编程,因为它往往会使代码的可读性大大降低。应该有一些其他的 Ruby 结构可以使用。
  • 我不明白这个问题。例如,对于params[:user_id]#code that stays the same across my_method_1, 2, 3, 4, etc. 可以放入一个方法中,该方法将由每个方法1...4 调用?我建议你提供一个完整的例子,我认为这不会特别冗长。

标签: ruby dynamic methods eval


【解决方案1】:

代码中的重复分支告诉我,您的类可以使用一些重构来消除对多个 if 语句的需要。听起来您的班级需要委托给另一个班级以获得特定功能。虽然我不知道您的类看起来像什么,或者它的意图,但以下是一个示例,您可以将其应用于您的代码,以便您根本不需要生成动态方法。

带有重复 if 语句的假设 Order

考虑这个Order 类具有多个相似的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 类。

重构 Order 类以利用委托方法

首先,让我们清理Order 类,然后我们将实现一个Discount 类:

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 类现在几乎可以无限扩展,因为计算折扣的逻辑完全卸载到另一个类。 Order#order_type 值确定折扣。现在,让我们定义我们的Discount 类。

实现Discount

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

  1. 20% 折扣,2014 年底到期
  2. 50% 折扣,2014 年 3 月底到期
  3. 0% 折扣(无折扣),始终从今天起 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 实例。此外,我们通过定义自己的折扣来防止人们玩系统,所有折扣的逻辑都在一个类中。

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

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

您可以使用子类来创建更具体的业务逻辑,例如“随机”折扣:

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 语句的存在意味着您的班级做得太多。它应该委托给另一个专门研究其中一个代码分支的类。您不必在运行时操作 Ruby 中的方法来实现这一点。这太“神奇”了,让新开发人员感到困惑。使用我上面概述的方法,您可以获得这些折扣是什么的强类型定义,并且您可以让每个班级专注于一项任务(不,“强类型”在正确使用时不是 Ruby 中的四个字母词)。您可以获得对象之间明确定义的关系、更易于测试的代码以及强大的业务规则执行。一切都没有“魔法”。

【讨论】:

    【解决方案2】:

    您可以通过使用instance_eval 来改进这一点,它只是在您传递它的块内更改selfString#to_sym 也避免了哈希键的 eval)。也可以让辅助方法自己定义方法,使用起来会短一些。

    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" }
    )
    

    【讨论】:

      【解决方案3】:

      你需要使用动态方法:

      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
      

      【讨论】:

      • 我不认为这是对method_missing 的适当使用,因为所有可能的情况都是已知的。不需要动态运行时挂钩。
      • ...更不用说method_missing 调试起来很麻烦。
      猜你喜欢
      • 2010-12-17
      • 1970-01-01
      • 2011-09-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-06-06
      • 1970-01-01
      相关资源
      最近更新 更多