在编写程序时,我给自己设立这样的一个限制: 所有的程序都只可以编写一次,当你认为程序写完并运行后,便不能再次修改并重启了,然后,程序要尽可能对需求的扩展做出正确的回应。
场景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
结语: 对缺失的定义,可以很大程度提高程序、系统的适应能力,减少代码的数量。不仅只用在元编程中,在系统的各个环节都应引入这种思想。
网友评论