Ruby 有没有比较好的方法,去解析一个 block 里面的代码

ad583255925 · March 31, 2017 · Last by pynix replied at April 03, 2017 · 1775 hits

我想造一个方法,使用方法是这样的

class User < ActiveRecord::Base

  redis_cache 10 do
    def self.aaa
      'it works'
    end

    def bbb
      'it works'
    end
  end
end

我想实现的效果是,包裹在 redis_cache 块里面的方法,会在第一次执行后,写入 redis,并且可以设定周期,这样第二次执行的时候会从 redis 里面取,起到缓存的效果

为此我是这样写的

class ActiveRecord::Base
  def self.redis_cache seconds=300, &block
    if !self.is_a?(Class)
      mod = self.const_set(:TopMethods, Module.new)
      mod.module_eval &block
      include mod
    else
      self.superclass.class_eval &block
    end
    require 'sourcify'
    s = block.to_source(:strip_enclosure => true)
    s.split(/\n/).select{|f| f.include?('def')}.each do |s|
      m_name = s.gsub('def', '').split('(')[0].strip
      self.class_eval <<-CODE
        def #{m_name}(*args)
          key = __method__.to_s + args.join('-')
          key += ('-' + self.id.to_s) if !self.is_a?(Class)
          puts key
          results = Api::RedisClient.get(key)
          if results
            puts 'from redis'
            results
          else
            results = Api::RedisClient.set(key, super)
            Api::RedisClient.set_expire(key, "#{seconds}".to_i)
            super
          end
        end
      CODE
    end
  end
end

思路是这样的,拿到 block 之后,找到 self 的 superclass,把方法写在里面,然后再打开 self,再写一遍方法,这样就能用 super 往上面找,我用的解析代码块的 gem 'sourcify' http://numbers.brighterplanet.com/2010/12/17/replacing-parsetree-in-ruby-1-9/不是很稳定,复杂的方法经常解析报错,有什么更好的实现思路吗。

感觉是要在 Ruby 里面做解析器的事情了,有好多细节的,例如变量、Model 属性是否能取到这样的问题。

为何不在函数里面使用 redis_cache 呀?无非就是多写几个而已,况且这样的做法也不是什么地方都适用,因此也不会需要些很多的。


当然,你要说从研究实现的角度来看,这没问题,要这么用,我觉得有点过头了。


貌似 Ruby 编写 DSL 是无法支持 def 这样的关键字的,所以好像还从没在 Ruby 社区里面见过可以实现做成你希望的:

redis_cache do
  def foo
  end
end

哦,有一个 class_methods do; end http://api.rubyonrails.org/classes/ActiveSupport/Concern.html

Reply to huacnlee

纯兴趣,我感觉要是 block 里面的东西能有个比较好的解析方法,能实现好多有趣的东西

我猜你是想实现一个装饰器?

试试 prepend

Reply to ad583255925

研究 class_methods 的实现

Reply to cqualpha

实现不是难在祖先连关系上,是 Proc 中的众多 def ... end 怎么一个个拿出来加工,目前是转成字符串,做一些文字处理取出来。。

按照 ruby 流,You don't really need it 的思想,不妨先说说 LZ 为什么要实现这个么一个东西。

尝试了一下另一种写法,不知道能不能满足。不过这种代码玩一下好,真要用还真不敢..😅

module RedisCache
  CACHE = {}

  def self.redis_cache_method_prefix
    4.times.map { 10.times.map { (97 + rand(26)).chr }.join('') }.join('_')
  end

  def redis_cache &block
    mod1 = Module.new(&block)

    ims = mod1.instance_methods

    mod2 = Module.new do
      ims.each do |im|
        prefix = RedisCache.redis_cache_method_prefix

        class_eval <<-CODE
          def #{im}(*args)
            key = '#{prefix}_#{self.object_id}_#{im}'

            if RedisCache::CACHE[key]
              puts RedisCache::CACHE[key]
            else
              super
              RedisCache::CACHE[key] = key
            end
          end
        CODE
      end
    end

    mod1.prepend(mod2)
    self.include(mod1)
  end
end

class A
  extend RedisCache

  redis_cache do
    def hi
      puts 'hello, world'
    end

    def hi2
    end
  end
end

A.new.hi #=> hello, world
A.new.hi #=> btafxausko_gnmqyfidcy_kdyvsmyqha_jkxqiwvemz_16998140_hi
Reply to saiga

本来也就是好玩,整天写的都是公司用的业务代码,很 KUSO,偶尔也玩玩。

Reply to saiga

instance_methods 这个很妙啊,我完全想多了😅

执行 block 前,重写 method_added,然后 class_eval 那个 block,之后恢复 method_added

在 Java 里面很流行的做法,用 annotation 做 AOP,ruby 有一些 gem 可以让你很方便写,比如 https://github.com/comparaonline/co_aspects

你需要自己写一个 Aspect:

module CoAspects
  module Aspects
    class CacheAspect < Aspector::Base
      around method_arg: true do |method, proxy, *args, &block|
        cache_key = [self.class, method].join('.') + ':' + args.join(',')
        Cache.fetch cache_key do
          proxy.call(*args, &block)
        end
      end
    end
  end
end

用到这个 Aspect 的地方,写一个 annotation(伪)就可以了:

class Foo
  aspects_annotations!

  _cache
  def bar
    'bar'
  end
end
Reply to saiga

ruby 有个钩子叫 method_added,然后拿到名字以后,用 instance_method 把方法拿出来,再 bind(xx).call 就好了,这样可以不用写 eval 字符串(虽然可能更快)

X-Y problem

还是把你的原始需求说出来吧。。。。

You need to Sign in before reply, if you don't have an account, please Sign up first.