基於相容性、程式典範、面對的問題與可能引入的複雜度等考量,程式語言的語法演化不易,相較之下,程式庫進化則較為靈活,可根據既有的程式語法與某些模式進行組合,並考慮特定領域需求來為API命名,如此一來,就可運用程式庫來構造特定領域語意。

用工廠模式隱藏物件建立細節
設計模式中的工廠(Factory)模式,可用來隱藏物件建立細節。

舉例來說,JDK6使用群集(Collection)的泛型(Generics)功能建立物件時,必須使用List scores = new ArrayList()之類的語法,等號兩邊重複指定了Integer型態資訊,因而JDK7使用List scores = new ArrayList<>()語法來簡化型態指定,但在JDK6中要解決此問題,可以設計static的 List emptyList()方法,令其傳回new ArrayList(),如此一來,就可使用List scores = emptyList()來建立實例。

JDK API本身亦有類似案例,像是Arrays的asList方法,可以使用asList(1, 2, 3)的方式構造List實例。類似地,亦可設計static的 List set(T... elems),內部封裝Set實例的建立與相關元素的新增,之後,就可以用set(1, 2, 3)的語法來建立Set物件。

有時候,程式語言本身語法雖不支援,但可透過工廠模式隱藏細節,以建立高階抽象語意。

例如某些程式語言有建立字典(Dictionary)物件的實字(Literal)語法,像是Python可用{'Justin' : 123456, 'Monica' : 933933}語法來建立;Java的字典物件是Map實例,必須建立後逐一新增鍵(Key)值(Value),若想擁有類似的建立Map實字語法,可設計static的 Map map(Object......kvs),如此,就可使用map("Justin", 123456, "Monica", 933933)來建立Map實例,其中map方法的實作概念如下:

Map map = emptyMap();
for(int i = 0; i < kvs.length; i += 2) { map.put((K) kvs[i], (V) kvs[i + 1]); }
return map;

使用樣版回呼重用共通流程

我之前在〈實現共用程式碼樣版的模式〉這篇中,談過樣版回呼(Template callback)模式,可將共用流程予以封裝,特定演算委託回呼物件執行,只要透過適當名稱,就可以賦予共用流程高階語意。

例如說,Java中有「條件成立時予以執行」的if語法,但在沒有「條件不成立時予以執行」的unless語法,可設計static的void unless(boolean cond, Block block),實作內容為if(!cond) { block.apply(); },其中Block介面僅定義為void apply(),如果使用JDK8的Lambda語法,則可以如下運用unless:

unless(number == 2, () -> {
out.println("猜錯囉!");
});

使用API創造語意時,必須配合語言原有語法支援,才不會有冗餘資訊並呈現出適切語意。例如上述unless的例子受限於Java語法,多了不必要的括號、箭頭符號與分號資訊,因此創造出來的unless與原語言語法有格格不入的感覺;同樣是unless的例子,Scala由於具備以名呼叫參數(By-name parameter)、鞣製(Curry)等語法特性,因而定義的unless方法,可以如unless(number == 2) { println("猜錯囉!") }的方式使用,看來就像程式語言內建語法。Ruby可指定區塊(Block)給方法,因而可構造出butThat(number == 1) { print "猜錯囉!" }之類的語法。

有時可試著結合工廠與回呼,利用工廠隱藏物件建立細節的特性,將繁瑣語法封裝起來。

舉例來說,某個斷言為assertTrue(number < 2),可試著設計static的 void assertThat(T elem, Matcher matcher),其中Matcher介面定義了boolean matches(T item)方法,而assertThat內部實作為呼叫Matcher實例matchers方法,若傳回為false就拋出例外;如果設計了static的 Matcher lessThan(Object that)方法,實作內容使用JDK8的Lambda語法傳回(T elem) -> elem.compareTo(that) < 0,則可改用assertThat(1, lessThan(2))的方式進行斷言。

類似地,像assertTrue(numbers.contains(1) && numbers.contains(3) && numbers.contains(4))不易閱讀的語法,可設計static的 Matcher hasItems(Object... elems),若其實作如下,就可使用assertThat(numbers, hasItems(1, 3, 4))來進行斷言:

return (T c) -> {
for(Object elem : elems) if(!c.contains(elem)) { return false; }
return true;
};使用鞣製概念建立管線操作

透過設計,還可進一步地自由組合API,形成更複雜語意。例如想達成assertThat(numbers, hasItem(lessThan(5)))效果,可設計static的 Matcher hasItem(Matcher matcher),若其實作如下,就可取代不易閱讀的assertTrue(numbers.get(0) < 5 || numbers.get(1) < 5 || numbers.get(2) < 5):

return (T c) -> {
for(Object o : c) if(matcher.matches(o)) { return true; }
return false;
};

我之前在〈函式的鞣製化〉這篇中談過,使用傳回物件保有前次操作成果,以便開發者進一步作後續方法呼叫,因而可形成方法鏈(Method chain),這種形式可說是鞣製概念的延伸。

像是JavaScript程式庫jQuery的鏈狀風格,Java的ORM框架Hibernate的Criteria API,或者是JDK8中為Lambda語法增加的Collection API,都運用了相同概念。像assertThat(numbers, everyItem(lessThan(5)))的組合方式,也是鞣製概念的延伸。

相較於session.createCriteria(User.class).setFirstResult(51).setMaxResults(50).list()風格是由左而右逐一生成具可執行能力的物件,assertThat(numbers, everyItem(lessThan(5)))則是由右而左逐一生成具可執行能力的物件,lessThan會生成Matcher實例作為hasItem的回呼物件,而hasItem再生成Matcher實例作為assertThat的回呼物件,無論是由左而右或由右而左,形成的管線(Piped)操作,只要方法命名適當,都可增加不少語意可讀性。

思考程式庫於特定領域的命名與組合方式
今日的程式庫不再僅是共用功能的集中地,特定領域的程式庫,某種程度上就是在建立特定領域語言,只不過有時程式庫本身的命名過於空泛而不易看出,與其如此,不如思考基於程式既有語法,採用適當模式建立特定領域的相關語意,並重視API命名,讓程式庫不僅是程式庫,而是作為語言的延伸。

舉例來說,JDK8的Collection配合Lambda語法增加了一些高階操作,實際上,前述的hasItems是在進行一種allMatch操作,然而使用allMatch的高階語意來撰寫會是assertTrue(asList(1, 2, 3).allMatch(elem -> numbers.contains(elem))),雖然重用了allMatch定義的流程,但可讀性並不好,不如針對測試領域採用assertThat(numbers, hasItems(1, 2, 3))來得清楚;類似地,前述的hasItem實際上是在進行一種anyMatch操作,然而與其採用assertTrue(numbers.anyMatch(elem -> elem < 5))的寫法,不如針對測試領域在命名上給予巧思,改採可讀性更好的assertThat(numbers, hasItem(lessThan(5)))寫法。

 

專欄作者

熱門新聞

Advertisement