使用Java腳本構建強大、靈活的公式管理系統
在很多中大型的應用中,如SCM(供應鏈管理)、CRM(客戶關系管理)和ERP(企業資源計劃)等,使用者往往要根據自身的需求,靈活的對某一些參數值進行變更,使得按照某固定公式計算的結果符合目前的情況。如不同時期商品價格的折扣率需要根據實際情況進行調整,或者職員的獎金百分比要根據公司的業績而定。這就需要有一個強大的公式管理機制來對一些參數進行靈活調整。
前言
客戶的需求是在不斷變化的。雖然他們說現在他們公司的職員獎金應該就是按照那個公式計算,但是過了幾個月他們會告訴你這個公式並不是很合理,還需要加一些參數。你可能會說,這個沒問題,我們可以改程序。但是當這樣的變更不是一次一個的發生,而是頻繁的、大量的出現時,你也許就在想應該有一個公式管理系統來完成這些瑣碎卻很重要的變更了。是的,在很多系統中已經這樣做了。這裡將介紹一種簡單的、易擴展的公式管理系統,它采用簡單靈活的BeanShell腳本機制,並且結合JDOM技術來實現。在閱讀本文以前,你需要對BeanShell和JDOM有所了解。
BeanShell簡介
BeanShell 是一種Java 解釋器,它包含的腳本語言基本與Java 語言兼容,具有體積小、簡單、符合Java 風格等特點。本文不是介紹BeanShell的語法和用法,而是基於BeanShell腳本實現一個公式管理器來說明BeanShell的強大腳本功能,從而簡化Java程序員的編程工作,使他們更深入的了解什麼時候使用BeanShell技術將使得構建的系統更靈活。你可以閱讀參考資料了解更多 BeanShell的知識。類似的Java腳本技術還有DynamicJ等,想要詳細了解它們的更多信息,請查閱後面的參考資料。(請注意:這裡的 Java腳本不是Javascript。)
JDOM簡介
JDOM使得用Java操作xml文件更輕松。這裡使用xml文件格式對用戶的自定義公式庫進行存儲,即簡單又容易管理。利用JDOM技術,能夠簡單、快速的完成這個任務。如果你想詳細了解有關JDOM的知識,請查閱文章後面的參考資料。
公式管理系統的目標
公式管理系統實現的主要目標是:用戶可以根據自己的需要自定義公式,包括添加、修改和刪除公式或者公式包含的參數;提供接口使得用戶或其它系統能夠利用公式庫中的公式進行計算求值。從以上系統的主要功能,可以知道該系統主要包含兩個用例:自定義公式和計算表達式。自定義公式是用戶預先定義好某公式包含的參數(包括參數名、參數類型等),然後將這些參數用運算符按照一定的法則組合成所需要的公式。公式定義好後,將被保存到公式庫中,供以後用戶或其它系統計算時調用。這是管理者根據自身的需求,靈活更改公式或公式包含的參數的系統功能;計算表達式是用戶給相應參數賦值,然後指定要遵照的公式進行計算求值。這是該系統提供給使用者的外部接口。一般的,使用者只需要提供要遵照的公式ID和相關的參數值,就可以調用該接口進行計算。該系統的用例圖如下:
圖1. 公式管理系統用例圖
公式管理系統的設計
明確了系統的目標,我們第一步要做的就是找出系統的核心類。首先,我們需要一個公式管理器(FormulaParser),它負責將用戶自定義的公式轉換成系統格式並保存到公式庫中,它提供外部接口,供外部需要根據公式計算時使用;我們還需要一個計算器(Calculator),它的工作就是進行計算。它包含了所有的運算類型,如加、減、乘或除,當然還可以包含大(小)於、等於等其它運算,可以根據需要進行擴展;最後,我們還需要一個公式類(Formula)。顧名思義,公式對象就代表一個公式。公式管理器就是一個管理者,它既要接受用戶的自定義公式,又要提供接口給外部系統,調用相應的公式並用計算器進行計算。該系統的核心類如下圖所示。
圖2. 核心類圖
很顯然,這樣的類設計有利於BeanShell的實施(筆者一直認為:設計人員中至少要包括一位語言專家,這樣才能設計得更靈活、更簡單,為編碼階段做鋪墊)。那麼,BeanShell應用於系統哪一部分將最為合適呢?
何時應用BeanShell
答案是要根據實際情況而定。你可能會說這是廢話,但是這是事實的真相。一般的,當系統的某個部分變化很多,而且需要根據實際情況動態的調用一些方法和參數,那麼你可以試著去實施BeanShell到你的系統中。如果你發現問題變得簡單而且靈活性大大提高,那麼BeanShell就值得你去試一試;相反的,如果你發現問題不僅沒有簡化,甚至變得更加復雜,那麼你就要考慮一下你的方法是否合適,或者找一些BeanShell應用專家交流一下。注意,我們不是追求新技術,而是追求一種能夠提高我們生產效率的新方法。
那麼,在這個公式管理系統中我們如何應用 BeanShell技術呢?從以上的分析我們知道,該系統中用戶需要將自定義的公式保存起來,以供外部系統計算時調用。設想一下,當外部系統調用該接口時,它至少應該傳入兩個參數:一個是計算需要遵照的公式ID;另外一個是一個Hashtable對象,存放"參數名-參數值"列表。 FormulaParser類接收到這些參數後,根據公式ID從公式庫中裝載指定的公式,然後將參數值賦值給公式包含的對應參數,最後根據定義好的公式表達式進行計算求值。在沒有BeanShell以前,我們一般通過分析字符竄的方式完成自定義公式到系統格式公式的轉換、參數賦值、表達式求值等,這樣做雖然比較直接,但是很復雜,尤其當運算符、運算優先級很多的時候。筆者就曾經見過一個1000多行代碼的公式解析器,而且功能很簡單。原因就在於它的大部分代碼是在做字符竄的解析。那麼,你應該抓住了問題的所在。我們試試讓BeanShell來完成這些解析工作。
首先你必須對BeanShell腳本的語法有所了解,並且應該熟悉一下它的文檔中的幾個例子。然後我們將重點放在BeanShell應用的地方�D�D外部接口caculateByFormula(Formula formula, Hashtable parameters)。其中的兩個參數,formula是計算要遵照的公式,parameters是"參數名-參數值"列表,它的返回值是計算的結果。假設公式庫中我們已經定義好了兩個公式,分別是商品折扣後價格公式和職員獎金計算公式。公式庫xml文件如下所示:
清單1.
<formulas>
<formula id="1001" name="F_DISCOUNT">
<parameters>
<parameter type="double" name="price"/>
<parameter type="double" name="discount"/>
</parameters>
<script>result= Calculator.mutiply(price, discount)</script>
</formula>
<formula id="1002" name="F_BONUS">
<parameters>
<parameter type="double" name="sale"/>
<parameter type="double" name="score"/>
</parameters>
<script>result= Calculator.add(Calculator.mutiply(sale, 0.1),
Calculator.mutiply(score, 10000))</script>
</formula>
</formulas>
公式庫中每個公式有一個唯一的ID來標識,並且有一個易記憶的名字。每個公式包含一個parameters元素和一個script元素,它們分別對應公式包含的參數列表和公式計算腳本。公式計算腳本就是BeanShell腳本。這樣做的好處是:無需對表達式字符竄的解析,用BeanShell解釋器來完成對腳本的求值。下面的代碼簡潔的完成了對表達式的求值:
清單2.
public double caculateByFormula(Formula formula, Hashtable parameters) {
double result=0.0;
try
{
Interpreter i = new Interpreter();
// 實例化一個BeanShell解釋器
i.eval("import parse.*;");
//引用公式管理系統
Vector para= formula.getParameters();
//獲取公式中包含的參數列表
Iterator it= para.iterator();
//設置參數值
while (it.hasNext()){
String[] dec= (String[])it.next();
String declare= dec[1]+ " "+ dec[0];
i.eval(declare);
String value= ((Double)parameters.get(dec[0])).toString();
if (value != null){
String assign_value= dec[0]+ "="+ value;
i.eval(assign_value);
}else{
System.out.println("caculateByFormula():"+ dec[0]+
"參數名不符或改參數不存在"); System.exit(1);
}
}
//參數設置成功,根據公式計算腳本進行計算,僅用了一行代碼就完成了求值過程,
BeanShell值得你去了解 i.eval(formula.getScript());
Double rst= (Double)i.get("result");
result= rst.doubleValue();
}catch(Exception e){
System.out.println("caculateByFormula():"+ e.getMessage());
}
return result;
}
也許你曾經用解析字符竄的方法做過類似的編程工作,當你看到這裡僅用一行代碼就完成了對表達式的求值,是不是很驚訝?首先,實例化一個BeanShell解釋器,然後將要使用的系統包import 進來,Interpreter.eval()方法就是對括號內的腳本求值或者說是解釋執行腳本。然後,將formula的參數取出來,用 BeanShell腳本方式聲明這些參數,再將parameters中參數對應的值再次用BeanShell腳本的方式賦值給對應的變量。請注意,如果這時候傳入的參數名不正確或者是參數名對應的參數類型不符,就會拋出系統異常。如果參數設置成功,則調用formula的公式計算腳本。僅用一行代碼,BeanShell完成了大部分的工作:i.eval(formula.getScript())。最後,從解釋器中取得計算結果,該結果存放在 result變量中(可以查看清單1.中公式計算腳本)。現在是不是覺得BeanShell是一個好幫手?只要使用恰當,BeanShell將幫助你大大簡化你的編程工作,這是一件非常快樂的事情。
另外,裝載formula使用了JDOM技術,它從公式庫中找到對應的公式,然後將該公式的參數列表以及計算腳本讀出來組裝成一個公式對象。見如下代碼:
清單3.
public Formula loadFormula(String formulaID) {
Vector paras= new Vector();
try{
SAXBuilder builder= new SAXBuilder();
Document doc= builder.build(prefix+ "Formulas.xml");
//prefix是一個字符竄,用來指定公式庫實際所在的位置
Element root= doc.getRootElement();
List formulas= root.getChildren("formula");
Iterator it= formulas.iterator();
Element formula= null;
while( it.hasNext()){
formula= (Element)it.next();
if(formula.getAttributeValue("id").equals(formulaID)){
break;
}
}
//獲取參數列表
List parameters= formula.getChild("parameters").getChildren();
Iterator itp= parameters.iterator();
while(itp.hasNext()){
String[] s_para= new String[2];
Element e_para= (Element)itp.next();
s_para[0]= e_para.getAttributeValue("name");
s_para[1]= e_para.getAttributeValue("type");
paras.add(s_para);
}
Element script= formula.getChild("script");
String s_script= script.getTextTrim();
return new Formula(s_script, paras);
//將讀出的信息組裝成一個公式對象
}catch(Exception e){
System.out.println("loadFormula():"+ e.getMessage());
}
return null;
}
更好的了解該系統
上面介紹了系統應用BeanShell的部分,也就是系統的外部接口實現部分。你可能覺得有些迷惑,不是要自定義公式嗎?怎麼公式庫早就有公式了呢?其實細心的讀者早就發現,要自定義公式完成系統的另外一個重要功能並不是什麼難事,你甚至可以想到直接編輯公式庫來添加、修改、刪除公式。當然你可以開發一個友好易用的自定義公式界面,你完全可以這樣做。你需要完成的只是將用戶輸入的自定義公式信息(包括公式的參數及參數類型,運算表達式)轉換成公式庫中的參數列表和公式計算腳本。如果你要這樣做,你不可避免的陷入到字符竄的解析當中去了。不過建議不要這樣做,因為用戶來寫公式計算腳本將是可行的。再看看公式庫的兩個公式計算腳本,相信你會同意這一點。因為本文的討論重點是BeanShell應用,所以這部分工作不做詳細討論,讀者可以選擇一個合適的方式來完成自定義公式的用戶接口。下面是該系統的循序圖和FormulaParser的狀態圖,能夠幫助你更好的理解該系統。
圖3. 調用公式計算外部接口
圖4. FormulaParser狀態圖
最後,我們給出一個測試用例來說明如何使用該系統。公式庫中有兩個公式,一個用來計算商品折扣價格,一個用來計算職員獎金。我們將公式包含的參數值傳給系統外部接口。見下面代碼:
清單4.
FormulaParser fp= new FormulaParser();
Hashtable paras= new Hashtable();
paras.put("price", new Double(100.0));
//價格paras.put("discount", new Double(0.9));
//折扣率為0.9
System.out.println("計算結果:"+ fp.caculateByFormula(fp.loadFormula("1001"), paras));
//遵照公式1001計算,計算預期結果為90.0
FormulaParser fp1= new FormulaParser();
Hashtable paras1= new Hashtable();
paras1.put("sale", new Double(11000.0));
//銷售額
paras1.put("score", new Double(0.8));
//表現得分
System.out.println("計算結果:"+ fp1.caculateByFormula(fp1.loadFormula("1002"), paras1));
// 遵照公式1002計算,計算預期結果為9100.0
程序輸出為:
計算結果:90.0
計算結果:9100.0
與預期結果完全一致。開發環境為JDK1.3.1。
結束語
這就是BeanShell給我們帶來的奇妙體驗。並且,基於BeanShell的公式管理系統是一個很有用的工具。你可以試著將更多的運算法則加入到系統的計算器中,試著擴展該公式系統以集成到你目前的工作當中,你也可以提供一個非常友好的界面給用戶,讓他們輕松的定制自己的公式。一個增強功能的公式管理系統已經成功應用到筆者參與的一個電信系統中。當然,本文的方法可能不是最好的,歡迎你與我討論。系統相關的源代碼你可以在參考資料中找到。衷心希望“開源世界裡充滿了思想者。”