常規運算式設計模型

作者:葉木金

 

摘要

本文主要是提供常規運算式(Regular Expression)的設計模型。此常規運算式定義特定重覆發生的問題(例如:特定的SQL表示式),運算式包括了常數、變數及運算元,並可根據變數設值並求解。此設計模型包括了常規運算式的結構定義、運算式的產生及運算式的求解;運算式的拆解及語法檢核並不在本文討論之列,但可針對此模型再進一步地延伸。

 

動機

在資料查詢的界面,常常會由使用者輸入查詢的符合條件,然後動態產生結構查詢語言(SQL)表示式向資料庫查詢符合條件的資料。如果供輸入的條件不多,可以簡單地判斷使用者輸入那些條件欄位,用if-else或switch-case來串接這些欄位組成SQL表示式。但如果輸入的條件變多了,或是輸入條件間的邏輯不是單一的時候(例如:比較邏輯可能有'='、'>'、'<'等,關係邏輯不只AND一種,有時還有OR及NOT等),查詢界面就會容易與程式邏輯難以切割,每次都必須重寫類似的程式碼。而當需求變多,系統會愈變愈複雜,你的程式會變得難以維護及容易出錯。所以有必要設計一個可以根據輸入條件而自動產生SQL表示式的模組。

根據上面所述需求,我們可以這樣設計:設計一個固定制式的查詢輸入框頁(Frame),以供實際需要可被繼承,繼承的框頁須置入所有查詢條件的輸入界面元件。當組成SQL表示式時,逐一掃描輸入界面元件,若使用者有輸入此欄位值,則將元件所設定的條件加入SQL表示式。如此,只要放入不一樣的查詢條件的輸入界面便可動態產生不一樣的SQL表示式了。

 

問題

以上的作法雖然看來可行,可是卻存在下列問題:

  1. 僵化的查詢界面:你必須一開始就想清楚你需要的「制式」為何?但人的理性有限,如果你想的不夠,需求改變時你不是重新設計界面就是把查詢界面搞得十分複雜。

  2. 比較式可能不只一種:除了'='外,我們可能還會需要用到其他的比較運算元,而且可能會與日俱增,如果考慮IS、IN、BETWEEN等,狀況還會更複雜。

  3. 關係式也不只一種:比較式的聯接,一般會用AND關係運算子,但你無法保證不會有其他關係式的需求(比如說OR、NOT)。

  4. 執行效率問題:逐一掃描控制項作法有彈性,但浪費程式執行效率,等到你的查詢界面控制項一多,就會造成反應速度變慢。

 

如果你的查詢界面變化不多,以上問題還不致造成很大的困擾;但正如動機所述,我們的目的是為了因應不同的查詢條件的需求。隨著需求愈變愈複雜時,制式的查詢界面將會變得不敷使用。屆時,你不是增加查詢界面的相關屬性,查詢界面勢必需要修改,而所有繼承此查詢界面的模組皆要跟著修改;或是繼承既有輸入元件以增加功能,元件數量勢必增加,增加查詢界面模組運用的複雜度。所以有沒有方法可以滿足我們多變化的需求,而且此模組的運用卻很簡便。

 

方案

此時我們想到常規運算式剛好可以派上用場。把SQL表示式寫成常規運算式的表示法,組成SQL表示式時,則將使用者輸入查詢條件的值代入常規運算式中,則可求出查詢條件的SQL語法。如此一來,查詢條件值的元件便不須與SQL表示式的邏輯牽扯在一起,也不用為特殊的條件需求增加輸入條件的元件。同時也不須要逐一掃描輸入條件的元件,只須要事先設定好輸入條件值元件與運算式之間的關聯,根據關聯取得變數內涵值可組成需要的SQL表示式了。而查詢界面也可以較有彈性,因為不須與特定的元件耦合,只須定義變數關聯之界面,此界面即為運算式解譯器之語境(Syntax Context)。

現在我們以ColA=:VarA and ColB is null or not :CnsC這個運算式為例作解釋,此運算式定義:VarA及:CnsC變數,然後可以求算運算式結果,我們採用簡明作法-建立以下語法樹:

圖中圓形符號代表運算元,陰影方塊代表常數,立體方塊代表變數。運算元左邊的子運算式我們稱為運算元的擁有者運算式;而運算元右端的子運算式則稱為運算元的參數運算式。變數值存放在語境中,所以當欲求運算結果則須傳入語境,從最頂端運算式開始向下運算(若運算式為運算元,則向下遞迴求算結果)。運算過程若遇到變數時,則從傳入語境尋找變數值代入。以此例,若':VarA'為空值,則上面的'='的運算結果應為空值,而上一層的'and'運算結果應為'ColB is null';同理,若':CnsC'為空值,則最上層的'or'運算結果會只剩下其左邊的'and'運算式。

 

樣式分析

欲建立運算式,我們須拆解析運算式的內容,然後以Builder樣式來建造運算式。至於運算式解譯器,我們以Interpeter樣式來實現。運算式的結構為樹狀結構,不管運算式是資料或是運算元,我們皆希望以一視同仁的方式來操作它們。想當然爾,Composite樣式是不二人選;但運算式的參數只有一個,所以我們運用了Decorator樣式(可視為Composite樣式的退化版本)組成運算式的複合結構,在此運算式會是一個修飾類別,它可代表有一個參數的單一運算資料運算子,而有一個擁有者的雙運算資料運算子則繼承它。

