使用 Ruby 編寫 DSL 語言
領(lǐng)域特定語言(英語:domain-specific language、DSL)指的是專注于某個(gè)應(yīng)用程序領(lǐng)域的計(jì)算機(jī)語言。又譯作領(lǐng)域?qū)S谜Z言。同名著作是 DSL 領(lǐng)域的豐碑之作,由世界級(jí)軟件開發(fā)大師和軟件開發(fā)“教父” Martin Fowler 歷時(shí)多年寫作。
Ruby 中很多的框架都采用了 DSL 語言的風(fēng)格,比如:Grape 和 Rspec。今天讓我們學(xué)習(xí)使用 Ruby 的語言來寫一下 DSL。
1. 編寫第一個(gè) DSL 語言
現(xiàn)在經(jīng)理給我們提了一個(gè)需求:讓我們監(jiān)聽?zhēng)讉€(gè)數(shù)據(jù):用戶成功創(chuàng)建訂單數(shù)、用戶成功付款訂單數(shù)、商家及時(shí)放貨的訂單數(shù)、等等幾十個(gè)事件,每天更新一次,后臺(tái)管理員可以從后臺(tái)看到這些數(shù)據(jù)。
我們理想中的 DSL 代碼格式應(yīng)該是這樣的:
listen "用戶成功創(chuàng)建訂單數(shù):" do
# 從數(shù)據(jù)庫獲取用戶今天創(chuàng)建的訂單數(shù)
# order = ...
# order.count
end
代碼塊(Block)中返回需要顯示的數(shù)量。
由此我們可以寫出這個(gè)代碼。
實(shí)例:
def listen description
puts "#{description}#{yield}" if yield
end
listen "用戶成功創(chuàng)建訂單數(shù):" do
300
end
listen "用戶成功付款訂單數(shù):" do
150
end
listen "商家及時(shí)放貨的訂單數(shù):" do
130
end
# ---- 輸出結(jié)果 ----
用戶成功創(chuàng)建訂單數(shù):300
用戶成功付款訂單數(shù):150
商家及時(shí)放貨的訂單數(shù):130
2. DSL中使用變量
2.1 定義常規(guī)變量
如果我們現(xiàn)在要在 DSL 中插入變量應(yīng)該怎們辦呢,比如增加一個(gè)必須大于 150 才通知的限制。在上一章節(jié)的作用域中我們可以學(xué)到,變量是可以在閉包外定義作用于閉包內(nèi)的,所以我們可以這樣的改動(dòng)代碼。
實(shí)例:
limit = 150
listen "用戶成功創(chuàng)建訂單數(shù):" do
order_count = 300
order_count > limit ? order_count: nil
end
listen "用戶成功付款訂單數(shù):" do
order_count = 150
order_count > limit ? order_count: nil
end
listen "商家及時(shí)放貨的訂單數(shù):" do
order_count = 130
order_count > limit ? order_count: nil
end
# ---- 輸出結(jié)果 ----
用戶成功創(chuàng)建訂單數(shù):300
2.2 變量集中初始化
但是如果我們需要增加很多種變量,這樣定義變量的方式會(huì)讓人感覺散亂又無序,一般DSL的語法會(huì)讓我們定義變量在一個(gè)專門定義變量的塊中,下面就是一個(gè)例子。
實(shí)例:
define do
@limit = 150
end
listen "用戶成功創(chuàng)建訂單數(shù):" do
order_count = 300
order_count > @limit ? order_count: nil
end
listen "用戶成功付款訂單數(shù):" do
order_count = 150
order_count > @limit ? order_count: nil
end
listen "商家及時(shí)放貨的訂單數(shù):" do
order_count = 130
order_count > @limit ? order_count: nil
end
我們將上述代碼制成一個(gè) event.rb,然后在實(shí)現(xiàn)代碼中進(jìn)行引用:
實(shí)例:
@defines = []
@listens = []
def define &block
@defines << block
end
def listen description, &block
@listens << {description: description, condition: block}
end
load 'event.rb'
@listens.each do |listen|
@defines.each do |define|
define.call
end
condition = listen[:condition].call
puts "#{listen[:description]}#{condition}" if condition
end
# ---- 輸出結(jié)果 ----
用戶成功創(chuàng)建訂單數(shù):300
Tips:load 方法會(huì)加載 event.rb 并執(zhí)行文件中的代碼。
解釋:
在實(shí)例中我們將塊yield
換成了&proc
的形式,我們定義了兩個(gè)處于頂級(jí)作用域中的變量:@defines
和@listens
,我們將每次從define
中定義的proc
對(duì)象都保存在了@defines
中,將所有監(jiān)聽的事件也都保存到了@listens
中,在后面的代碼里面,每一次我們處理listen
事件的時(shí)候都會(huì)運(yùn)行define
,這樣就完成了變量集中初始化。
2.3 消除事件之間共享頂級(jí)作用域變量
2.3.1 潔凈室
上述處理方法中,不同事件頂級(jí)實(shí)例變量會(huì)存在一個(gè)共享的問題,處理這個(gè)問題之前,首先讓我了解一下潔凈室(Clean Room)的概念。
潔凈室是一個(gè)用來執(zhí)行塊的環(huán)境。理想的潔凈室是不應(yīng)該有任何的方法以及實(shí)例變量的,所以不會(huì)產(chǎn)生任何方法名或者變量名的沖突。因此BasicObject
和Object
往往被用來充當(dāng)潔凈室。
實(shí)例:
obj = Object.new
obj.instance_eval do
@a = 1
@b = 2
@c = 3
end
obj.instance_eval do
puts "@a == #{@a}"
puts "@b == #{@b}"
puts "@c == #{@c}"
puts "sum == #{@a + @b + @c}"
end
# ---- 輸出結(jié)果 ----
@a == 1
@b == 2
@c == 3
sum == 6
解釋:
讓我們創(chuàng)建一個(gè)Object
的實(shí)例作為潔凈室,使用instance_eval
在潔凈室第一個(gè)塊中里面定義三個(gè)變量,在第二個(gè)塊中定義4個(gè)方法。因?yàn)樗麄兊淖饔糜蚴沁@個(gè)潔凈室的實(shí)例,所以會(huì)得到最后的輸出結(jié)果。
2.3.2 用潔凈室來處理上述問題
讓我們使用潔凈室處理剛剛頂級(jí)作用域的問題。
先修改一下event.rb。
define do
@limit = 150
end
listen "用戶成功創(chuàng)建訂單數(shù):" do
@num = 1
puts "@num1 == #{@num}"
order_count = 300
order_count > @limit ? order_count: nil
end
listen "用戶成功付款訂單數(shù):" do
@num = @num.to_i + 1
puts "@num2 == #{@num}"
order_count = 150
order_count > @limit ? order_count: nil
end
listen "商家及時(shí)放貨的訂單數(shù):" do
@num = @num.to_i + 1
puts "@num3 == #{@num}"
order_count = 130
order_count > @limit ? order_count: nil
end
重新運(yùn)行一下腳本,得到結(jié)果:
@num1 == 1
用戶成功創(chuàng)建訂單數(shù):300
@num2 == 2
@num3 == 3
每一個(gè)事件之間應(yīng)該是獨(dú)立的,不應(yīng)該共享不必要的變量,為此,我們使用潔凈室修改一下實(shí)現(xiàn)代碼。
實(shí)例:
@defines = []
@listens = []
def define &block
@defines << block
end
def listen description, &block
@listens << {description: description, condition: block}
end
load 'event.rb'
@listens.each do |listen|
env = Object.new
@defines.each do |define|
env.instance_eval &define
end
condition = env.instance_eval &listen[:condition]
puts "#{listen[:description]}#{condition}" if condition
end
# ---- 輸出結(jié)果 ----
@num1 == 1
用戶成功創(chuàng)建訂單數(shù):300
@num2 == 1
@num3 == 1
解釋:
我們?cè)谥暗幕A(chǔ)上,每一次定義事件的時(shí)候創(chuàng)建了一個(gè)潔凈室,這樣實(shí)例變量的作用范圍就從頂級(jí)作用域變?yōu)榱藵崈羰覍?duì)象之中,事件之間就不會(huì)存在共享變量的情況了。
3. 小結(jié)
本章節(jié)中我們學(xué)習(xí)到了如何使用 Ruby 去寫一個(gè) DSL 語言,了解了 DSL 語言中使用不同變量的方法,學(xué)習(xí)了潔凈室的使用方法。