Mark24
Sinatra源码分析(一):set系统工作原理
前言
大家好,我是MARK24 。可以叫我 MARK。这是我研究Sinatra的笔记。
阅读过程大约 10 分钟。
基于 Sinatra 2.1.0 进行讨论
Sinatra set 介绍
set 系统可以让Sinatra在自身自由的定义 设置相关的变量。
比如定义模板所在:
set :views, settings.root + '/templates'
定义 session secret:
set :session_secret, ENV.fetch('SESSION_SECRET') { SecureRandom.hex(64) }
等等,非常自由且灵活。
set 系统这部分的源码恰巧是可以简单修改之后独立工作的。摘要如下:
# https://github.com/Mark24Code/sinatra-code-review/blob/master/lib/sinatra/base.rb#L1267
def define_singleton(name, content = Proc.new)
singleton_class.class_eval do
undef_method(name) if method_defined? name
String === content ? class_eval("def #{name}() #{content}; end") : define_method(name, &content)
end
end
def set(option, value = (not_set = true), ignore_setter = false, &block)
raise ArgumentError if block and !not_set
value, not_set = block, false if block
if not_set
raise ArgumentError unless option.respond_to?(:each)
option.each { |k,v| set(k, v) }
return self
end
if respond_to?("#{option}=") and not ignore_setter
return __send__("#{option}=", value)
end
setter = proc { |val| set option, val, true }
getter = proc { value }
case value
when Proc
getter = value
when Symbol, Integer, FalseClass, TrueClass, NilClass
getter = value.inspect
when Hash
setter = proc do |val|
val = value.merge val if Hash === val
set option, val, true
end
end
define_singleton("#{option}=", setter)
define_singleton(option, getter)
# 原始代码放在一个类中, 如果我们想放在单文件执行,需要 改写为 `self.class.method_defined?` 调用到方法
# define_singleton("#{option}?", "!!#{option}") unless method_defined? "#{option}?"
define_singleton("#{option}?", "!!#{option}") unless self.class.method_defined? "#{option}?"
self
end
set源码分析
Sinatra 内部实现了一套 配置系统,基于一个 DSL 语法 set。 这是Sinatra Class部分初始化之后唯一的初始化的DSL。Sinatra没有做很多复杂的前置工作。
一致很让我疑惑的是,这里的 getter、setter。我们传统理解的 是这样工作的:
class Sample
def initialize()
@name
end
# getter
def name
@name
end
# setter
def name=(new_name)
@name = new_name
end
end
但是Sinatra这里似乎是一个循环一样的,你会发现他的 setter 永远在调用 set 这是为什么呢?我一度非常迷惑。
setter = proc { |val| set option, val, true }
我刚开始进入这段是百思不得其解。但事实证明我格局小了。这部分其实根本不是传统的 setter, 我们观察传统的 setter 他的问题是必须要以一个实例变量为依托。所以他才必须写成这样。 如果是下面这样呢?
class Sample
# getter
def name
"new value"
end
# setter
def name=(new_name)
# setter 的逻辑,就是覆盖式定义一个 新的 直接返回新值的 getter
re_define_name_getter(new_name)
end
end
直接伪代码,我们每次调用 setter, setter的任务不是去修改一个 中间值,而是每次去重新定义 新的 getter方法,定义的时候就塞入新的值。
这样依然保持了 getter 的功能!豁然开朗!
Sinatra的 set系统就是这样工作的,不论是 set 函数定义本身,还是 set 内部调用的 set,还是用户最终在外部书写 set xxx, new_value
最终殊途同归的进入 set option, value, true
然后都会走到最后一部分,重新定义三个方法。
define_singleton("#{option}=", setter)
define_singleton(option, getter)
define_singleton("#{option}?", "!!#{option}") unless method_defined? "#{option}?"
setter方法的作用就是调用set自身,这样只要被调用,时间上可以完成了一种循环。闭环调用(原谅我用了闭环这个词 :P) getter方法 是以新的值直接返回,respond_to 方法同理,以新值计算返回。
补充
1.定义处有趣的写法 value = (not_set = true)
def set(option, value = (not_set = true), ignore_setter = false, &block)
# ....
end
可以通过实验证实这种写法的特点是:
如果 value 赋值 比如是 99,那么 value = 99, not_set = nil
如果 value 没有赋值, 那么 value = not_set = true
这里主要是没有赋值,not_set 开始发挥逻辑上的作用。