iThome 網站首載:笨方法學程式庫
除了使用程式語言描述問題解決方式之外,如何將相關程式庫兜在一起,也是撰寫程式時重要的一環,初次接觸某程式庫時,有範例豐富的書籍或文件非常重要,然而若要活用甚至評估程式庫優缺點的話,就不能僅止於範例的臨摩,網路上有所謂笨方法(The Hard Way)學習語言的文件,為的是培養讀者自行解決問題的能力,實際上學習程式庫也有些笨方法,讓程式庫新手在遇到問題時,不再只是當個索取程式範例的伸手牌。
- 從範例程式碼著手
無論如何,能有範例程式碼絕對是個好的起點,不過一開始並非指書本的範例程式碼,而是指程式庫官方網站提供的範例程式碼。如果程式庫本身希望獲得關注,多半都會提供最具特色的範例程式碼,就像是程式語言的Hello!World!範例,程式庫本身提供的範例程式碼,呈現出本身最基本的風格與結構,也可以從中看出程式庫創建者或參與成員,想要傳達的設計理念及意義。
不過並非每個程式庫的創建者或參與者都有合適的文筆,有能力清楚描述程式庫的行為與運作方式,有時文件的撰寫者也不一定是程式庫參與成員,而可能是另一組人馬,像是社群成員貢獻的文件,程式庫文件也可能過期,基於諸如此類的原因,文件描述模糊不清甚至是誤導方向是有可能的,適當保持懷疑心態,親手跟著文件做些範例驗證,以確認API正確性,甚至照著文件的想法,自行模彷實作一個簡易版本程式庫,以驗證文件中想傳遞的觀念是否正確。
通常官方提供的範例程式碼不是很多,透過網路搜尋更多範例程式碼是必要的,官方文件基本上會列出程式庫本身的優點,然而網路上會有各種範例呈現出程式庫的正反面,特別是在像stackoverflow.com這樣的地方,官方網站的文件呈現出程式庫參與者希望你採用的方式,然而往往在搜尋網路更多的意見後,可看出實際上程式庫該有的使用樣貌。
- 摸清API架構
在大致看過一些範例程式碼後,下一步就是透過API文件勾勒出程式庫的主要API架構,這是為了在沒有任何範例程式碼可以觀看的情況下,也能夠自行活用、組合程式庫中的相關API。以Java標準程式庫為例,想活用群集(
Collection
)框架,首先就是按照Javadoc文件勾勒出主要的介面定義與實作類別,想活用串流輸入輸出,就是瞭解InputStream
、OutputStream
繼承架構與相關裝飾器類別,想活用JDBC、NIO2等,就得勾勒出標準介面與實作者之間的關係。有時某個程式庫會是另一程式庫的擴充或功能增強,此時除了瞭解新程式庫架構之外,得連同舊程式庫的架構一併瞭解。例如想瞭解guava-libraries中對JDK群集框架的擴充,就得瞭解JDK群集框架中的介面定義,接著才有辦法瞭解那些
ImmutableCollection
、Multiset
、Multimap
、BiMap
實際上的繼承與實作關係,如此你才會知道Multiset
實際上不是Set
,而Multimap
也不是Map
。有時同性質的兩個程式庫,使用與組合時範例程式碼會極為相似,通常這是為了有平滑的轉換曲線,但實際上兩者的API架構可能有很大的不同,日後想要活用,還是得瞭解兩者API架構的不同。例如JSR310的許多概念雖然來自Joda-Time,然而Joda-Time將瞬時(
Instant
)、局部(Partial
)等概念,實際對應至API的ReadableInstant
、ReadablePartial
等介面定義,也就是將它們視為行為而不是具體概念;JSR310則是將瞬時對應為實際的Instant
類別,也就是機器上具體的時間概念,JSR310沒有局部概念,有的是人類具體的時間概念,像是當地時間、年、月、時、分、秒等,可對應至實際的LocalDateTime
、Year
等類別。JSR310的介面定義是屬於框架等級的API,例如TemporalAccessor
介面定義了時間基本讀取行為,Temporal
介面增加了時間加、減與調整等時間讀寫行為。由於JSR310原預計於JDK7發表,然而因為一些政治性與時間因素的考量,轉而在未來的JDK8發表,在這段間隔下,雖然JSR310核心概念不變,然而API架構有了許多調整,許多文件已經過時,直接從目前的實作版本中瞭解API架構就更顯重要,在輔以核心概念的情況下,對於文件不足的程式庫,通常可藉由瞭解API架構,自行摸索出程式庫更多的使用方式。
- 閱讀原始碼
無論是範例程式碼或是API文件,揭示的都只是抽象化後的公開協定,若單單僅止於拼湊與使用,基本上無需追究程式碼實作,然而有時我們會想知道細節,以便進一步瞭解、善用或避免誤用程式庫。舉例而言,guava-libraries中的
ImmutableCollection
提供不可變(Immutable)群集,官方Wiki中提到了copyOf
方法雖然名義上是複製,但實際上可能不會真的進行複製,以在某些時候節省資源以增進效能,至於是哪些情況下會複製,哪些情況下又不複製,API文件中並不明載(Undocumented),如果你在意這方面的效能問題,就得直接閱讀原始碼來得知。另一方面,
ImmutableCollection
與其子介面,到底實作時分別運用了哪些資料結構,API文件上也不載明,這本意是要API使用者完全信任工廠方法,然而實際上很多情況需要知道實際資料結構,而這可從原始碼中得知,因為ImmutableCollecton
物件都是不可變,所以內部實作時若僅包括單元素則直接包裹,多個元素則多半使用陣列,即使是ImmutableSet
也不例外,以獲取更好的效能。透過閱讀原始碼,可瞭解這類群集因為不可變特性,而可共用一些資料結構,開發者在使用上考量到效能問題時,才能更加善用程式庫提供的這些特性。有些程式庫提供擴充用的介面,並會提供預設的實作品,透過閱讀這些實作品的原始碼,為必須自行擴充程式庫的場合提供了參考方向。例如guava-libraries的
Range
物件僅僅表示範圍,無法直接針對範圍進行迭代,如果要迭代範圍中的不連續元素,則必須實作DiscreteDomain
定義範圍內不連續元素間的關係,DiscreteDomain
提供可迭代int
、long
與BigInteger
三種型態的預設實作,不過都是DiscreteDomain
實作的私有內部類別,透過閱讀IntegerDomain
、LongDomain
與BigIntegerDomain
,就可以明瞭如何自行定義DiscreteDomain
,從而瞭解DiscreteDomain
本身扮演了產生器(Generator)的角色。簡單來說,無論是透過API架構的勾勒,或者是原始碼的閱讀,都在於發掘沒有記載於文件的程式庫運用方式,多數的情況下,程式庫應提供良好的說明文件,然而有時因為程式庫發展快速,或者是程式庫維護組織或社群之風氣使然,而無法提供良好說明文件,透過API架構的勾勒或是原始碼閱讀,就是瞭解程式庫的有力手段。
- 認識創建者與演進歷史
如同程式語言,程式庫會有創建者,也會有演進歷史,瞭解創建者建立程式庫時的初始理念,並瞭解程式庫演化的過程,亦有助於得知程式庫為何會有今日的樣貌,因而能更進一步的活用程式庫。
Java日期時間API的演進就是個例子,有關於
Date
、Calendar
的問題,可參考我之前的專欄〈從JDK時間API演進看時間處理〉;有感於Date
、Calendar
定義的不完善與API的混亂,Stephen Colebourne創建了Joda-Time,並提出了瞬時、局部等解決Java日期時間API的幾個核心概念,而Stephen Colebourne後來帶著Joda-Time中得到的教訓與經驗,參與了JSR310的制訂,如前所述,JSR310與Joda-Time的API架構截然不同,然而若閱讀過Stephen Colebourne撰寫的〈Why JSR-310 isn't Joda-Time〉,就不難理解為何會有如此之差別。從範例程式碼著手之後,摸清API架構、閱讀原始碼、認識創建者與演進歷史,就現今速食年代來說,確實是個笨方法,初期投入的時間成本雖高,對程式庫卻能夠有札實的認識,在後續就能靈活運用、擴充程式庫,在真正遇到問題時,也才能精準地自行尋求解答,或者是在必須發問討論時,能夠精準地描述問題,在最短時間內解決問題。