程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 跨越邊界: 活動記錄和Java編程中特定於域的語言

跨越邊界: 活動記錄和Java編程中特定於域的語言

編輯:關於JAVA

DSL 是專門解決特定於域問題的語言。通過更接近問題的操作,DSL 可以提供在通用語言中可能找不 到的好處。Java 世界中充滿了 DSL。屬性文件、Spring 上下文、標注的某種用法以及 Ant 任務,都是 DSL 的示例。

在開始研究其他像 Ruby 這樣的語言的時候,我開始理解到 Java 語言目前對於 DSL 還沒有良好的把 握。在這篇文章中,將看到 Ruby 使用的四種集成干淨的 DSL 的技巧。然後,將看到在 Java 語言中可 能存在的選項是什麼。

隱藏語言的世界

雖然您可能不知道,但實際上您無處不遇到 DSL,從日常生活到使用的應用程序,到您編寫的程序。 在法庭上,可以看到速記員用 DSL 迅速地進行記錄。音樂使用幾種不同的標注來描述音量、音調和每個 音的時長,采用一種適合特定樂器的格式。(我使用吉它六線譜,裡面每條線都代表吉它上的一根弦。) 使用 DSL 是因為它們比口述或筆錄更能有效地解決問題。

在使用日常的應用程序時,也在使用 DSL。最好的示例是電子表格。編寫電子表格,要比使用最簡單 的會計程序還要容易。電子表格的 DSL 從根本上改變了為特定問題進行編程的實質。

Java 編程中的 DSL

回頭來看,Java 也在到處使用 DSL:

JSP 使得構建定制的用戶界面更容易。

SQL 代表數據庫操作。

屬性文件代表程序的配置。

XML 描述數據。

XML 描述程序配置,例如在 EJB、Hibernate 或 Spring 中。

XML 描述動作,例如 Ant 任務或某種引擎中的業務規則。

Java 語言並不特別擅長特定於域的語言,因為這個語言很難按照對 DSL 開發人員來說最有吸引力的 方式進行擴展。這就是為什麼 XML 這麼泛濫的一個原因。XML 是可擴展的,Java 和它的集成很好,可以 容易地構建解釋它的工具,而且它也不需要和 Java 類一起編譯。但是 XML 對於人類閱讀來說很不友好 。所以,可以看到對於在 Java 語言中 XML 的過度使用有廣泛的抱怨。

使用 Ruby 和活動記錄的 DSL

在跨越邊界 系列的 第一篇文章 中,您看到了活動記錄(Ruby on Rails 背後的持久化引擎)。在這 篇文章中,我又回到活動記錄,因為它在多個地方對 DSL 概念進行了精彩的應用:

特定於域的語句結構和詞匯表。 活動記錄構建了一個用 Ruby 對象包裝關系數據庫的詞匯表。例如, 在數據庫支持的對象中,可以用 has_many :people 來構建與另一個數據庫支持的對象的一對多關系映射 。

擴展類的行為。 根據命名規范,聲明叫作 People 的活動記錄類,就會擁有與數據庫中每個列對應的 屬性。

修飾現有類型。 Rails 通常修飾 Fixnum 這樣的類以提供對域友好的體驗。

動態地擴展詞匯表。 活動記錄提供了一些驚喜,例如根據數據庫的結構添加定制查找器。

英語建模。 活動記錄根據上下文修改類的復數形式。

隨著繼續閱讀本文,將看到讓這些技巧成為可能的 Ruby 特性。您將真正體會到在 Ruby 和 Java 操 作方式之間的區別。要跟隨本文一起編寫代碼,需要安裝 Ruby 和 Ruby on Rails,其中包含了活動記錄 (請參閱 參考資料)。

Ruby 中的詞匯表

Ruby 語法開放的結構和符號的包含,使得定義詞匯相當容易。可以使用方法、符號和類來形成詞匯。 請輸入 irb 來啟動 Ruby 解釋器。輸入清單 1 中的代碼。(清單 1 顯示了輸入的內容和 Ruby 中的結 果。只需要輸入黑體的代碼。)

清單 1. 創建 Ruby 類

irb(main):001:0> class Person
irb(main):002:1> attr_accessor :name, :email
irb(main):003:1> end
=> nil
irb(main):004:0> person = Person.new
=> #<Person:0x2b61a80>
irb(main):005:0> person.name = "Elvis"
=> "Elvis"
irb(main):006:0>