本模型對運算式的操作有兩種,建立運算式及求算運算式運算結果。它們對運算式會有相似的尋訪方式,且考慮將來可能會視實際需求增添操作,我們不想因此修改我們的運算式類別,所以在此利用Visitor樣式抽離出運算式的操作行為。此模型也定義了運算元的識別類別,除了存放運算元之識別資料外,還運用Strategy樣式抽離運算元求算運算結果的多型操作。

 

結構

 

參與者

TExprDirector定義了產生運算式的操作,此操作負責拆解欲產生的運算式內容,然後建立運算式結構。建立運算式結構的工作委託IExprBuilder界面負責,在此我們以TBuildRegExprVisitor來實作它。

IRegExpr定義運算式結構的基本界面,此界面有幾個重要操作,包含一個Accept操作,以運算式的不同功能之Visitor(建立運算式的Visitor、求算運算式結果的Visitor)為參數傳入,可對運算式作不同的操作。還有一個GetOperatorIdentity操作,參數為運算元名稱,可取得運算式的運算元識別資料。實作此界面的類別包括TNullExpr、TDataExpr及ToperatorExpr。TNullExpr定義空運算式,代表尚未開始建立運算式或新運算元之參數運算式;TDataExpr則定義運算資料,有IsConst(是否常數)及BeSubstituted(是否變數)兩個屬性;TOperatorExpr則定義運算元,內含操作參數,其型態為IRegExpr。

IOperatorIdentity則定義運算元之識別資料界面,GetCode及SetCode分別可取得運算元的識別碼。除了識別的功能之外,還有運算優先順序的用途。故在實作時,須注意考慮其運算優先順序,愈優先的運算元識別碼數值愈大。而GetExprFormula則為求算運算式結果的多型操作,在此運用了Strategy Pattern的自委託技術,實作此多型操作則可達到不同種類的運算元有不同的運算結果的目的。GetOperatorIdentity也是一個多型操作,實作此操作則根據傳入運算元名稱傳回運算元識別資料。

TOperatorExpr定義單一運算資料運算元,定義了OprId運算的優先順序,及提供設定運算元參數的操作SetParamExpr。而雙運算資料運算元呢?我們運用了Decorator Pattern擴展為TOperatorOwnerExpr,其中定義了運算元的擁有運算式OwnerExpr屬性及覆載(override)了SetParamExpr的操作。

IRegExprVisitor則定義了運算式的Visitor界面,本模型目前只實作TBuildRegExprVisitor及TEvaluateRegExprVisitor,將來可視實際需要而增添不同的Visitor來擴展運算式的用途(例如語法檢查或運算式代換功能)。TBuildRegExprVisitor提供設定運算式建造指示操作,此操作執行後,可根據運算式建造指示來走訪運算式節點,來增添運算式結構。LeafExpr定義在建造過程中,運算式語法樹尾端的運算式;建造完成後可透過GetResult傳回運算式結構。TEvaluateRegExprVisitor則定義了變數語境EvaluateCtx屬性,而運算結果存放ResultFormula屬性。

 

實作

以下說明此模型一些實作細節:

  1. 運算式由底端向頂端建立;運算結果則為由頂端至底端求算。我們可以用雙向鍊接的資料結構,但要維護雙向鍊接資料的一致及正確性會花費不小的成本;另外一種作法則以一個運算式堆疊來存放已建立的運算式節點,則會較具有彈性。

  2. 運算元的產生方式:此模型運用運算元識別資料來識別運算資料,在產生運算式的運算元之前,必須先找到該運算式的運算元識別資料。如果找到且運算順序比上一層運算式節點(必為運算元)的運算順序還高,就根據識別資料及運算元名稱產生新的運算元;否則便往上一層運算式節點走訪,如果到頂端仍未發現適合的運算式則代表此運算式語法有誤。

  3. 利用空運算式概念:初始運算式或新運算元的初始參數運算式皆為空運算式。利用空運算式可簡化程式邏輯,不須判斷運算式是否為不存在。空運算式的運算元識別資料我們會定義單一運算資料運算式,因為單一運算資料運算元並無運算元的擁有者,處理起來和雙運算資料運算元會不一樣。所以運用抽象化思維,我們把空運算式視作單一運算資料運算元的擁有者。

  4. 括號的處理:左括號'('可能是單一運算資料運算元,其運算優先順序為最低,當產生右括號')'運算式時,則向運算式頂端找左括號節點,並將其優先順序設為最高。

  5. 若運算式為運算元,而欲加入的運算式亦為運算元,則此兩運算元應結合起來(例如SQL表示式的NOT IS、LEFT JOIN);若運算式為運算資料,而欲加入的運算式亦為運算資料,則此兩運算資料的組合應以一省略運算元結合稱(例如SQL表示式的AS)-特定運算元識別資料、空白識別名。

  6. 針對不同的運算式需求,我們只須增添不同運算式及運算識別資料的定義,建議運用Decorator樣式,以減少類別數量。

 

 

結語

本模型已成功地使用在動態產生SQL表示式的系統中,該系統為以Delphi 5開發,運用三層式架構的資訊系統,此模型使得複雜設計得以簡化及系統彈性得以維持。預計此模型還能運用在系統參數公式的設定,只須修改運算元識別資料類別及實作結合資料庫的語境即可輕易達到目的。

本模型並未論述運算式字串的拆解器(Command Parser)的作法,一般而言我們會以Strategy樣式來設計Command Parser,將另外再以專文討論。