在Java 8之中,除了Lambda、日期時間API等幾個顯著的特性,在標準API的許多角落,隱約可察覺Java努力說行話的企圖,無論是日期時間、高階排序、Stream等 API,都可看到努力展現DSL的設計,Java語言本身的特性,其實並不適合創建DSL這種事,然而更因為如此,從努力說行話的Java中,更可以看到DSL設計時應注意的本質。
程式庫設計就是語言設計
DSL此一名詞興起已有一段時間,指的是特定領域使用的特定語言,談到DSL設計,似乎許多開發者就會聯想起Ruby、Groovy、Scala等極力支援簡潔語法、meta-programming的語言,談到Java,大概就是提到XML,因為XML可用來建立外部(External)DSL,而Java提供了分析XML的程式庫,不必為XML撰寫特定的語言處理程式,至於創建以Java為宿主(host)語言的內部(Internal)DSL,似乎總不容易沾上邊。
Java不適合創建DSL的原因很明顯,第一,它不夠簡潔,根本就是囉嗦──型態宣告囉嗦、方法呼叫囉嗦,連後來號稱要減輕型態宣告負擔的泛型,也只有囉嗦兩字可以形容;第二個原因是自由度不夠,每個物件都得是一個模子(也就是類別)印出來的,不能說改就改,模子本身也是如此。在Java的世界裏似乎沒有DSL,只是充滿了程式庫、框架,而這正是Java過去歷史的象徵,Java中充滿了為各種領域而設計的各種程式庫與框架。
對於程式庫或框架,一般開發者的印象是可重用功能的集中處,開發者也都知道,在程式庫的使用過程中,都各有規則與流程必須遵守,越是特定領域的程式庫,越是如此,無異是程式語言的擴充。
在我之前專欄〈現代程式庫的多重角色〉中就探討過,擴充語法或語義在近來的程式庫設計中,越來越見其重要性,事實上在貝爾實驗室(Bell Labs)流傳,因C++創建者Stroustrup, Bjarne的引用,而廣為人知的一句話也談到:「程式庫設計就是語言設計(Library design is language design)」。
實際上,如果曾在Ruby、Groovy、Scala等語言中試著自訂內部DSL,也就會知道,內部DSL的本質,其實就是具有巧妙API設計的程式庫,如果語言本身的靈活性與自由度夠,在API設計時儘量接近自然語言或特定領域行話,這麼一來,在閱讀程式碼時,就能避免意識到宿主語言功能上「如何」達到,突顯程式想做些「什麼」,甚至不懂程式的特定領域專業人士,也可以看懂程式內容。
不只對開發者隱藏細節
Java不是沒說過特定領域行話,只是對象是著重在特定領域的開發者,像是測試領域,這時想到的,就是元老級的JUnit,測試套件(Test suite)、測試案例(Test case)等領域詞彙,只不過,測試用的程式碼看起來就像是……程式碼,即使是最後斷言測試,例如:assertTrue(names.contains("Justin") && names.contains("Monica") && names.contains("Irene")),如果不是開發者來觀看,通常馬上會意識到這是Java程式碼,而產生閱讀抗拒。
在《松本行弘談程式世界的未來》中,談到:「設計API時,如果抱著『這是在設計新DSL』的想法,或許會有全新的體認也說不一定」。程式庫設計的本質,就是隱藏細節,只是這次不是對開發者隱藏細節,而是為非開發者隱藏細節,就上例而言,可以隱藏List的API與&&判斷的細節,設計為assertThat(names, hasItems("Justin", "Monica", "Irene"))的形式,Harmcrest就是這樣設計的,在Java 8的高階排序API中,也運用了類似的技巧,像是words.sort(nullsFirst(reverseOrder()))。
那麼,hasItems實際上隱藏了什麼呢?一個測試領域開發者的詞彙Matcher,也就是斷言比較器,開發者會知道這是什麼,但非開發者不清楚,那麼reverseOrder、nullsFirst隱藏了什麼?一個排序領域開發者的詞彙Comparator,Java開發者都知道它在排序上的作用,只是現在得將之隱藏起來,因為非開發者的語意模型(Semantic Model)中不存在這個詞彙。
在Martin Fowler的《Domain-Specific Languages》中強調了,語意模型必須與DSL區分,藉由中介的建造器(Builder)來建構出語意模型實例,方才看過的hasItems、nullsFirst、reverseOrder,就是一種建造器,這邊的建造器只是一個名詞,並非設計模式中的建造器,當然,運用設計模式中的建造器模式,也是一種常見的方式,特別是加上了方法鏈結(Method chaining)的設計,更是過去為人知曉,Java中創建DSL的主要方式之一。
突顯領域中的名詞與動作
過去Java建立內部DSL的方式很有限,主要是隱藏語意模型建立的過程,能介入這過程的方式,就是指定某些值,而值只有基本型態與物件兩種選擇。
如果想在這些過程中自訂一些動作,那就得使用醜陋的匿名類別語法。guava-libraries的FluentIterable其中一個例子,在Java 8的Lambda還沒出現之前,使用它不是沒有效益,只不過受到匿名類別語法太多的干擾,看來就不太像個DSL(但本質上是了);想在過程中自訂一些動作,另一個例子就是框架,只不過,這仍然是Java開發者才會用的大傢伙。
語意模型基本上是領域中的名詞,藉由將方法等API設計為動詞,因而得以串聯起來而像個語言,只不過只透過方法等API設計,可運用的動詞就受到能接受的名詞之限制,而動詞本身也因而容易太具體,即使只是對特定領域的開發者隱藏細節,能實現DSL的場合也是受到相當的限制。
在Java 8中加入Lambda之後,這個限制得到解放,因為可以將程式碼當成資料傳遞(Code as data),也就表示可以自訂動作來介入DSL的流程了。
Java 8中最經典的Stream API,還是得再談一次,for、while等迴圈機制是程式語言面的元素,而filter、map對使用者隱藏了這些元素。以filter為例,它接受的Predicate實例,其實本質上與hasItems傳回的Matcher相似,只不過你僅能指定值給hasItems,來建立一個比對動作固定的Matcher,你卻能使用Lambda等語法來指定Predicate的比對動作。
因此,現在動詞可以不用過於具體,而可以抽象化為filter、map等動詞,程式撰寫時,就可以突顯特定領域中的動作,這就是Java 8標準API中,Stream、Optional、CompletableFuture、Comparator等API都開始像個DSL的原因了。
透過適當的設計,加入更多DSL的元素,像是文脈、語句、階層等,實現更特定領域的DSL就成了可能,在《Java 8 Lambdas》中,就以JavaScript的Jasmine為模仿對象,用Java設計了一個極為相似的BDD(Dehavior-driven development)框架。
DSL設計對象是人,不是機器
身為一名開發者,關心的事情當然是「如何」完成功能,然而,身為一名使用者,只關心功能是「什麼」,因而有個玩笑話(某些程度上也是事實)是:「用什麼技術不重要,東西會動才是最重要的」,確實也是如此,程式設計表面上是告知機器如何動作,然而實際上是要想清楚「人們實際要求什麼、這些要求的本質是什麼、以及達成這些要求的詳細步驟是什麼的過程」,松本行弘在《松本行弘談程式世界的未來》就是如此提到。
DSL的發展,似乎反映了從機器到人這樣的過程,開始是「可重用功能的集中處」,發展到有時是為了「限制或避免語言特性」,接著進入「擴充語法或語義」,然後有了特定領域語言的考量,更多關心從HOW轉移到了WHAT,無論那是開發者重視的WHAT,或者只是一名單純使用者的WHAT。
當更多設計的對象是人而不是機器時,語言在DSL上的可能性,往往超越了語言創建者的想像,Ruby之父就曾對此感到驚訝,囉嗦又不靈活的Java能開始講行話,也就不足為奇了!
專欄作者
熱門新聞
2024-11-25
2024-11-25
2024-11-22
2024-11-24
2024-11-25
2024-11-25