Ruby元编程偶遇问题

一、元编程的上下文self

元编程的上下文

重点是 class中方法,定义的 define_methods 是实例方法

class Config
  define_method(:outer) do 
    p "----outer"
    p self
    p "----outer end"
  end

  class << self
    def set(keyname, value)
      p '----set'
      p self
      p '----set end'

      define_method(keyname) do 
        p "----#{keyname}"
        p self
        p "----#{keyname} end"
      end

      define_singleton_method(:z2) do
        p "----singleton"
        p self
        p "----singleton end"
      end
    end
  end
end


Config.set :z1, "ffff"

c = Config.new
c.z1
c.outer
Config.z2

二、eval中 查找变量失败,要不要 inspect

value 如果不加 inspect 会在eval的上下文被认为是变量对待,寻找,找不到而报错 这里使用了 inspect 之后,value就可以作为值正常参与计算。

可能很难注意到,如果字符串,你需要添加单引号。这里使用inspect是合理的。

class Config
  class << self
    def set(keyname, value)
      instance_eval("@#{keyname}=#{value.inspect}")
      instance_eval("def #{keyname};@#{keyname}; end")
      instance_eval("def #{keyname}=(val);@#{keyname}=val; end")
    end
  end
end


Config.set :z1, "ffff"

p Config.z1

Config.set :z1, "FFFF"

p Config.z1


为什么需要 inspect

class Config
  class << self
    def set(keyname, value)
      instance_eval("@#{keyname}='#{value}'") # 必须加引号,否则就是认为是变量呀
      instance_eval("@#{keyname}=#{value.inspect}") # inspect做了一个可读值转化,其实这里就是字符串了,所以可以工作

      instance_eval("def #{keyname};@#{keyname}; end")
      instance_eval("def #{keyname}=(val);@#{keyname}=val; end")
    end
  end
end


Config.set :z1, "ffff"
p Config.z1
Config.set :z1, "FFFF"
p Config.z1

三、你可能使用 heredoc 失败, 因为 heredoc 需要严格对待开始结束符

HereDoc的问题在于,HereDoc后面不能写东西

class Config
  class << self
    def set(keyname, value)
      instance_eval(<<-CODE
        @#{keyname}=#{value.inspect}
        
        def #{keyname}
          @#{keyname}
        end

        def #{keyname}=(val)
          @#{keyname}=val
        end        
      CODE
      # 重点在于 CODE 的heredoc 结束符后面不能有东西,不能写括号
      )
    end
  end
end


Config.set :z1, "ffff"
p Config.z1
Config.set :z1, "FFFF"
p Config.z1

四、一个配置实例

我写的不如 Sinatra 源码好,但是胜在清晰。

set 动态捆绑三个方法到 单例上。

configure 方法执行的时候,上下文在class中,使用的是 class 方法的set

class Setting
  class << self
    def set(keyname, value)
      instance_eval(<<-CODE
        @#{keyname}=#{value.inspect}
        
        def #{keyname}
          @#{keyname}
        end

        def #{keyname}=(val)
          @#{keyname}=val
        end
        
        def #{keyname}?
          !!#{keyname.inspect}
        end
      CODE
      )
    end

    def configure(&block)
      class_eval(&block)
      # instance_eval(&block)  # 这两个在这里其实是一样的
    end
  end
end

完整的例子,可以进行继承的配置例子。

module Config
  class BaseConfig
    class << self
      def set(keyname, value)
        instance_eval(<<-CODE
          @#{keyname}=#{value.inspect}
          
          def #{keyname}
            @#{keyname}
          end

          def #{keyname}=(val)
            @#{keyname}=val
          end
          
          def #{keyname}?
            !!#{keyname.inspect}
          end
        CODE
        )
      end
      def configure(&block)
        instance_eval(&block)
      end
    end
  end

  class Production < BaseConfig
  end

  class Develop < Production
  end

  class Test < Develop
  end
end


Config::Develop.configure do
  set :app_env, ENV.fetch('APP_ENV'){ 'development' }
  set :bind, ENV.fetch('HOST') { '0.0.0.0' }
  set :port, ENV.fetch('PORT') { 3000 }
  set :secrets, ENV.fetch('SECRETS') { 'YOU CANNT GUESS ME' }
  set :max_threads, ENV.fetch('MAX_THREADS') { 5 }
  set :database_url, ENV['DATABASE_URL']
end

Config::Test.configure do 
  # set :database_url, ENV['DATABASE_URL']
end

Config::Production.configure do 
  # set :database_url, ENV['DATABASE_URL']
end

p Config::Develop.secrets


主要的思想就是:

  1. 通过set方法,根据输入 的 keyname、value 动态的在 class上,生成类似静态的方法的 类方法、亦或是 单例方法。这两个概念在class上是一致的。
  2. 使用 configure 方法,对外提供一个接口,接口内部的block 执行在 class 的上下文中,可以使用 set 对 class 进行动态定义
  3. 使用继承,以及搜索链条,可以让配置呈现出继承关系。

我在 Python的 Flask框架中看到过类似的配置。思想是一致的,实施的具体差别在于,Python和JavaScript很类似,它允许你自由的定义变量。所以可以直接把变量定义在类上。 Ruby这边略显繁琐,繁琐的地方在于:

  1. 使用Module来管理类。你必须多一层保护的嵌套module
  2. 对外暴露只能是方法,所以我们在围绕 输入变量 ,生成定义方法。 当然这是有好处的,有一个抽象层意味着可以做很多事情。代价就是写代码繁琐。
  3. 主要是 class_eval 在 class 上下文中,还有方法绑定在单例类上,一切看起来很抽象。我觉得这是不友好的地方。

参考

发现了一些别人元编程的总结

Mark24

Everything can Mix.