使用 Ruby 編寫(xiě) DSL 語(yǔ)言
領(lǐng)域特定語(yǔ)言(英語(yǔ):domain-specific language、DSL)指的是專注于某個(gè)應(yīng)用程序領(lǐng)域的計(jì)算機(jī)語(yǔ)言。又譯作領(lǐng)域?qū)S谜Z(yǔ)言。同名著作是 DSL 領(lǐng)域的豐碑之作,由世界級(jí)軟件開(kāi)發(fā)大師和軟件開(kāi)發(fā)“教父” Martin Fowler 歷時(shí)多年寫(xiě)作。
Ruby 中很多的框架都采用了 DSL 語(yǔ)言的風(fēng)格,比如:Grape 和 Rspec。今天讓我們學(xué)習(xí)使用 Ruby 的語(yǔ)言來(lái)寫(xiě)一下 DSL。
1. 編寫(xiě)第一個(gè) DSL 語(yǔ)言
現(xiàn)在經(jīng)理給我們提了一個(gè)需求:讓我們監(jiān)聽(tīng)?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ù)庫(kù)獲取用戶今天創(chuàng)建的訂單數(shù)
# order = ...
# order.count
end
代碼塊(Block)中返回需要顯示的數(shù)量。
由此我們可以寫(xiě)出這個(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ì)讓人感覺(jué)散亂又無(wú)序,一般DSL的語(yǔ)法會(huì)讓我們定義變量在一個(gè)專門(mén)定義變量的塊中,下面就是一個(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)聽(tīng)的事件也都保存到了@listens中,在后面的代碼里面,每一次我們處理listen事件的時(shí)候都會(huì)運(yùn)行define,這樣就完成了變量集中初始化。
2.3 消除事件之間共享頂級(jí)作用域變量
2.3.1 潔凈室
上述處理方法中,不同事件頂級(jí)實(shí)例變量會(huì)存在一個(gè)共享的問(wèn)題,處理這個(gè)問(wèn)題之前,首先讓我了解一下潔凈室(Clean Room)的概念。
潔凈室是一個(gè)用來(lái)執(zhí)行塊的環(huán)境。理想的潔凈室是不應(yīng)該有任何的方法以及實(shí)例變量的,所以不會(huì)產(chǎn)生任何方法名或者變量名的沖突。因此BasicObject和Object往往被用來(lái)充當(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 用潔凈室來(lái)處理上述問(wèn)題
讓我們使用潔凈室處理剛剛頂級(jí)作用域的問(wèn)題。
先修改一下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 去寫(xiě)一個(gè) DSL 語(yǔ)言,了解了 DSL 語(yǔ)言中使用不同變量的方法,學(xué)習(xí)了潔凈室的使用方法。
杜驍 ·
2025 imooc.com All Rights Reserved |