本文作爲初入ERP行業的新人的防坑指南,講解了一些常見犯的錯,這樣也少走一些彎路,如果你是老鳥,請繞過 :-)
本文關聯的代碼使用kotlin編寫,請自行轉換爲c#、java等你熟悉的語言,表述的坑在各個語言基本都是一樣的。
不用使用單精度和雙精度類型
1 @Test 2 fun Test1(){ 3 val a : Double = 0.3 4 var b : Double = 0.0 5 for (i in 0..9){ 6 b += a 7 } 8 assert(b == 3.0) 9 }
你認爲這個測試用例會通過嗎?
是的,他的確不能通過,你仔細看看b的結果,你會發現b很接近3但不是3,關於這個問題有很多兄弟有詳細的解釋,我這裏就不重複了。
在ERP中,這種錯誤可不能犯,不然可憐的財務人員就發現帳不平了,除非你是想讓財務小妹天天找你套近乎。 囧。
所以凡是處理錢、數量、比例等等數值有關的,你應該用decimal類型(C#是decimal,java是BigDecimal),這裏我想吐槽一下,java爲什麼要設計那麼大一個BigDecimal?就不能設計一個折中的Decimal嗎?(Java老鳥請指教)。
好吧,關鍵點來了,前端的小朋友注意了,javaScript中內置的number不是decimal哦,所以避免在前端算賬了,甚至不能用number存儲用戶輸入的數據,而是用字符串。或者用一些第三方庫解決(比如decimal.js)。
要爲數值檢查範圍
這個是我剛參加工作時,犯的一個錯誤,現在還記憶猶新,當時我用VB設計一個POS 收銀程序,可是客戶剛上線一個月後打電話來,統計報表出錯,報:“數值溢出”,我一愣,這得多大的收入啊,能把數值溢出來。
後來經過仔細排查,得到問題的原因,在收銀的收錢環節,有個界面收銀員會錄入用戶付款多少,然後軟件計算應該找零多少,有點像這樣的:
總金額: 6 元
付款: 10 元
找零:4 元
就是在這個界面中,掃描槍經常無意間掃描到商品,你要知道,掃描槍對於電腦來說就是一個鍵盤,掃描一個商品條碼就是模擬鍵盤錄入一堆數字,並且幫你按回車鍵。然後就悲劇了,我們的POS機這個時候就可能變成了收款20億,找零19億9999萬。。。(⊙o⊙)…你們好有錢哦。
這些信息也會進入數據庫,雖然不會影響最後的收入,但是我們的統計報表中會用到這個字段,就“數值溢出”了。
所以後來的辦法就是檢查用戶錄入的數值不能超過總金額太多,即做範圍檢查。
可能你會認爲,這是因爲有條碼槍這個特殊設備,我們做的普通軟件都是在辦公室用的,或者現在用的是高大上的手機,沒有你說的事。那麼我說,to yang to simple。
首先用戶會自己加裝條碼槍,其次,你知道鍵盤會被諸如手機這樣的東西丟在上面,然後不幸鍵入一排111111111111嗎?更加不幸的是,好多領導是不看訂單內容直接審覈的。
so,本着對用戶負責的太多,還是多做最大範圍檢查吧。
負數檢查
我們剛纔聊到要防止用戶輸入很大的值,其實我們也應該防止用戶輸入負數,我們說很大的值可能是用戶無意間輸入的,而負數就是用戶故意輸入的,有些用戶不熟悉ERP軟件,在處理退貨等操作時,會很“聰明”的輸入數量爲負數,從而達到退貨處理的目的,當然,你設計的軟件也很“愚蠢”的通過了負數。可能用戶覺得他很厲害,而你要爲這個你沒有考慮到的數值加班調整數據庫了。
關於在ERP中是否允許使用負數,其實是存在爭議的,有些ERP軟件會利用負數實現對舊賬的衝正處理,對此,我保留我的意見。我的觀點是,讓用戶永遠輸入正數,然後用明確的衝正、退回等指令,讓用戶知道他在幹什麼,也讓你在設計很多流程時不必處處小心負數。
溢出檢查
這可能不算一個大坑,但作爲知識,你還是需要知道這一點,上代碼:
1 @Test 2 fun Test2() { 3 val a = Int.MAX_VALUE - 3 4 val b = 5 5 val c = a + b 6 assert(c == -2147483647) 7 }
正如你看到的,一個很大的值,在加法超過邊界後是不會出錯的,而是“循環”到負數了。
我知道c#可用用checked{}來強制某段代碼做溢出檢查的,但似乎java沒有內置的機制(請java老鳥指正)。
其實在ERP中,如果你做好了前面的範圍檢查,這個溢出檢查基本上是不需要的,但如果你沒有做好檢查,就可能會造成計算結果不正確,比如累加的結果是負數。
作爲額外的甜點,我們其實可以充分利用這個缺點,比如計算兩個時間差多少毫秒時,就是利用操作系統的一個特定API,而那個API用的是int32,所以多少天后這個數值會不斷循環的,而這不會影響我們用減法計算差額,不信你試試。
格式化小數點
在設計ERP時,很多界面是需要顯示金額的,而需求會要求你按照當前幣別格式化小數,比如,人民幣應該顯示到小數點後兩位,即分,比如這個樣子的: 3.14 元
如果你照做,你就會掉坑裏了,因爲我們剛從這個坑爬出來,☺
事情是這個樣子的,我們的ERP允許爲幣別這個系統參數定義小數位數,而某個客戶在剛上線時,出於小心的目的吧,將人民幣設置到小數位數3位,我們在運算時也根據這個定義去四捨五入,比如:
0.31415 公斤(數量) * 10 元(單價) = 3.142 元
我們也將這個金額存入了數據庫,在上線一年之後,客戶覺得這個3位實在多餘,而且造成單據有這個0.002元,沒辦法付錢或收款啊,所以就重新將人民幣設置爲2位,新建的單據工作正常。但是月底時,埋好的坑被踩到了 :-(
因爲是中途修改的參數,所以可能上半個月的單據還存在 3.142 這樣的數據,但月底的各種報表顯示的結果可能就是3.14了,我們內部實際存儲的是3.142,所以如果用戶付款了3.14元的話,我們會說沒有結算完畢的,關鍵是如果很多單據合計起來可能就差幾元錢了。
所以說,這種小數點保留多少位,其實是兩種需求,
一種需求是顯示的格式化,我的觀點是,數據庫現在存放的是多少,就應該顯示多少,3.142 就應該顯示3.142。(當然,3.1420000 當然應該顯示爲3.142)
一種需求是錄入和運算的四捨五入,例如上面的數量,如果數量的位數是5,當乘法運算後,其結果是 3.1415,但由於人民幣的小數位數爲2,這個時候就需要四捨五入爲3.14。還有就是用戶在錄入數據時,如果用戶錄入3.1415時,就需要四捨五入或者提示用戶數據有問題(依據業務設計的愛好)。
小心字符串
很多大型的ERP,在處理大任務很緩慢的時候,90%的可能是糟糕的SQL操作,還剩下7%可能就是濫用字符串了,不斷的創建字符串、拼接、拼接再拼接,CPU說,我要抗議,GC說,我也要抗議,哪個龜兒子又在拼接字符串了。
如果你有段程序必須頻繁的處理字符串,我們都知道可以使用StringBuilder,但如果StringBuilder都已經不能滿足你了(怎麼感覺怪怪的),那麼你可以嘗試一下 線程變量緩存 這樣的寫法,比如參考:.net framework的內部實現。
數據庫的超時
我們都知道,你在執行某個sql時,如果消耗太長的時間(比如ERP中的月底的結算、MRP計算等),可能會報超時錯誤的。同理,如果你開啓一個事務,結果很長時間後你還沒有提交事務,一樣會報告超時的。
那麼你想過,這些超時錯誤對數據庫有什麼影響嗎?
//僞代碼 val tran = Tran() //開啓事務 tran.Begin() val cmd = SqlCommand() cmd.sql = "....;..." //很多條sql,使用分號隔開 cmd.Execute() //很長,很長時間 tean.commit()
當事務超時了,而操作的命令沒有超時時,SQL語句是繼續執行的,效果就是事務超時前的數據被回滾了,而後面繼續執行的sql是會被寫入數據庫的, 想想好恐怖吧。
所以,你的辦法可以是很粗魯的將超時時間設置很長的時間,討巧的辦法是讓事務的超時時間總是大於命令的超時時間。
最好的辦法是,優化你的sql吧,讓他短時間執行完,別老霸佔着數據庫妹妹,實在不行的話,看看能不能拆分成很多的小事務,好事大家輪流轉,你說是吧。
對異常的態度
有些新人,生怕自己的程序出現異常,或者從C、C++上帶來一些“壞習慣”,在程序不能完成任務時,使用false、0或者""表示沒有完成,然後你就發現調用他們寫的庫就是這個樣子的:
1 private fun DoSomething() : Int { 2 val data = GetData() 3 if (data !== null) { 4 var message = ChangeSomeData(data) 5 if (message != "") { 6 MessageBox(message) 7 return -1 8 } 9 10 val number = SaveData(data) 11 if (number == 0) { 12 MessageBox("Error") 13 return -2 14 } 15 16 return number 17 } 18 19 return -3 20 }
如果你把這個函數公開出去,那就更有意思了,文檔中需要說清楚返回的結果中有-1,-2,-3 三種情況。
囧,然後你就隔三差五打噴嚏,一定是新來的程序員調用你的代碼時再罵你了。
爲什麼不能是這樣用呢?
1 fun DoSomething() : Int { 2 val data = GetData() 3 ChangeSomeData(data) 4 return SaveData(data) 5 }
事實上,你調用那些 .net framework或者jdk之類的都是這個感覺,對吧,這裏的訣竅就是:你的函數沒有搞定事情,就應該拋出異常。
以上面的GetData方法爲例,如果你沒有獲取到數據,管他是數據錯了,還是數據庫連接不上了,還是其他任何錯誤,都應該以異常的方式拋出,只將你完成的結果作爲返回值。
當然,世事無絕對,比如你看見.net framework就設計了 Int32.TryParse 這樣的方法,因爲這種操作是很關心是否成功的。再比如,Java和C#的枚舉器中,hasNext()和MoveNext()都設計成bool返回值,表示是否成功移動到下一個位置。
小提示:Java沒有out方式的參數,所以設計TryXX這樣的方法就比較蹩腳,然後我看見一個帖子就貼心的設計了一個類解決這個問題。
1 class Out<T>{ 2 T s; 3 public void set(T value){ 4 s = value; 5 } 6 public T get(){ 7 return s; 8 } 9 public Out() { 10 } 11 } 12 13 public static Boolean TryParse(String str, Out<Int32> result){ 14 ...
好吧,我承認,最後一個不能叫坑,應該叫 不能給被人挖坑的坑。