當我們一談到宣告式的程式設計(Declarative programming),多數人會馬上聯想到函數式(Functional)程式語言,並且認為這是與命令式(Imperative)對比鮮明的設計典範。

實際上,為了控制開發時的複雜度,即便採用命令式語言,亦可採用宣告式設計的概念,在實作前,弄清楚面對的問題、需求與架構,加以陳述。

宣告問題中的組成元素
使用純函數式語言進行設計時,會有些外在特徵,像是必須定義代數資料型態(Algebraic data type),以數學概念使用變數,也就是令變數值不可變(Immutable),因而也沒有迴圈的可能性,必須以函式來進行重複性的運算,因此常看到遞迴的使用。代數資料型態、不可變的變數、遞迴的本質,其實都是在宣告待解問題中有哪些組成元素。

在我先前專欄〈抽象資料型態與代數資料型態〉中談過,定義代數資料型態時,得優先思考資料的結構與規律性,你不能在拿到一筆資料時,就急著寫程式去處理它,必須花費時間分析,瞭解資料的組成模式,才有辦法宣告它的結構與規律性。

我在另一篇〈不可變動性帶來的思維轉換〉則談過,若變數不可變,程式中定義的名稱實際上就只是運算式(Expression)的代名詞,因此必須思考每一次運算之目的,弄清楚每一步代入函式的數值為何,以及希望得到何種結果。每建立一個變數,實際上就是在宣告該處需要一個子結果,而這個子結果會用來組成整個問題的答案。

問題通常由一些子問題組成,有些子問題只要一次運算,而有些問題需要進一步地針對資料子集重複處理,如果變數不可變,重複處理就必需以遞迴方式實現,也因此,你得搞明白問題中有哪些子問題可重複處理,才可能在函式中獨立地宣告子問題的組成元素,能夠獨立宣告很重要,如專欄〈遞迴的美麗與哀愁〉中談到的,如此才能運用遞迴,將複雜問題單純化。

使用純函數式語言是一種約束,「得抽象地定義問題,無法命令式地列出實際的求解步驟」。你必須瞭解資料的組成模式,弄清楚完整答案是由哪些子答案組成,搞明白問題可分解為哪些子問題,如此才有辦法搭配純函數式語言,來進行宣告式設計,如果沒有思考過這些待解問題中的組成元素,就會覺得寸步難行。

在測試中宣告規格與需求
測試驅動(Test-driven)開發中,如果要增加一個新功能,得先撰寫測試,從中定義必需的資料、操作的介面,以及預期的結果,接著才實作新功能,想辦法讓實作通過測試。

而在撰寫測試時,由於實際上新功能還未實作,無法根據實作細節來撰寫測試,為了能真正根據規格來進行測試,也建議撰寫測試的人不能是實作功能的人。這樣的情況下,若不是清楚地瞭解新功能的需求,就連定義測試都做不到。

在實作還不存在的情況下,測試中對新功能的操作只是一個抽象的空殼,你只是在測試中宣告想要的規格與需求。從這個角度來看,測試驅動可視為一種宣告式設計:你宣告抽象的功能,而具體實作則被封裝起來。這表示即使採用命令式的語言,也可以採用宣告式的思維,而且,只有在先搞清楚規格與需求的情況下,才能清楚而具體地在測試中,陳述想要的功能。

將測試驅動視為一種宣告式設計,不將宣告式局限在函數式語言的話,那宣告式的本質是?我在專欄〈用函數式重構程式碼與演算法〉中談到,當面對複雜問題不知如何重構時,可運用函數式的思考,強迫自己理解與分解問題,而後才能宣告問題的定義;而採用測試驅動,則是強迫自己搞懂功能需求,而後才能宣告測試的流程。至於宣告就是在陳述,如果連問題、規格與需求都無法陳述,實作時創造的方案解決的對象,大概也是模糊不清。

在框架中宣告應用程式流程與架構

在應用程式中套用框架時,你會從哪個部份開始進行?元件實作還是組態設定?

我的偏好是從架構相關的組態設定開始,組態媒介用XML、標註(Annotation)或DSL(Domain-specific language)都沒關係,重點在完整實作某個流程功能前,設定好物件相依性、處理請求的元件、呈現結果的畫面等,組態設定時先不去考慮具體的物件、元件或畫面是否存在,將焦點放在組態設定時,是否能清楚地瞭解完成此次流程功能所必要的架構,如此才能在組態設定時進行架構的宣告。

在組態設定中完成應用程式的架構宣告後,如果必要的物件、元件等不存在,我會脫下組態設定者的帽子,轉換為物件、元件實作者的身分,由於已經宣告好架構,現在只要根據架構中對物件、元件的需求加以實作就可以了,當宣告的架構中必要元件都具體存在後,此次的流程功能就完成了──就算出錯,因為對應用程式架構已有清楚的瞭解,要找到錯誤並加以修正,通常也比較容易。

有時,我會急就章地在元件實作與組態設定之間穿梭,結果往往造成混亂,不清楚現在到底實作至哪個階段,或者是經常出錯且難以找出問題所在,白白浪費時間。

在套用任何框架前,基本上必須對框架支援的流程與架構都認識,並清楚地瞭解自身應用程式是否符合框架支援的流程與架構,在套用框架時先進行組態宣告,其實就是在測試本身是否瞭解應用程式的流程與架構,如此才能在組態中清楚陳述出來。

在《Struts 2 in Action》書中,就採用了宣告式架構(Declarative architecture)這個字彙,來表示一種組態方式,可讓開發者在較高的層次上,描述應用程式的架構,而不是直接進行程式設計上的處理,而這正是套用框架時應有的態度,最好是在能清楚描述應用程式架構的情況下,再來套用框架。

其實,以宣告式風格來約束開發者,並不是函數式語言的專利,像Rails、Django、Play等快速開發框架,在建立應用程式某個流程功能時,得先指出需要哪些模型、控制器等,這些框架的工具會自動產生依循慣例的架構與基本元件,之後你在這樣的架構上將元件功能實作齊全,使用工具產生架構與基本元件,其實就是宣告應用程式流程與架構,如果不清楚應用程式流程與架構,而胡亂使用工具,就算用了這類快速開發框架,也沒辦法快速起來。

充分瞭解意圖才能宣告意圖
無論是宣告問題中的組成元素、在測試中宣告規格與需求,或者是在框架中宣告應用程式流程與架構,其實都是從抽象的層面來陳述你的意圖,而不是實作的細節,這其實與一個古老的設計原則是類似的:「針對介面進行設計,而不是針對實作」。

例如,在函數式語言中,是抽象地定義問題,而不是具體的求解步驟;在測試驅動中,抽象地說明需求,而不是針對還沒實作的功能;在套用框架時,抽象地陳述應用程式的架構,而不是考慮各個元件細部的處理細節。

只有充分瞭解意圖,才能宣告意圖,如果無法清楚把意圖陳述出來,就會難以達成目的。相信不少人經常在討論區看到一些發問時,心中會浮現的話語是「你到底在問什麼?」,發問者連自己的問題都無法清楚陳述,更別說希望別人出手救援了,將這類情況放在函數式語言的宣告式設計、測試驅動的規格與需求宣告、應用程式流程與架構的宣告上,就是類似的道理。

專欄作者

熱門新聞

Advertisement