美文网首页Ruby
Define the Missing

Define the Missing

作者: 就这个皂倍儿爽 | 来源:发表于2017-10-03 13:43 被阅读3次

    在编写程序时,我给自己设立这样的一个限制: 所有的程序都只可以编写一次,当你认为程序写完并运行后,便不能再次修改并重启了,然后,程序要尽可能对需求的扩展做出正确的回应。

    场景1:

    需要编写一个名为 Config 的类,通过传入 Hash 对象来实例化,传入的 Hash 中规定了两个键值对来代表程序所用的时间和空间复杂度。并且,实例可以用 "点" 的方式调用 Hash 中的值,即:

    config = Config.new({time: "O(1)",  "space" => "O(N)"})
    config.time # => "O(1)"
    config.space # => "O(N)"
    

    如果用静态的眼光来考虑这个问题,可以把 Config 写成这样:

    • 方案一
    class Config
      attr_reader :time, :space
      def initialize(hash)
        @time = hash[:time] || hash['time']
        @space = hash[:space] || hash['space']
      end
    end
    

    这样做当然没问题,但假如这时 config 要增加一个名为 version 的属性来存储语言的版本,依照上述这种方法就得在 attr_accessor 后加上 :version, 对应的初始化方法再加上一行。如果再有更多的新属性要添加,那就要不停地重复这样的过程。需要注意的是,每次执行这个过程程序是需要被重启的,所以这种方案不符合我们的编写目标,当然,也不符合 DRY 的原则。

    使用 method_missing 来实现。

    • 方案二
    class Config
      attr_reader :hash_data
    
      def initialize(hash={})
        @hash_data = hash
      end
    
      def method_missing(method)
        # 可以通过正则检查方法名称是否携带 '=' 来生成 set 方法 
        # 本处只演示 get 方法
        hash_data[method.to_s] || hash_data[method]
      end
    end
    

    这段代码也达成了场景1的需求,而在属性值增长时,使用 method_missing 代码量始终可以维持不变,并且,在这一过程中,程序可以保持不重启。

    相对于 method_missing 在 Ruby 的名气, const_missing 这个方法就显得默默无闻了,当然也因为使用的场景的确不多。这个方法是在当前命名空间找不到对应的常量名时会触发的hook 方法,一般来说,若没有做任何处理,解释器便会返回 uninitialized constant,如:

    module Asd
      A = 1
      class C
      end
    end
    Asd::A # => 1
    Asd::C # => Asd::C
    Asd::B #= uninitialized constant Asd::B
    

    通过覆写对应命名空间的 const_missing 方法便可以对不存在的常量进行操作,比如在文件变动时,通过 load 新文件来加载新的类(只是我这么用过)。

    但 missing 方法其实不仅仅是方法,我认为也是一种理念,就是用发展的眼光来看待程序,对未发生但可能发生的事件做统一的处理,以不变应万变。

    场景2 :

    编写一个 HTTP 的 API,使得 '.../xx/a' 作为客户a提交的地址, '..../xx/b' 作为客户b提交的地址(假设无法规定客户提交的参数所以如此设计)。

    方案一, 依然先以只解决现有问题的静态策略写出这个 API :

    # Use Rack
    class MyApi
      def call(env)
        req = Rack::Request.new(env)
        case req.path_info
        when '/xx/a'
          [200, {"Content-Type" => "text/html"}, ["Hello a!"]]
        when '/xx/b'
          [200, {"Content-Type" => "text/html"}, ["Hello b!"]]
        else
          [404, {"Content-Type" => "text/html"}, ["Can't find!"]]
        end
      end
    end
    
    run MyApi.new
    

    大多 API 都会考虑这样的设计: 写好特定的路由给与调用,否则的话就返回 404。但在这个场景中,有个潜在的需求,客户(即a,b)的数量并不是不变的,可能会增加也会减少,而我们希望程序启动一次后就能适应这些改变,该怎么做呢?

    不妨按照上文中的 missing 理念,在找不到路由的时候去动态的生成路由。而在这边代码中所谓的“找路由”,其实就是匹配 req.path_info 而已。我们可以在数据库存储每个客户提交的路由地址,通过每次调用得到的 path_info, 寻找对应的客户是否存在,若存在,就可以给与对应的响应。

    方案二:

    class MyApi
      # 数据库连接
      DB.connect! 
      def call(env)
        # 假设使用了 ActiveRecord 并建立了 Customer 的模型
        customer = Customer.find_by(path: req.path_info) 
    
       # 返回的内容都可以在数据库读取,这样更加灵活
        if customer
         [200, {"Content-Type" => customer.content_type}, [customer.response]]
        else
         [404, {"Content-Type" => "text/html"}, ["Can't find!"]]
        end
      end
    end
    
    run MyApi.new
    

    这样,现在这个 API 便可以根据数据库中客户的信息‘动态的产生路由’了。顺便提及一下,使用 Grape 框架应该怎么做到这点,当然思路还是一样的,我们需要覆写捕获找不到路由的方法

    # Rescue 404 Route In Grape 
    route :any, '*path' do
      # do anything by req.path, database, etc..
    end
    

    结语: 对缺失的定义,可以很大程度提高程序、系统的适应能力,减少代码的数量。不仅只用在元编程中,在系统的各个环节都应引入这种思想。

    相关文章

      网友评论

        本文标题:Define the Missing

        本文链接:https://www.haomeiwen.com/subject/biskjxtx.html