本文作为初入ERP行业的新人的防坑指南,讲解了一些常见犯的错,这样也少走一些弯路,如果你是老鸟,请绕过 :-)

本文关联的代码使用kotlin编写,请自行转换为c#、java等你熟悉的语言,表述的坑在各个语言基本都是一样的。

不用使用单精度和双精度类型

     @Test
fun Test1(){
val a : Double = 0.3
var b : Double = 0.0
for (i in 0..9){
b += a
}
assert(b == 3.0)
}

你认为这个测试用例会通过吗?

是的,他的确不能通过,你仔细看看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软件会利用负数实现对旧账的冲正处理,对此,我保留我的意见。我的观点是,让用户永远输入正数,然后用明确的冲正、退回等指令,让用户知道他在干什么,也让你在设计很多流程时不必处处小心负数。

溢出检查

这可能不算一个大坑,但作为知识,你还是需要知道这一点,上代码:

     @Test
fun Test2() {
val a = Int.MAX_VALUE - 3
val b = 5
val c = a + b
assert(c == -2147483647)
}

正如你看到的,一个很大的值,在加法超过边界后是不会出错的,而是“循环”到负数了。

我知道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或者""表示没有完成,然后你就发现调用他们写的库就是这个样子的:

 private fun DoSomething() : Int {
val data = GetData()
if (data !== null) {
var message = ChangeSomeData(data)
if (message != "") {
MessageBox(message)
return -1
} val number = SaveData(data)
if (number == 0) {
MessageBox("Error")
return -2
} return number
} return -3
}

如果你把这个函数公开出去,那就更有意思了,文档中需要说清楚返回的结果中有-1,-2,-3 三种情况。

囧,然后你就隔三差五打喷嚏,一定是新来的程序员调用你的代码时再骂你了。

为什么不能是这样用呢?

 fun DoSomething() : Int {
val data = GetData()
ChangeSomeData(data)
return SaveData(data)
}

事实上,你调用那些 .net framework或者jdk之类的都是这个感觉,对吧,这里的诀窍就是:你的函数没有搞定事情,就应该抛出异常。

以上面的GetData方法为例,如果你没有获取到数据,管他是数据错了,还是数据库连接不上了,还是其他任何错误,都应该以异常的方式抛出,只将你完成的结果作为返回值。

当然,世事无绝对,比如你看见.net framework就设计了 Int32.TryParse 这样的方法,因为这种操作是很关心是否成功的。再比如,Java和C#的枚举器中,hasNext()和MoveNext()都设计成bool返回值,表示是否成功移动到下一个位置。

小提示:Java没有out方式的参数,所以设计TryXX这样的方法就比较蹩脚,然后我看见一个帖子就贴心的设计了一个类解决这个问题。

 class Out<T>{
T s;
public void set(T value){
s = value;
}
public T get(){
return s;
}
public Out() {
}
} public static Boolean TryParse(String str, Out<Int32> result){
...

好吧,我承认,最后一个不能叫坑,应该叫 不能给被人挖坑的坑。

04-23 21:18