最后一個優化是重命名酸生成器的方法,以讓它們更加易讀。生成器的存在使得我們不需要擔心底層領域對象的命名沖突。
為每一個領域對象創建一個生成器并不是表達式生成器唯一的使用方法。另一種思路是為所有領域對象創建一個生成器對象。以下就是為上面同樣的DSL編寫的新生成器。
下載 lairs/builder13.rb
def self.item arg
result = self.new
result.item arg
return result
end
def initialize
@subject = Configuration.new
end
def item arg
@current_item = Item.new(arg)
@subject.add_item @current_item
return self
end
這段代碼不再每次都創建一個新對象,而是用一個上下文變量來跟蹤當前使用的物件。這也意味著我們不再需要定義父生成器的向前跟蹤方法。
更多方法鏈
方法鏈是一個好工具,但是否可以在所有地方都使用它呢?我們是否可以去掉資源工廠呢?事實上的確可以,去掉之后DSL代碼就會變成下面這樣。
下載 lairs/rules2.rb
ConfigurationBuilder.
item(:secure_air_vent).
item(:acid_bath).
uses.acid.
type(:hcl).
grade(5).
uses.electricity(12).
item(:camera).uses.electricity(1).
item(:small_power_plant).
provides.electricity(11).
depends_on(:secure_air_vent)
(請注意我在這里添加了一些空行來提高Ruby代碼的可讀性。)
使用方法鏈還是參數是一個時常碰到的問題。當參數是直接量的時候,比如grade(5),那么使用方法鏈就過于復雜。在復雜和簡單之間,我傾向于后者,這是個顯而易見的選擇。難以選擇的時候是在碰到如uses.electricity...和uses(Resources.electricity...這樣的代碼時。
隨著方法鏈的增多,生成器的復雜度也隨之增加。一個很好的例子是:在引入附屬對象后生成器的復雜性急速增加。資源在兩種情況中使用,跟隨著uses或者是跟隨著provides。因此,如果要使用方法鏈,就需要跟蹤資源使用的環境,才能正確地響應對electricity的調用。
另一方面,使用參數會使我們失去方法鏈所帶來的對作用域的控制,所以在參數創建時需要提供作用域控制——此例使用的是工廠提供的類方法。同時,引用工廠名也是我樂意去避免的不斷重復的麻煩事之一。
引入參數引起的另外一個問題是: DSL編寫者經常需要在使用何種方法之間進行抉擇,這會使DSL的編寫變得困難。
由于在這方面經驗不夠,我也無法給你提供一個明確的建議。當然我覺得首選是使用方法鏈,因為作為一種技術它擁有很多的支持。但在使用時需要注意使用方法鏈所帶來的復雜性。一旦生成器的實現開始變得混亂(我知道這是一個含糊的說辭),就使用參數。隨后我就會介紹一些技術來避免引入參數時帶來的類工廠重復問題,但那取決于我們使用的是何種宿主語言。
3.4 使用閉包
閉包是語言中一個越來越常見的特性,特別是在一些支持內部DSL的動態語言中。閉包給一個等級式結構引入了一個新的作用域上下文,這對于DSL特別適合。下面就是使用了閉包的“巢穴”程序示例。
下載 lairs/rules3.rb
ConfigurationBuilder.start do |config|
config.item :secure_air_vent
config.item(:acid_bath) do |item|
item.uses(Resources.acid) do |acid|
acid.type = :hcl
acid.grade = 5
end
item.uses(Resources.electricity(12))
end
config.item(:camera) do |item|
item.uses(Resources.electricity(1))
end
config.item(:small_power_plant) do |item|
item.provides(Resources.electricity(11))
item.depends_on(:secure_air_vent)
end
end
此例放棄了方法鏈的使用,而且每個方法都有一個清晰的接收者。接收者通過宿主語言的閉包語法來設定。DSL中往往會有等級式結構,這使方法調用的嵌套變得更加簡單。
DSL代碼需要的嵌套布局正好由宿主語言的嵌套結構所提供,這使代碼的布置更加簡單。另外,變量(比如item和acid)的作用域也被限制在宿主語言的塊中。
顯式方法接收者的使用代表方法鏈的多余。這也意味著當領域對象本身的API已經具有這個功能時,生成器也是多余的。在這里,我們仍對item使用生成器,但對acid使用真實的領域對象。
使用這項技術的一個限制是它需要宿主語言提供對閉包功能的支持。雖然也可以使用臨時變量模擬,但臨時變量帶來的問題是它們不能很好地控制作用域,除非你提供一個額外的作用域機制。無論有沒有額外的作用域控制,代碼都不會如DSL那么流暢,而且容易出錯。閉包通過綁定作用域和定義變量很好地避免了這個問題。
3.5 執行上下文
到目前為止,我們還沒有談到DSL代碼的執行上下文。如果沒有定義執行上下文,談論一個沒有接收者的函數調用和數據項就完全沒有意義。之前我們假設執行上下文是全局的,比如函數foo()就被假設為一個全局函數。我們已經談到過如何使用方法鏈和類方法在其他作用域中調用函數,但我們仍然能夠改變整個DSL程序的上下文。
提供執行上下文最簡單的方法是把DSL代碼嵌入一個類中,這樣DSL代碼就可以使用類的其他方法和字段(field)。在支持開放類的語言中,可以通過直接打開一個類來實現;而在其他語言中,需要定義一個子類來實現。
下面就是一個定義子類的例子。
下載 lairs/rules17.rb
class PrimaryConfigurationRules < ConfigurationBuilder
原文轉自:http://www.ituring.com.cn/article/17818