在我做過的一個針對網絡設備和主機的數據采集系統中,某些采集到的數據需要經過一定的計算后才保存入庫,而不是僅僅保存其原始值。為了提供給用戶最大的靈活性,我設想提供一個用戶界面,答應用戶輸入計算表達式(或者稱為計算公式)。這樣,除了需要遵從少量的規則,用戶可以得到最大的靈活性。
這樣的表達式具有什么特點呢?它一般不是純的可立即計算的表達式(簡單的如:1+2*3-4)。它含有我稱為變量的元素。變量一般具有非凡的內定的語法,例如可能用"@totalmemory"表示設備或主機(下面簡稱為設備)的物理內存總數,那么表達式"(@totalmemory-@freememory)/@totalmemory*100"就表示設備當前內存使用率百分比。假如與告警系統聯系起來,監測此值超過80系統就發出Warning,那么這就成為一件有意義的事情。不同種類的采集數據入庫前可能需要經過復雜度不同的計算。但顯然,最后求值的時候,必須將那些特定的變量用具體數值(即采集到的具體數值)替換,否則表達式是不可計算的。這個過程是在運行時發生的。
我認為表達式計算是個一般性的話題,并且也不是一個新的話題。我們可能在多處碰到它。我在讀書的時候編寫過一個表達式的轉換和計算程序,當時作為課余作業。我看到過一些報表系統,不管它是單獨的,還是包含在MIS系統、財務軟件中,很多都支持計算公式。我認為這些系統中的計算公式和我所碰到的問題是大致相同的。對我來說,我在數據采集項目中碰到這個問題,下次可能還會在其他項目中碰到它。既然已經不止一次了,我希望找到一個一般性的解決方案。
在設計和實現出第一個版本之后,我自己感覺不很滿足。隨后我花點時間上網搜索了一下(要害字:表達式 中綴 后綴 or EXPRession Infix Postfix)。令人稍感失望的是,所找到的一些關于表達式的轉換、計算的程序不能滿足我的要求。不少這樣的程序僅僅支持純的可立即計算的表達式,不支持變量。而且,表達式解析和計算是耦合在一起的,很難擴展。增加新的運算符(或新的變量語法)幾乎必定要修改源代碼。在我看來,這是最大的缺陷了(實際上,當年我編寫的表達式轉換和計算的程序,雖然當時引以自豪,但是現在看來也具有同樣的缺陷)。但是,表達式的轉換和計算本身有成熟的、基于棧的的經典算法,許多計算機書籍或教材上都有論述。人們以自然方式書寫的表達式是中綴形式的,先要把中綴表達式轉換為后綴表達式,然后計算后綴表達式的值。我打算仍然采用這個經典的過程和算法。
既然表達式的轉換和計算的核心算法是成熟的,我渴望把它們提取出來,去除(與解析相關的)耦合性!試想,假如事物具有相對完整的內涵和獨立性,會產生這個需要,并且也能夠通過形式上的分離和提取來把內涵給表現出來,這個過程離不開抽象。我不久意識到自己實際上在設計一個小規模的關于表達式計算的框架。
表達式要支持加減乘除運算符,這是基本的、立即想到的?;蛟S還應該支持平方,開方(sqrt),三角運算符如sin,cos等。那么假如還有其它怎么辦,包括自定義的運算符?你能確定考慮完備了嗎?像自定義的運算符,是完全存在的、合理的需求。在數據采集系統中,我一度考慮引入一個diff運算符,表明同一個累加型的采集量,在相距最近的兩次(即采集周期)采集的差值。以上的思考促使我決定,運算符的設計必須是開放的。用戶(這里指的是用戶程序員,下同)可以擴展,增加新的運算符。
表達式中答應含有變量。對于變量的支持貫穿到表達式解析,轉換,計算的全過程。在解析階段,應該答應用戶使用適合他/她自己的變量語法,我不應該事先實現基于某種特定語法的變量識別。
由于支持可擴展的運算符,未知的變量語法,甚至連基本的數值(象123,12.3456,1.21E17)理論上也有多種類型和精度(Integer/Long/Float/Double/BigInteger/BigDecimal),這決定了無法提供一個固化的表達式解析方法。表達式解析也是需要可擴展的。最好的結果是提供一個輕易使用和擴展的解析框架。對于新的運算符,新的變量語法,用戶在這個框架上擴展,以提供增強的解析能力。從抽象的角度來看,我打算支持的表達式僅由四種元素組成:括號(包括左右括號),運算符,數值和變量。一個最終用戶給出的表達式字符串,解析通過后,可能生成了一個內部表示的、便于后續處理的表達式,組成這個表達式的每個元素只能是以上四種之一。
一開始我寫了一個表達數值的類,叫做Numeral。我為Numeral具體代表的是整數、浮點數還是雙精度數而操心。從比較模糊的意義上,我希望它能表達以上任何一種類型和精度的數值。但是我也希望,它能夠明確表達出代表的具體是哪種類型和精度的數值,假如需要的話。甚至我想到Numeral最好也能表達BigInteger和BigDecimal(設想恰巧在某種場合下,我們需要解析和計算一個這樣的表達式,它答應的數值的精度和范圍很大,以至于Long或Double容納不下),否則在非凡的場合下我們將碰到麻煩。在可擴展性上,數值類不大像運算符類,它應該是成熟的,因而幾乎是不需要擴展的。
經過反復嘗試的混亂(Numeral后來又經過修改甚至重寫),我找到了一個明智的辦法。直接用java.lang.Number作為數值類(實際上這是一個接口)。我慶幸地看到,在Java中,Integer,Long,Float,Double甚至BigInteger,BigDecimal等數值類都實現了java.lang.Number(下面簡稱Number)接口,使用者把Number作為何種類型和精度來看待和使用,權利把握在他/她的手中,我不應該提前確定數值的類型和精度。選擇由Number類來表達數值,這看來是最好的、代價最小的選擇了,并且保持了相當的靈活性。作為一個順帶的結果,Numeral類被廢棄了。
新聞熱點
疑難解答