http://www.ibm.com/developerworks/cn/java/l-expression/index.html
問題由來
在我做過的一個(gè)針對網(wǎng)絡(luò)設(shè)備和主機(jī)的數(shù)據(jù)采集系統(tǒng)中,某些采集到的數(shù)據(jù)需要經(jīng)過一定的計(jì)算后才保存入庫,而不是僅僅保存其原始值。為了提供給 用戶最大的靈活性,我設(shè)想提供一個(gè)用戶界面,允許用戶輸入計(jì)算表達(dá)式(或者稱為計(jì)算公式)。這樣,除了需要遵從少量的規(guī)則,用戶可以得到最大的靈活性。
這樣的表達(dá)式具有什么特點(diǎn)呢?它一般不是純的可立即計(jì)算的表達(dá)式(簡單的如:1+2*3-4)。它含有我稱為變量的元素。變量一般具有特殊的 內(nèi)定的語法,例如可能用"@totalmemory"表示設(shè)備或主機(jī)(下面簡稱為設(shè)備)的物理內(nèi)存總數(shù),那么表達(dá)式"(@totalmemory- @freememory)/@totalmemory*100"就表示設(shè)備當(dāng)前內(nèi)存使用率百分比。如果與告警系統(tǒng)聯(lián)系起來,監(jiān)測此值超過 80 系統(tǒng)就發(fā)出 Warning,那么這就成為一件有意義的事情。不同種類的采集數(shù)據(jù)入庫前可能需要經(jīng)過復(fù)雜度不同的計(jì)算。但顯然,最后求值的時(shí)候,必須將那些特定的變量 用具體數(shù)值(即采集到的具體數(shù)值)替換,否則表達(dá)式是不可計(jì)算的。這個(gè)過程是在運(yùn)行時(shí)發(fā)生的。
回頁首
問題的一般性
我認(rèn)為表達(dá)式計(jì)算是個(gè)一般性的話題,并且也不是一個(gè)新的話題。我們可能在多處碰到它。我在讀書的時(shí)候編寫過一個(gè)表達(dá)式的轉(zhuǎn)換和計(jì)算程序,當(dāng)時(shí) 作為課余作業(yè)。我看到過一些報(bào)表系統(tǒng),不管它是單獨(dú)的,還是包含在 MIS 系統(tǒng)、財(cái)務(wù)軟件中,很多都支持計(jì)算公式。我認(rèn)為這些系統(tǒng)中的計(jì)算公式和我所遇到的問題是大致相同的。對我來說,我在數(shù)據(jù)采集項(xiàng)目中遇到這個(gè)問題,下次可能 還會在其他項(xiàng)目中遇到它。既然已經(jīng)不止一次了,我希望找到一個(gè)一般性的解決方案。
回頁首
一些已有的設(shè)計(jì)和實(shí)現(xiàn)不能滿足要求
在設(shè)計(jì)和實(shí)現(xiàn)出第一個(gè)版本之后,我自己感覺不很滿意。隨后我花點(diǎn)時(shí)間上網(wǎng)搜索了一下(關(guān)鍵字:表達(dá)式 中綴 后綴 or Expression Infix Postfix)。令人稍感失望的是,所找到的一些關(guān)于表達(dá)式的轉(zhuǎn)換、計(jì)算的程序不能滿足我的要求。不少這樣的程序僅僅支持純的可立即計(jì)算的表達(dá)式,不支 持變量。而且,表達(dá)式解析和計(jì)算是耦合在一起的,很難擴(kuò)展。增加新的運(yùn)算符(或新的變量語法)幾乎必定要修改源代碼。在我看來,這是最大的缺陷了(實(shí)際 上,當(dāng)年我編寫的表達(dá)式轉(zhuǎn)換和計(jì)算的程序,雖然當(dāng)時(shí)引以自豪,但是現(xiàn)在看來也具有同樣的缺陷)。但是,表達(dá)式的轉(zhuǎn)換和計(jì)算本身有成熟的、基于棧的的經(jīng)典算 法,許多計(jì)算機(jī)書籍或教材上都有論述。人們以自然方式書寫的表達(dá)式是中綴形式的,先要把中綴表達(dá)式轉(zhuǎn)換為后綴表達(dá)式,然后計(jì)算后綴表達(dá)式的值。我打算仍然 采用這個(gè)經(jīng)典的過程和算法。
回頁首
我的設(shè)計(jì)想法和目標(biāo)
既然表達(dá)式的轉(zhuǎn)換和計(jì)算的核心算法是成熟的,我渴望把它們提取出來,去除(與解析相關(guān)的)耦合性!試想,如果事物具有相對完整的內(nèi)涵和獨(dú)立 性,會產(chǎn)生這個(gè)需要,并且也能夠通過形式上的分離和提取來把內(nèi)涵給表現(xiàn)出來,這個(gè)過程離不開抽象。我不久意識到自己實(shí)際上在設(shè)計(jì)一個(gè)小規(guī)模的關(guān)于表達(dá)式計(jì) 算的框架。
表達(dá)式要支持加減乘除運(yùn)算符,這是基本的、立即想到的。或許還應(yīng)該支持平方,開方(sqrt),三角運(yùn)算符如 sin,cos 等。那么如果還有其它怎么辦,包括自定義的運(yùn)算符?你能確定考慮完備了嗎?像自定義的運(yùn)算符,是完全存在的、合理的需求。在數(shù)據(jù)采集系統(tǒng)中,我一度考慮引 入一個(gè) diff 運(yùn)算符,表明同一個(gè)累加型的采集量,在相距最近的兩次(即采集周期)采集的差值。以上的思考促使我決定,運(yùn)算符的設(shè)計(jì)必須是開放的。用戶(這里指的是用戶 程序員,下同)可以擴(kuò)展,增加新的運(yùn)算符。
表達(dá)式中允許含有變量。對于變量的支持貫穿到表達(dá)式解析,轉(zhuǎn)換,計(jì)算的全過程。在解析階段,應(yīng)該允許用戶使用適合他 / 她自己的變量語法,我不應(yīng)該事先實(shí)現(xiàn)基于某種特定語法的變量識別。
由于支持可擴(kuò)展的運(yùn)算符,未知的變量語法,甚至連基本的數(shù)值(象 123,12.3456,1.21E17)理論上也有多種類型和精度(Integer/Long/Float/Double/BigInteger /BigDecimal),這決定了無法提供一個(gè)固化的表達(dá)式解析方法。表達(dá)式解析也是需要可擴(kuò)展的。最好的結(jié)果是提供一個(gè)容易使用和擴(kuò)展的解析框架。對 于新的運(yùn)算符,新的變量語法,用戶在這個(gè)框架上擴(kuò)展,以提供增強(qiáng)的解析能力。
從抽象的角度來看,我打算支持的表達(dá)式僅由四種元素組成:括號(包括左右括號),運(yùn)算符,數(shù)值和變量。一個(gè)最終用戶給出的表達(dá)式字符串,解析通過后,可能生成了一個(gè)內(nèi)部表示的、便于后續(xù)處理的表達(dá)式,組成這個(gè)表達(dá)式的每個(gè)元素只能是以上四種之一。
回頁首
數(shù)值
一開始我寫了一個(gè)表達(dá)數(shù)值的類,叫做 Numeral。我為 Numeral 具體代表的是整數(shù)、浮點(diǎn)數(shù)還是雙精度數(shù)而操心。從比較模糊的意義上,我希望它能表達(dá)以上任何一種類型和精度的數(shù)值。但是我也希望,它能夠明確表達(dá)出代表的 具體是哪種類型和精度的數(shù)值,如果需要的話。甚至我想到 Numeral 最好也能表達(dá) BigInteger 和 BigDecimal(設(shè)想恰巧在某種場合下,我們需要解析和計(jì)算一個(gè)這樣的表達(dá)式,它允許的數(shù)值的精度和范圍很大,以至于 Long 或 Double 容納不下),否則在特別的場合下我們將遇到麻煩。在可擴(kuò)展性上,數(shù)值類不大像運(yùn)算符類,它應(yīng)該是成熟的,因而幾乎是不需要擴(kuò)展的。
經(jīng)過反復(fù)嘗試的混亂(Numeral 后來又經(jīng)過修改甚至重寫),我找到了一個(gè)明智的辦法。直接用 java.lang.Number 作為數(shù)值類(實(shí)際上這是一個(gè)接口)。我慶幸地看到,在 Java 中,Integer,Long,F(xiàn)loat,Double 甚至 BigInteger,BigDecimal 等數(shù)值類都實(shí)現(xiàn)了 java.lang.Number(下面簡稱 Number)接口,使用者把 Number 作為何種類型和精度來看待和使用,權(quán)利掌握在他 / 她的手中,我不應(yīng)該提前確定數(shù)值的類型和精度。選擇由 Number 類來表達(dá)數(shù)值,這看來是最好的、代價(jià)最小的選擇了,并且保持了相當(dāng)?shù)撵`活性。作為一個(gè)順帶的結(jié)果,Numeral 類被廢棄了。
回頁首
括號
在表達(dá)式中,括號扮演的角色是不可忽視的。它能改變運(yùn)算的自然優(yōu)先級,按照用戶所希望的順序進(jìn)行計(jì)算。我用 Bracket 類來表示括號,這個(gè)類可以看作是 final,因?yàn)樗恍枰獢U(kuò)展。括號分作括號和右括號,我把它們作為 Bracket 類的兩個(gè)靜態(tài)實(shí)例變量(并且是 Bracket 類僅有的兩個(gè)實(shí)例變量)。
public class Bracket { private String name; private Bracket(String name) { this.name = name; } public static final Bracket LEFT_BRACKET = new Bracket("("), RIGHT_BRACKET = new Bracket(")"); public String toString() { return name; } } |
回頁首
運(yùn)算符
運(yùn)算符的設(shè)計(jì)要求是開放的,這幾乎立即意味著它必須是抽象的。我一度猶豫運(yùn)算符是作為接口還是抽象類定義,最后我選擇的是抽象類。
public abstract class Operator { private String name; protected Operator(String name) { this.name = name; } public abstract int getDimension(); // throws ArithmeticException ? public abstract Number eval(Number[] oprands, int offset); public Number eval(Number[] oprands) { return eval(oprands,0); } public String toString() { return name; } } |
這個(gè)運(yùn)算符的設(shè)計(jì)包含二個(gè)主接口方法。通過 getDimention() 接口它傳達(dá)這么一個(gè)信息:運(yùn)算符是幾元的?即需要幾個(gè)操作數(shù)。顯然,最常見的是一元和二元運(yùn)算符。這個(gè)接口方法似乎也意味著允許有多于二元的運(yùn)算符,但是 對于多于二元的運(yùn)算符我沒有作更深入的考察。我不能十分確定基于棧的表達(dá)式的轉(zhuǎn)換和計(jì)算算法是否完全支持二元以上的運(yùn)算符。盡管有這么一點(diǎn)擔(dān)憂,我還是保 留目前的接口方法。
運(yùn)算符最主要的接口方法就是 eval(),這是運(yùn)算符的計(jì)算接口,反映了運(yùn)算符的本質(zhì)。在這個(gè)接口方法中要把所有需要的操作數(shù)傳給它,運(yùn)算符是幾元的,就需要幾個(gè)操作數(shù),這應(yīng)該是一 致的。然后,執(zhí)行符合運(yùn)算符含義的計(jì)算,返回結(jié)果。如果增加新的運(yùn)算符,用戶需要實(shí)現(xiàn)運(yùn)算符的上述接口方法。
回頁首
變量
從某種意義上說,變量就是"待定的數(shù)值"。我是否應(yīng)該設(shè)計(jì)一個(gè) Variable 類(或接口)?我的確這樣做了。變量什么時(shí)候,被什么具體數(shù)值替代,這些過程我不知道,應(yīng)該留給用戶來處理。我對于變量的知識幾乎是零,因此 Variable 類的意義就不大了。如果繼續(xù)保留這個(gè)類 / 接口,還給用戶帶來一個(gè)限制,他 / 她必須繼承或?qū)崿F(xiàn) Varibale 類 / 接口,因此不久之后我丟棄了 Variable。我只是聲明和堅(jiān)持這么一點(diǎn):在一個(gè)表達(dá)式中,如果一個(gè)元素不是括號,不是數(shù)值,也不是運(yùn)算符,那么我就把它作為變量看待。
回頁首
表達(dá)式解析接口
表達(dá)式解析所要解決的基本問題是:對于用戶給出的表達(dá)式字符串,要識別出其中的數(shù)值,運(yùn)算符,括號和變量,然后轉(zhuǎn)換成為內(nèi)部的、便于后續(xù)處理的表達(dá)式形式。我提供了一個(gè)一般的表達(dá)式解析接口,如下。
public interface Parser { Object[] parse(String expr) throws IllegalExpressionException; } |
在這個(gè)解析接口中我只定義了一個(gè)方法 parse()。表達(dá)式串作為輸入?yún)?shù),方法返回一個(gè)數(shù)組 Object[] 作為解析結(jié)果。如果解析通過的話,可以肯定數(shù)組中的元素或者是 Number,或者 Operator,或者 Bracket。如果它不是以上三種之一,就把它視為變量。
也許這樣把表達(dá)式解析設(shè)計(jì)的過于一般了。因?yàn)樗乇芰?如何解析"這個(gè)關(guān)鍵的問題而顯得對于用戶幫助不大。表達(dá)式究竟如何解析,在我看來是一個(gè)復(fù)雜的、甚至困難的問題。
主要困難在于,無法提供一個(gè)現(xiàn)成的,適用于各種表達(dá)式的解析實(shí)現(xiàn)。請考慮,用戶可能會增加新的運(yùn)算符,引入新的變量語法,甚至支持不同類型和 精度的數(shù)值處理。如前面所提到的,如果能設(shè)計(jì)出一個(gè)表達(dá)式解析框架就好了,讓用戶能夠方便地在這個(gè)框架基礎(chǔ)上擴(kuò)展。但是我沒能完全做到這一點(diǎn)。后面將提到 已經(jīng)實(shí)現(xiàn)的一個(gè)缺省的解析器(SimpleParser)。這個(gè)缺省實(shí)現(xiàn)試圖建立這樣一個(gè)框架,我覺得可能有一定的局限性。
回頁首
中綴表達(dá)式到后綴的轉(zhuǎn)換
這是通過一個(gè)轉(zhuǎn)換器類 Converter 來完成的。我能夠把轉(zhuǎn)換算法(以及下面的計(jì)算算法)獨(dú)立出來,讓它不依賴于運(yùn)算符或變量的擴(kuò)展,這得益于先前所做的基礎(chǔ)工作 - 對于表達(dá)式元素(數(shù)值,括號,運(yùn)算符和變量)的分析和抽象。算法的基本過程是這樣的(讀者可以從網(wǎng)上或相關(guān)書籍中查到,我略作改動,因?yàn)橹С肿兞康木?故):創(chuàng)建一個(gè)工作棧和一個(gè)輸出隊(duì)列。從左到右讀取表達(dá)式,當(dāng)讀到數(shù)值或變量時(shí),直接送至輸出隊(duì)列;當(dāng)讀到運(yùn)算符 t 時(shí),將棧中所有優(yōu)先級高于或等于 t 的運(yùn)算符彈出,送到輸出隊(duì)列中,然后 t 進(jìn)棧;讀到左括號時(shí)總是將它壓入棧中;讀到右括號時(shí),將靠近棧頂?shù)牡谝粋€(gè)左括號上面的運(yùn)算符全部依次彈出,送至輸出隊(duì)列后,再丟棄左括號。在 Converter 類中,核心方法 convert() 執(zhí)行了上述的算法,其輸入是中綴表達(dá)式,輸出是后綴表達(dá)式,完成了轉(zhuǎn)換的過程。
public abstract class Converter { public abstract int precedenceCompare(Operator op1, Operator op2) throws UnknownOperatorException; public Object[] convert(Object[] infixExpr) throws IllegalExpressionException, UnknownOperatorException { return convert(infixExpr, 0, infixExpr.length); } public Object[] convert(Object[] infixExpr, int offset, int len) throws IllegalExpressionException, UnknownOperatorException { if (infixExpr.length - offset < len) throw new IllegalArgumentException(); // 創(chuàng)建一個(gè)輸出表達(dá)式,用來存放結(jié)果 ArrayList output = new ArrayList(); // 創(chuàng)建一個(gè)工作棧 Stack stack = new Stack(); int currInputPosition = offset; // 當(dāng)前位置(于輸入隊(duì)列) System.out.println(" ----------- Begin conversion procedure --------------"); while (currInputPosition < offset + len) { Object currInputElement = infixExpr[currInputPosition++]; if (currInputElement instanceof Number) // 數(shù)值元素直接輸出 { output.add(currInputElement); System.out.println("NUMBER:"+currInputElement);//TEMP! } else if (currInputElement instanceof Bracket) // 遇到括號,進(jìn)棧或進(jìn)行匹配 { Bracket currInputBracket = (Bracket)currInputElement; if (currInputBracket.equals(Bracket.LEFT_BRACKET)) { // 左括號進(jìn)棧 stack.push(currInputElement); } else { // 右括號,尋求匹配(左括號) // 彈出所有的棧元素 ( 運(yùn)算符 ) 直到遇到 ( 左 ) 括號 Object stackElement; do { if (!stack.empty()) stackElement = stack.pop(); else throw new IllegalExpressionException("bracket(s) mismatch"); if (stackElement instanceof Bracket) break; output.add(stackElement); System.out.println("OPERATOR POPUP:"+stackElement);//TEMP! } while (true); } } else if (currInputElement instanceof Operator) { Operator currInputOperator = (Operator)currInputElement; // 彈出所有優(yōu)先級別高于或等于當(dāng)前的所有運(yùn)算符 // ( 直到把滿足條件的全部彈出或者遇到左括號 ) while (!stack.empty()) { Object stackElement = stack.peek(); if (stackElement instanceof Bracket) { break;// 這一定是左括號,沒有可以彈出的了 } else { Operator stackOperator = (Operator)stackElement; if (precedenceCompare(stackOperator, currInputOperator) >= 0) { // 優(yōu)先級高于等于當(dāng)前的,彈出(至輸出隊(duì)列) stack.pop(); output.add(stackElement); System.out.println("OPERATOR:"+stackElement);//TEMP! } else { // 優(yōu)先級別低于當(dāng)前的,沒有可以彈出的了 break; } } } // 當(dāng)前運(yùn)算符進(jìn)棧 stack.push(currInputElement); } else //if (currInputElement instanceof Variable) // 其它一律被認(rèn)為變量,變量也直接輸出 { output.add(currInputElement); System.out.println("VARIABLE:"+currInputElement);//TEMP! } } // 將棧中剩下的元素 ( 運(yùn)算符 ) 彈出至輸出隊(duì)列 while (!stack.empty()) { Object stackElement = stack.pop(); output.add(stackElement); System.out.println("LEFT STACK OPERATOR:"+stackElement);//TEMP! } System.out.println("------------ End conversion procedure --------------"); return output.toArray(); } } |
讀者可能很快注意到,Converter 類不是一個(gè)具體類。既然算法是成熟穩(wěn)定的,并且我們也把它獨(dú)立出來了,為什么 Converter 類不是一個(gè)穩(wěn)定的具體類呢?因?yàn)樵谵D(zhuǎn)換過程中我發(fā)覺必須要面對一個(gè)運(yùn)算符優(yōu)先級的問題,這是一個(gè)不可忽視的問題。按照慣例,如果沒有使用括號顯式地確定計(jì) 算的先后順序,那么計(jì)算的先后順序是通過比較運(yùn)算符的優(yōu)先級來確定的。因?yàn)槲覠o法確定用戶在具體使用時(shí),他 / 她的運(yùn)算符的集合有多大,其中任意兩個(gè)運(yùn)算符之間的優(yōu)先級順序是怎樣的。這個(gè)知識只能由用戶來告訴我。說錯(cuò)了,告訴給 Converter 類,所以 Converter 類中提供了一個(gè)抽象的運(yùn)算符比較接口 precedenceCompare() 由用戶來實(shí)現(xiàn)。
在一段時(shí)間內(nèi),我為如何檢驗(yàn)表達(dá)式的有效性而困惑。我意識到,轉(zhuǎn)換通過了并不一定意味著表達(dá)式是必定合乎語法的、有效的。甚至接下來成功計(jì)算 出后綴表達(dá)式的值,也并不能證明原始表達(dá)式是有效的。當(dāng)然,在某些轉(zhuǎn)換失敗或者計(jì)算失敗的情況下,例如運(yùn)算符的元數(shù)與操作數(shù)數(shù)量不匹配,左右括號不匹配 等,則可以肯定原始表達(dá)式是無效的。但是,證明一個(gè)表達(dá)式是有效的,條件要苛刻的多。遺憾的是,我沒能找到檢驗(yàn)表達(dá)式有效性的理論根據(jù)。
回頁首
計(jì)算后綴表達(dá)式
這是通過一個(gè)計(jì)算器類 Calculator 來完成的。Calculator 類的核心方法是 eval(),傳給它的參數(shù)必須是后綴表達(dá)式。在調(diào)用本方法之前,如果表達(dá)式中含有變量,那么應(yīng)該被相應(yīng)的數(shù)值替換掉,否則表達(dá)式是不可計(jì)算的,將導(dǎo)致拋 出 IncalculableExpressionException 異常。算法的基本過程是這樣的:創(chuàng)建一個(gè)工作棧,從左到右讀取表達(dá)式,讀到數(shù)值就壓入棧中;讀到運(yùn)算符就從棧中彈出 N 個(gè)數(shù),計(jì)算出結(jié)果,再壓入棧中,N 是該運(yùn)算符的元數(shù);重復(fù)上述過程,最后輸出棧頂?shù)臄?shù)值作為計(jì)算結(jié)果,結(jié)束。
public class Calculator { public Number eval(Object[] postfixExpr) throws IncalculableExpressionException { return eval(postfixExpr, 0, postfixExpr.length); } public Number eval(Object[] postfixExpr, int offset, int len) throws IncalculableExpressionException { if (postfixExpr.length - offset < len) throw new IllegalArgumentException(); Stack stack = new Stack(); int currPosition = offset; while (currPosition < offset + len) { Object element = postfixExpr[currPosition++]; if (element instanceof Number) { stack.push(element); } else if (element instanceof Operator) { Operator op = (Operator)element; int dimensions = op.getDimension(); if (dimensions < 1 || stack.size() < dimensions) throw new IncalculableExpressionException( "lack operand(s) for operator '"+op+"'"); Number[] operands = new Number [dimensions]; for (int j = dimensions - 1; j >= 0; j--) { operands[j] = (Number)stack.pop(); } stack.push(op.eval(operands)); } else throw new IncalculableExpressionException("Unknown element: " +element); } if (stack.size() != 1) throw new IncalculableExpressionException("redundant operand(s)"); return (Number)stack.pop(); } } |
回頁首
缺省實(shí)現(xiàn)
前面我主要討論了關(guān)于表達(dá)式計(jì)算的設(shè)計(jì)。一個(gè)好的設(shè)計(jì)和實(shí)現(xiàn)中常常包括某些缺省的實(shí)現(xiàn)。在本案例中,我提供了基本的四則運(yùn)算符的實(shí)現(xiàn)和一個(gè)缺省的解析器實(shí)現(xiàn)(SimpleParser)。
運(yùn)算符
實(shí)現(xiàn)了加減乘除四種基本運(yùn)算符。 需要說明一點(diǎn)的是,對于每種基本運(yùn)算符,當(dāng)前缺省實(shí)現(xiàn)只支持 Number 是 Integer,Long,F(xiàn)loat,Double 的情況。并且,需要關(guān)注一下不同類型和精度的數(shù)值相運(yùn)算時(shí),結(jié)果數(shù)值的類型和精度如何確定。缺省實(shí)現(xiàn)對此有一定的處理。
解析器
這個(gè)缺省的解析器實(shí)現(xiàn),我認(rèn)為它是簡單的,故取名為 SimpleParser。其基本思想是:把表達(dá)式看作是由括號、數(shù)值、運(yùn)算符和變量組成,每種表達(dá)式元素都可以相對獨(dú)立地進(jìn)行解析,為此提供一種表達(dá)式 元素解析器(ElementParser)。SimpleParser 分別調(diào)用四種元素解析器,完成所有的解析工作。
ElementParser 提供了表達(dá)式元素級的解析接口。四種缺省的表達(dá)式元素解析器類 BasicNumberParser, BasicOperatorParser, DefaultBracketParser,DefaultVariableParser 均實(shí)現(xiàn)這個(gè)接口。
public interface ElementParser { Object[] parse(char[] expr, int off); } |
解析方法 parse() 輸入?yún)?shù)指明待解析的串,以及起始偏移。返回結(jié)果中存放本次解析所得到的具體元素(Number、Operator、Bracket 或者 Object),以及解析的截止偏移。本次的截至偏移很可能就是下次解析的起始偏移,如果不考慮空白符的話。
那么在整個(gè)解析過程中,每種元素解析器何時(shí)被 SimpleParser 所調(diào)用?我的解決辦法是:它依次調(diào)用每種元素解析器。可以說這是一個(gè)嘗試的策略。嘗試的先后次序是有講究的,依次是:變量解析器 - 〉運(yùn)算符解析器 - 〉數(shù)值解析器 - 〉括號解析器。
為什么執(zhí)行這么一種次序?從深層次上反映了我的一種憂慮。這就是:表達(dá)式的解析可能是相當(dāng)復(fù)雜的。可能有這樣的表達(dá)式,對于它不能完全執(zhí)行" 分而治之"的解析方式,因?yàn)榇嬖谛枰?整體解析"的地方。例如:考慮"diff(@TotalBytesReceived)"這樣的一個(gè)子串。用戶可能用 它可能表達(dá)這樣的含義:取采集量 TotalBytesReceived 的前后兩次采集的差值。diff 在這里甚至不能理解成傳統(tǒng)意義上的運(yùn)算符。最終合理的選擇很可能是,把"diff(@TotalBytesReceived)"整個(gè)視為一個(gè)變量來解析和 處理。在這樣的情況下,拆開成"diff","(","@bytereceived",")"分別來解析是無意義的、錯(cuò)誤的。
這就是為什么變量解析器被最先調(diào)用,這樣做允許用戶能夠截獲、重新定義超越常規(guī)的解析方式以滿足實(shí)際需要。實(shí)際上,我安排讓變化可能性最大的 部分(如變量)其解析器被最先調(diào)用,最小的部分(如括號)其解析器被最后調(diào)用。在每一步上,如果解析成功,那么后續(xù)的解析器就不會被調(diào)用。如果在表達(dá)式串 的某個(gè)位置上,經(jīng)過所有的元素解析器都不能解析,那么該表達(dá)式就是不可解析的,將拋出 IllegalExpressionException 異常。
回頁首
擴(kuò)展實(shí)現(xiàn)
由于篇幅的關(guān)系,不在此通過例子討論擴(kuò)展實(shí)現(xiàn)。這并不意味著目前沒有一個(gè)擴(kuò)展實(shí)現(xiàn)。在前面提到的數(shù)據(jù)采集項(xiàng)目中,由于基本初衷就是支持特別語 法的變量,結(jié)果我實(shí)現(xiàn)了一個(gè)支持變量的擴(kuò)展實(shí)現(xiàn),并且支持了一些其他運(yùn)算符,除四則運(yùn)算符之外。相信讀者能夠看出,我所做的工作體現(xiàn)和滿足了可擴(kuò)展性。而 擴(kuò)展性主要體現(xiàn)在運(yùn)算符和變量上。
回頁首
總結(jié)
對于表達(dá)式計(jì)算,我提出的要求有一定挑戰(zhàn)性,但也并不是太高。然而為了接近或達(dá)到這個(gè)目標(biāo),在設(shè)計(jì)上我頗費(fèi)了一番功夫,數(shù)易其稿。前面提到, 我丟棄了 Numeral 類,Variable 類。實(shí)際上,還不止這些。我還曾經(jīng)設(shè)計(jì)了 Element 類,表達(dá)式在內(nèi)部就表示成一個(gè)數(shù)組 Element[]。在 Element 類中,通過一個(gè)枚舉變量指明它包含的到底是什么類型的元素(數(shù)值,括號,運(yùn)算符還是變量)。但是我嗅出這個(gè)做法不夠清晰、自然(如果追根究底,可以說不夠 面向?qū)ο蠡詈髞G棄了這個(gè)類。相應(yīng)地,Element[] 被更直接的 Object[] 所代替。
我的不斷改進(jìn)的動力,就是力求讓設(shè)計(jì)在追求其它目標(biāo)的同時(shí)保持簡潔。注意,這并不等于追求過于簡單!我希望我的努力讓我基本達(dá)到了這個(gè)目標(biāo)。 我除去了主要的耦合性,讓相對不變的部分 - 表達(dá)式轉(zhuǎn)換和計(jì)算部分獨(dú)立出來,并把變化的部分 - 運(yùn)算符和變量,開放出來。雖然在表達(dá)式解析上我還留有遺憾,表達(dá)式的一般解析接口過于寬泛了,未能為用戶的擴(kuò)展帶來實(shí)質(zhì)性的幫助。所幸缺省解析器的實(shí)現(xiàn)多 少有所彌補(bǔ)。
最后,我希望這個(gè)關(guān)于表達(dá)式計(jì)算的設(shè)計(jì)和實(shí)現(xiàn)能夠?yàn)樗怂煤蛿U(kuò)展。我愿意看到它能經(jīng)受住考驗(yàn)。
關(guān)于作者
劉 源是上海復(fù)旦光華電信部的軟件工程師。他在C++、java等相關(guān)技術(shù)方面具有10年的實(shí)踐經(jīng)驗(yàn)。他對于面向?qū)ο蟮乃囆g(shù)和方法論,軟件的復(fù)雜性等方面有深 入的理解。他是一個(gè)并不喜歡聲張但是追求卓越的人。他希望看到軟件公司能夠意識到,然后逐漸學(xué)習(xí)和把握管理的藝術(shù),營造自己的文化。他希望看到周圍多一些 勤于思考、腳踏實(shí)地的開發(fā)人員和管理人員。