在清單 1 中,創建了叫作 Person 的類,它有兩個實例變量分別叫作 name 和 email。請特別注意 attr_accessor :name, :email 這一行。有兩個概念應當引起注意:

類定義中的方法調用

符號的使用

方法調用

清單 1 中的 attr_accessor :name, :email 語句創建兩個屬性,分別帶有 getter 和 setter 存取 器。attr 實際上是個方法調用 —— 是 Ruby 語言本身元編程的精彩示例。Java 開發人員習慣於在類體 中看到方法聲明,而不習慣看到方法調用。這個方法調用把方法和實例變量添加到 Person 類中。

如果沒有 attr_accessor :name, :email,就必須為每個需要的屬性輸入清單 2 的代碼:

清單 2. Ruby 存取器

def name=(value)
@name = value
end
def name
return @name
end

清單 2 —— Ruby 版的 getter 和 setter —— 看起來應當有點兒熟悉。name= 實際上是個方法名 稱,而 @ 加在所有實例變量前作為前綴,但剩下的就與 Java 的 getter 和 setter 很類似了。

如果不用清單 2 中的代碼,也可以用 @attr 的另一個版本來創建帶有 getter、setter 或兩者都有 的屬性。

符號

第二個值得注意的概念是符號。可以把 :email 當成名為 email 的東西。Ruby 符號像字符串,但是 是不可修改的字符串,而且只有一個實例。只能使用一個 :email 符號。

現在看起來像下面這樣的活動記錄代碼應當讓您有點兒感覺了:

class Manager < ActiveRecord::Base
has_one :department
end

has_one 是個方法,:department 是個符號,活動記錄只是把它解釋成類的名稱。因為 Ruby 並不強 制要求在方法參數兩邊使用括號,還因為 Rails 可以使用專門為活動記錄設計的符號和方法名稱,所以 這個詞匯暢通無阻。

可選的擴展

活動記錄充分利用了 Ruby 的另一個特性。會經常看到帶有可選參數的 Ruby 方法,可選參數是一個 默認為空的哈希 map。可以用這種方式模擬命名參數。例如,活動記錄方法 belongs_to 的定義看起來像 這樣:

def belongs_to(association_id, options = {})

現在可以把選項傳遞給 belongs_to 來優化它的行為:

class Manager < ActiveRecord::Base
has_one :department, :foreign_key => "department_number"
end

在 Ruby 中,用 key => value 指定哈希 map 的條目。意思很清楚:想讓活動記錄覆蓋默認值 (department_id,根據命名規范)而采用 department_number。因為可以修剪選項的名稱來滿足語法的 要求,所以 DSL 就得到了另一個強大的特性:可選的擴展。下面需要的能力是用自己的詞匯來擴展 Ruby 語言。

修飾現有類型

Ruby 是種動態語言,所以向現有類(甚至指定類的實例)添加行為很容易。現在先使用這項技術來針 對某個域修飾現有類,然後再根據詞匯擴展現有類。

羅馬數字的使用不太頻繁,但是在某些上下文中會有用。我們並不想直接把羅馬數字添加到 Ruby 的 Fixnum 基類,但是它們對於特定於域的語言可能是有用的。可以把 to_roman 方法添加到 Fixnum 類, 這個方法把 fixnum 轉換成羅馬數字。這件事做起來極為容易。只要再次打開類定義,並定義新方法即可 。清單 3 顯示了一個粗糙的羅馬數字處理方法:

清單 3. 羅馬數字處理方法

class Fixnum
def to_roman
  value = self
  str = ""
  (str << "C"; value = value - 100) while (value >= 100)
  (str << "XC"; value = value - 90) while (value >= 90)
  (str << "L"; value = value - 50) while (value >= 50)
  (str << "XL"; value = value - 40) while (value >= 40)
  (str << "X"; value = value - 10) while (value >= 10)
  (str << "IX"; value = value - 9) while (value >= 9)
  (str << "V"; value = value - 5) while (value >= 5)
  (str << "IV"; value = value - 4) while (value >= 4)
  (str << "I"; value = value - 1) while (value >= 1)
  str
end
end

一旦理解了分號分隔了兩個不同的 Ruby 語句,清單 3 就簡單了。當我想讓兩個不同的想法掛在一起 的時候,就經常用這種方式。可以用這項技術添加或修改任何 Ruby 類的定義。這一特殊實現的好處在於 使用模型。可以把它粘貼到一個文件中,並在 Ruby 解釋器中使用它,如清單 4 所示:

清單 4. 使用 to_roman 擴展

