仅在我自己的代码内进行猴子修补方法(通过自动使用优化?)

时间:2019-07-30 06:05:20

标签: ruby refinements

在我的Ruby(on Rails)项目中,我想禁止或限制使用标准库提供的某些方法。示例:我想禁止调用Float#to_d,因为当有人在Float文字上使用该方法时出现舍入错误。我想限制String#to_d仅适用于完全有效的字符串,因为我有一些错误由'string'.to_d返回0.0导致。

用猴子修补/全局覆盖这些方法当然是个坏主意。可能会破坏某些依赖性。

添加一个linter规则来扫描代码以不调用任何#to_d方法的问题在于,它错误地限制了调用Integer#to_d之类的合法方法。当然,所有合法方法都可以使用不同的名称添加到相应的类中。但这需要添加大量的锅炉(用于方法)并更改这些方法的所有调用。

我还考虑过使用改进。这将与猴子修补类似,但仅适用于使用细化的范围。但是,必须向每个文件添加using语句很丑陋且容易出错。是否可以为项目中的每个文件自动激活优化,但不能为依赖项激活?

1 个答案:

答案 0 :(得分:0)

如果您不小心,可以进行修补。我尝试实现它,看看它是否有效。显然,您将对性能造成很大的影响,所以我不建议在生产环境中使用它:P通过记住测试的位置而不是每次调用该方法都进入目录树,可以大大提高速度。 / p>

这个想法是,如果在类上定义了原始方法,则将其移开,然后替换一个方法,该方法将检查您是否正在从代码中调用。我正在使用包含.git目录的目录作为您的项目目录;如果您直接在项目目录中有一个vendor目录,则该目录与项目目录之外的所有目录都将被豁免。如果您位于免税地点,只需将其传递给保存的旧方法或继承链;如果没有,那就尖叫犯规。

require 'pathname'

PROJECT_CODE = Pathname.new(__dir__).ascend.find { |loc| (loc / '.git').directory? }
VENDOR_CODE = PROJECT_CODE / 'vendor'

class ForbiddenMethodError < StandardError; end

def forbid_method(klass, meth, &block)
  case
  when klass.instance_methods(false).include?(meth)
    old_meth = :"forbid_method_old_#{meth}"
    klass.alias_method old_meth, meth
  when klass.respond_to?(meth)
    old_meth = nil
  else
    raise ArgumentError, "No such method: #{klass}##{meth}"
  end

  klass.define_method(meth) do |*args|
    if !block || instance_exec(*args, &block)
      caller_loc = Pathname.new(caller_locations.first.path).expand_path
      caller_loc.ascend do |ancestor|
        case ancestor
        when PROJECT_CODE
          raise ForbiddenMethodError, "#{klass}##{meth}", caller[2..]
        when VENDOR_CODE
          break
        end
      end
    end

    if old_meth
      send(old_meth, *args)
    else
      super(*args)
    end
  end
end

这样,您可以使Float#to_d和(有条件地)String#to_d在项目代码中失败:

require 'bigdecimal/util'
forbid_method(Float, :to_d)
forbid_method(String, :to_d) { !BigDecimal(self) rescue true }

如果传递块,则仅当块条件为true时才禁止使用该功能。 (该块将传递该方法的所有参数,并将以被禁止的方法的接收者作为self执行。)