在編寫程序時(shí),我給自己設(shè)立這樣的一個(gè)限制: 所有的程序都只可以編寫一次,當(dāng)你認(rèn)為程序?qū)懲瓴⑦\(yùn)行后,便不能再次修改并重啟了,然后,程序要盡可能對(duì)需求的擴(kuò)展做出正確的回應(yīng)。
場(chǎng)景1:
需要編寫一個(gè)名為 Config 的類,通過傳入 Hash 對(duì)象來實(shí)例化,傳入的 Hash 中規(guī)定了兩個(gè)鍵值對(duì)來代表程序所用的時(shí)間和空間復(fù)雜度。并且,實(shí)例可以用 "點(diǎn)" 的方式調(diào)用 Hash 中的值,即:
config = Config.new({time: "O(1)", "space" => "O(N)"})
config.time # => "O(1)"
config.space # => "O(N)"
如果用靜態(tài)的眼光來考慮這個(gè)問題,可以把 Config 寫成這樣:
- 方案一
class Config
attr_reader :time, :space
def initialize(hash)
@time = hash[:time] || hash['time']
@space = hash[:space] || hash['space']
end
end
這樣做當(dāng)然沒問題,但假如這時(shí) config 要增加一個(gè)名為 version 的屬性來存儲(chǔ)語言的版本,依照上述這種方法就得在 attr_accessor 后加上 :version, 對(duì)應(yīng)的初始化方法再加上一行。如果再有更多的新屬性要添加,那就要不停地重復(fù)這樣的過程。需要注意的是,每次執(zhí)行這個(gè)過程程序是需要被重啟的,所以這種方案不符合我們的編寫目標(biāo),當(dāng)然,也不符合 DRY 的原則。
使用 method_missing 來實(shí)現(xiàn)。
- 方案二
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
這段代碼也達(dá)成了場(chǎng)景1的需求,而在屬性值增長時(shí),使用 method_missing 代碼量始終可以維持不變,并且,在這一過程中,程序可以保持不重啟。
相對(duì)于 method_missing 在 Ruby 的名氣, const_missing 這個(gè)方法就顯得默默無聞了,當(dāng)然也因?yàn)槭褂玫膱?chǎng)景的確不多。這個(gè)方法是在當(dāng)前命名空間找不到對(duì)應(yīng)的常量名時(shí)會(huì)觸發(fā)的hook 方法,一般來說,若沒有做任何處理,解釋器便會(huì)返回 uninitialized constant,如:
module Asd
A = 1
class C
end
end
Asd::A # => 1
Asd::C # => Asd::C
Asd::B #= uninitialized constant Asd::B
通過覆寫對(duì)應(yīng)命名空間的 const_missing 方法便可以對(duì)不存在的常量進(jìn)行操作,比如在文件變動(dòng)時(shí),通過 load 新文件來加載新的類(只是我這么用過)。
但 missing 方法其實(shí)不僅僅是方法,我認(rèn)為也是一種理念,就是用發(fā)展的眼光來看待程序,對(duì)未發(fā)生但可能發(fā)生的事件做統(tǒng)一的處理,以不變應(yīng)萬變。
場(chǎng)景2 :
編寫一個(gè) HTTP 的 API,使得 '.../xx/a' 作為客戶a提交的地址, '..../xx/b' 作為客戶b提交的地址(假設(shè)無法規(guī)定客戶提交的參數(shù)所以如此設(shè)計(jì))。
方案一, 依然先以只解決現(xiàn)有問題的靜態(tài)策略寫出這個(gè) 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 都會(huì)考慮這樣的設(shè)計(jì): 寫好特定的路由給與調(diào)用,否則的話就返回 404。但在這個(gè)場(chǎng)景中,有個(gè)潛在的需求,客戶(即a,b)的數(shù)量并不是不變的,可能會(huì)增加也會(huì)減少,而我們希望程序啟動(dòng)一次后就能適應(yīng)這些改變,該怎么做呢?
不妨按照上文中的 missing 理念,在找不到路由的時(shí)候去動(dòng)態(tài)的生成路由。而在這邊代碼中所謂的“找路由”,其實(shí)就是匹配 req.path_info 而已。我們可以在數(shù)據(jù)庫存儲(chǔ)每個(gè)客戶提交的路由地址,通過每次調(diào)用得到的 path_info, 尋找對(duì)應(yīng)的客戶是否存在,若存在,就可以給與對(duì)應(yīng)的響應(yīng)。
方案二:
class MyApi
# 數(shù)據(jù)庫連接
DB.connect!
def call(env)
# 假設(shè)使用了 ActiveRecord 并建立了 Customer 的模型
customer = Customer.find_by(path: req.path_info)
# 返回的內(nèi)容都可以在數(shù)據(jù)庫讀取,這樣更加靈活
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
這樣,現(xiàn)在這個(gè) API 便可以根據(jù)數(shù)據(jù)庫中客戶的信息‘動(dòng)態(tài)的產(chǎn)生路由’了。順便提及一下,使用 Grape 框架應(yīng)該怎么做到這點(diǎn),當(dāng)然思路還是一樣的,我們需要覆寫捕獲找不到路由的方法
# Rescue 404 Route In Grape
route :any, '*path' do
# do anything by req.path, database, etc..
end
結(jié)語: 對(duì)缺失的定義,可以很大程度提高程序、系統(tǒng)的適應(yīng)能力,減少代碼的數(shù)量。不僅只用在元編程中,在系統(tǒng)的各個(gè)環(huán)節(jié)都應(yīng)引入這種思想。