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