irb(main):001:0> load 'to_roman.rb'
=> true
irb(main):002:0> 10.to_roman
=> "X"
irb(main):003:0> 199.to_roman
=> "CXCIX"
irb(main):004:0>

Rails 利用這個能力處理像時間測量之類的事情。例如,在 Rails 應用程序中,可以說 10.days , 或 2.hours.ago,或 5.minutes.from_now。使用這個技術,可以把現有 Ruby 詞匯擴展到自己的域中, 處理類似測量、轉換或其他語法組合的事情。最終結果是一個干淨漂亮的 Ruby 核心類,帶有一些擴展, 提供特定於域的類,可以在域的上下文中做正確的事。

動態地構建類

在得到了詞匯和擴展類的能力之後,下一步是根據詞匯動態地 擴展類。在 清單 1 中的 attr 就是這 種技術的示例。現在將介紹如何實現它(感謝 Glenn Vanderburg;請參閱 參考資料)。清單 5 顯示了 初步的嘗試:

清單 5. 動態擴展類的初步嘗試

class Person
def my_attr
  self.class.class_eval "def name; @name; end"
  self.class.class_eval "def name=(val); @name = val; end"
end
end

這個示例稍微復雜了一些。self.class 返回 Person 的類。然後 class_eval 在這個類的上下文環境 下計算以下字符串。第一行定義 getter,第二行定義 setter。這個示例把 name 屬性添加到 Person。

清單 5 有兩個主要問題。首先,需要顯式地調用 my_attr。還不能從類中調用它,因為它還沒有定義 。其次,硬編碼的 name 應當是個符號。第一個問題可以通過聲明一個模塊並從這個模塊進行繼承來解決 。第二個問題可以通過傳遞進一個符號來解決。清單 6 顯示了第二次嘗試:

清單 6. 動態地擴展類的第二次嘗試

class Module
def my_attr(symbol)
  class_eval "def #{symbol}; @#{symbol}; end"
  class_eval "def #{symbol}=(value); @#{symbol} = value; end"
end
end

清單 6 看起來有點兒神秘,但是不用擔心。可以在一點兒幫助下理解這段代碼。剛才只改變了三件事 :

沒有聲明新的 Person 類,而是打開了超類 —— Ruby 的 Class。

沒有硬編碼 name,而是傳遞進一個叫作 symbol 的參數。用 #{symbol} 代替了 name。Ruby 用代表 符號的字符串替換 #{symbol}。

用 class_eval 代替了 self.class.class_eval。代碼已經在類中操作了,所以不需要得到 self.class。

要查看它的工作,可以在 Ruby 解釋器中輸入清單 7 中黑體部分的代碼:

清單 7. 定義定制屬性

irb(main):001:0> require "my_attr.rb"
=> true
irb(main):002:0> class Person
irb(main):003:1> my_attr :name
irb(main):004:1> end
=> nil
irb(main):005:0> person = Person.new
=> #<Person:0x2b5fb90>
irb(main):006:0> person.name = "Bruce"
=> "Bruce"
irb(main):007:0> person.name
=> "Bruce"

正如所期望的,可以把行為添加到任何現有類。現在看到了怎樣才能把行為綁定到可以添加到類的附 加功能上。這項技術就是活動目錄添加高級概念(例如 belongs_to 和 has_many)的方式。但是活動記 錄沒有把行為添加到類,而是添加到叫作 ActiveRecord::Base 的模塊。

現在已經看到了一些相當精密的功能的作用,但是 Ruby 還能做更多支持 DSL 的事。

method_missing 和動態行為

有時,想根據外部情況把方法添加到類。例如,假設想在 Ruby 中表示羅馬數字。要把它們與字符串 分開,可以用 Roman.III 的形式把數字 3 表示成羅馬數字。要為每個可能的羅馬數字都向 Roman 添加 類方法,是不現實的,而且使用 Ruby 時也不需要這麼做。可以利用一個小技巧。

在 Ruby 中,在遺漏了一個方法時,Ruby 就會調用 method_missing 方法。可以覆蓋它來提供羅馬數 字,如清單 8 所示:

清單 8. 覆蓋 method_missing 方法

class Roman
def self.method_missing name, *args
  roman = name.to_s
  if(roman =~ /^[IVXLC]*$/)
  roman.gsub!("IV", "IIII")
  roman.gsub!("IX", "VIIII")
  roman.gsub!("XL", "XXXX")
  roman.gsub!("XC", "LXXXX")
  return(roman.count("I") +
    roman.count("V") * 5 +
    roman.count("X") * 10 +
    roman.count("L") * 50 +
    roman.count("C") * 100)
  else
  super(name, *args)
  end
