Sinatra源码分析(一):set系统工作原理

前言

大家好,我是MARK24 。可以叫我 MARK。这是我研究Sinatra的笔记。

阅读过程大约 10 分钟。

基于 Sinatra 2.1.0 进行讨论

Sinatra v2.1.0 Fork代码地址

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 开始发挥逻辑上的作用。

Mark24

Everything can Mix.