Mark24
记录灵感、技术、思考
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
主要的思想就是:
- 通过set方法,根据输入 的 keyname、value 动态的在 class上,生成类似静态的方法的 类方法、亦或是 单例方法。这两个概念在class上是一致的。
- 使用 configure 方法,对外提供一个接口,接口内部的block 执行在 class 的上下文中,可以使用 set 对 class 进行动态定义
- 使用继承,以及搜索链条,可以让配置呈现出继承关系。
我在 Python的 Flask框架中看到过类似的配置。思想是一致的,实施的具体差别在于,Python和JavaScript很类似,它允许你自由的定义变量。所以可以直接把变量定义在类上。 Ruby这边略显繁琐,繁琐的地方在于:
- 使用Module来管理类。你必须多一层保护的嵌套module
- 对外暴露只能是方法,所以我们在围绕 输入变量 ,生成定义方法。 当然这是有好处的,有一个抽象层意味着可以做很多事情。代价就是写代码繁琐。
- 主要是 class_eval 在 class 上下文中,还有方法绑定在单例类上,一切看起来很抽象。我觉得这是不友好的地方。
参考
发现了一些别人元编程的总结