end
end

這個代碼相當簡單,但是確實使用了 Java 程序員不熟悉的一些 Ruby 特性。由於覆蓋了 method_missing,所以只要這個類的客戶調用一個不存在的方法,Ruby 就會調用這個方法。下面說明細 節:

使用兩個參數:

name 代表方法名

*args 代表遺漏方法的參數

name 是個符號,所以首先用 to_s 把它轉換成 String。

用正則表達式進行數字是否羅馬數字的合理猜測。

如果數字是羅馬數字,就進行一系列替換,讓羅馬數字更容易處理。IV 是 4 ,IX 是 9,所以只計算 X、V 和 I 的出現,還不能得到它們的值。

為羅馬字母的每次出現分配一個值,分別是:I(1)、V(5)、X(10)、 L(50)或 C(100)。

如果方法不是羅馬數字,就調用超類,超類報告方法遺失。

對於 DSL,這個技術極為強大。活動記錄使用這個功能實現動態查找器。活動記錄沒有為每個列實際 地添加查找器,而是使用了 method_missing。使用這個策略,活動記錄不僅能匹配一個列,還能匹配列 的組合。例如,把 name 和 email 列添加到 people 表,可以支持 Person 類的 People.find_by_name_and_email 查找器。像這樣的細節使得活動記錄的用戶體驗非常舒服。它們也讓活 動記錄的實現非常簡潔而有意義,所以在活動記錄做的工作不符合自己的要求時,隨時可以實現自己的補 丁。

Java 編程中的 DSL 回顧

在使用 Java 語言時,選項就非常有限了。元編程更困難,所以很少能夠得到活動記錄那樣的體驗。 但是如果真的急需 DSL,還是有些選項的。而且不用總是求助於 XML 或標注。下面是一些常用的方法:

對於需求不太迫切的 DSL,可以使用 Java 類、方法和名稱構建對英語友好的詞匯,並通過消息調用 做需要的事。

對於典型的 Java 用戶,可以用 XML 構建自己的語言。XML 難以閱讀,但是在某些情況下可能有用, 並在 Java 世界中相當普遍。

對於已經要求 XML 的解決方案,可以使用 XML 的派生物來簡化。Craig Walls 有一個貼子介紹了如 何用 XBean 為 Spring 上下文做這件事(請參閱 參考資料)。

可以使用 XML 的替代表示(例如 Relax NG)來簡化 XML(請參閱 參考資料)。

當 Java 代碼和 XML 都不夠用的時候,可以在 JVM 中嵌入一種語言。最好的方式是通過 BeanShell (請參閱 參考資料)。

對於在 Java 應用程序中需要動態腳本的解決方案,可以利用已經有 BeanShell 集成的更加動態的語 言。好的示例有 Jython、JRuby 和 Groovy(請參閱 參考資料)。

可以從頭開始構建 DSL。在 Java 語言中這很難做到,但是對於某些應用程序來說還是值得一做。

這些主意,每個都有一系列 developerWorks 文章,所以我在這裡對它們就不做太多詳細介紹了,但 是有一點我要提一下。如果需要在 Java 語言中使用 DSL,需要問自己四個問題:

真的需要 DSL 麼?通過 Java 技術的一些更聰明的使用,可能可以做到自己需要的事。

XML 或 XML 的派生物足夠嗎?Java 開發人員對於 XML 經常有點兒太熱心了,但是有些派生物可以把 事情略微簡化。

可以在 Java 語言內部 使用其他語言嗎?JRuby 正在越來越好,Groovy 正在就位,Jython 也正在變 得更穩定。

從頭開始構建 DSL 值得嗎?用 Java 語言做這件事很難 —— 需要詞法器、解析器和語法器。但是可 以做到,可能值得做,具體取決於應用程序。

結束語

現在,我還沒有許多確切的答案。Java 語言在 DSL 方面做得不太好,但是有必要了解在構建 DSL 時 其他語言中可能需要什麼。還應當注意新的研究。那些帶來 IDEA IDE 的人們和一些其他公司正在開發一 套叫作語言工作台 的產品(請參閱 參考資料)。他們完全有可能革新我們的編碼方式。這些想法 —— 許多超越了 Java 編程 —— 正在擴展 DSL 的邊界。

下次,我將討論與並行編程有關的問題。將看到 Erlang 作為軟實時分布系統的一種可能的解決方案 。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved