编写高质量代码:改善Java程序的151个建议(4)

  建议14:使用序列化类的私有方法巧妙解决部分属性持久化问题

  部分属性持久化问题看似很简单,只要把不需要持久化的属性加上瞬态关键字(transient关键字)即可。这是一种解决方案,但有时候行不通。例如一个计税系统和人力资源系统(HR系统)通过RMI(RemoteMethodInvocation,远程方法调用)对接,计税系统需要从HR系统获得人员的姓名和基本工资,以作为纳税的依据,而HR系统的工资分为两部分:基本工资和绩效工资,基本工资没什么秘密,根据工作岗位和年限自己都可以计算出来,但绩效工资却是保密的,不能泄露到外系统,很明显这是两个相互关联的类。先来看薪水类Salary类的代码:

  

    publicclassSalaryimplementsSerializable{ privatestaticfinallongserialVersionUID=44663L; //基本工资 privateintbasePay; //绩效工资 privateintbonus; publicSalary(int_basePay,int_bonus){ basePay=_basePay; bonus=_bonus; } /*getter/setter方法省略*/ }

  Peron类与Salary类是关联关系,代码如下:

  

    publicclassPersonimplementsSerializable{ privatestaticfinallongserialVersionUID=60407L; //姓名 privateStringname; //薪水 privateSalarysalary; publicPerson(String_name,Salary_salary){ name=_name; salary=_salary; } /*getter/setter方法省略*/ }

  这是两个简单的JavaBean,都实现了Serializable接口,都具备了持久化条件。首先计税系统请求HR系统对某一个Person对象进行序列化,把人员和工资信息传递到计税系统中,代码如下:

  

    publicclassSerialize{ publicstaticvoidmain(String[]args){ //基本工资1000元,绩效工资2500元 Salarysalary=newSalary(1000,2500); //记录人员信息 Personperson=newPerson(“张三”,salary); //HR系统持久化,并传递到计税系统 SerializationUtils.writeObject(person); } }

  在通过网络传送到计税系统后,进行反序列化,代码如下:

  

    publicclassDeserialize{ publicstaticvoidmain(String[]args){ //技术系统反序列化,并打印信息 Personp=(Person)SerializationUtils.readObject(); StringBuffersb=newStringBuffer(); sb.append(“姓名:”+p.getName()); sb.append(“\t基本工资:”+p.getSalary().getBasePay()); sb.append(“\t绩效工资:”+p.getSalary().getBonus()); System.out.println(sb); } }

  打印出的结果很简单:

  姓名:张三基本工资:1000绩效工资:2500。

  但是这不符合需求,因为计税系统只能从HR系统中获得人员姓名和基本工资,而绩效工资是不能获得的,这是个保密数据,不允许发生泄露。怎么解决这个问题呢?你可能马上会想到四种方案:

  (1)在bonus前加上transient关键字

  这是一个方法,但不是一个好方法,加上transient关键字就标志着Salary类失去了分布式部署的功能,它可是HR系统最核心的类了,一旦遭遇性能瓶颈,想再实现分布式部署就不可能了,此方案否定。

  (2)新增业务对象

  增加一个Person4Tax类,完全为计税系统服务,就是说它只有两个属性:姓名和基本工资。符合开闭原则,而且对原系统也没有侵入性,只是增加了工作量而已。这是个方法,但不是最优方法。

  (3)请求端过滤

  在计税系统获得Person对象后,过滤掉Salary的bonus属性,方案可行但不合规矩,因为HR系统中的Salary类安全性竟然让外系统(计税系统)来承担,设计严重失职。

  (4)变更传输契约

  例如改用XML传输,或者重建一个 Web Service服务。可以做,但成本太高。

  可能有读者会说了,你都在说别人的方案不好,你提供个优秀的方案看看!好的,这就展示一个优秀的方案。其中,实现了Serializable接口的类可以实现两个私有方法:writeObject和readObject,以影响和控制序列化和反序列化的过程。我们把Person类稍做修改,看看如何控制序列化和反序列化,代码如下:

  

    publicclassPersonimplementsSerializable{ privatestaticfinallongserialVersionUID=60407L; //姓名 privateStringname; //薪水 privatetransientSalarysalary; publicPerson(String_name,Salary_salary){ name=_name; salary=_salary; } //序列化委托方法 privatevoidwriteObject(java.io.ObjectOutputStreamout)throwsIOException{ out.defaultWriteObject(); out.writeInt(salary.getBasePay()); } //反序列化时委托方法 privatevoidreadObject(java.io.ObjectInputStreamin)throwsIOException,Class-NotFoundException{ in.defaultReadObject(); salary=newSalary(in.readInt(),0); } }

  其他代码不做任何改动,我们先运行看看,结果为:

  姓名:张三基本工资:1000绩效工资:0。

  我们在Person类中增加了writeObject和readObject两个方法,并且访问权限都是私有级别,为什么这会改变程序的运行结果呢?其实这里使用了序列化独有的机制:序列化回调。Java调用ObjectOutputStream类把一个对象转换成流数据时,会通过反射(Reflection)检查被序列化的类是否有writeObject方法,并且检查其是否符合私有、无返回值的特性。若有,则会委托该方法进行对象序列化,若没有,则由ObjectOutputStream按照默认规则继续序列化。同样,在从流数据恢复成实例对象时,也会检查是否有一个私有的readObject方法,如果有,则会通过该方法读取属性值。此处有几个关键点要说明:

  (1)out.defaultWriteObject()

  告知JVM按照默认的规则写入对象,惯例的写法是写在第一句话里。

  (2)in.defaultReadObject()

  告知JVM按照默认规则读入对象,惯例的写法也是写在第一句话里。

  (3)out.writeXX和in.readXX

  分别是写入和读出相应的值,类似一个队列,先进先出,如果此处有复杂的数据逻辑,建议按封装Collection对象处理。

  可能有读者会提出,这似乎不是一种优雅的处理方案呀,为什么JDK没有对此提供一个更好的解决办法呢?比如访问者模式,或者设置钩子函数(Hook),完全可以更优雅地解决此类问题。我查阅了大量的文档,得出的结论是:无解,只能说这是一个可行的解决方案而已。

  再回到我们的业务领域,通过上述方法重构后,其代码的修改量减少了许多,也优雅了许多。可能你又要反问了:如此一来,Person类也失去了分布式部署的能力啊。确实是,但是HR系统的难点和重点是薪水计算,特别是绩效工资,它所依赖的参数很复杂(仅从数量上说就有上百甚至上千种),计算公式也不简单(一般是引入脚本语言,个性化公式定制),而相对来说Person类基本上都是“静态”属性,计算的可能性不大,所以即使为性能考虑,Person类为分布式部署的意义也不大。

  建议15:break万万不可忘

  我们经常会写一些转换类,比如货币转换、日期转换、编码转换等,在金融领域里用到最多的要数中文数字转换了,比如把“1”转换为“壹”,不过,开源世界是不会提供此工具类的,因为它太贴合中国文化了,要转换还是得自己动手写,代码片段如下:

  

    publicclassClient{ publicstaticvoidmain(String[]args){ System.out.println(“2=”+toChineseNumberCase(2)); } //把阿拉伯数字翻译成中文大写数字 publicstaticStringtoChineseNumberCase(intn){ StringchineseNumber=””; switch(n){ case0:chineseNumber=”零”; case1:chineseNumber=”壹”; case2:chineseNumber=”贰”; case3:chineseNumber=”叁”; case4:chineseNumber=”肆”; case5:chineseNumber=”伍”; case6:chineseNumber=”陆”; case7:chineseNumber=”柒”; case8:chineseNumber=”捌”; case9:chineseNumber=”玖”; } returnchineseNumber; } }

  这是一个简单的转换类,并没有完整实现,只是一个金融项目片段。如此简单的代码应该不会有错吧,我们运行看看,结果是:2 = 玖 。

  恩?错了?回头再来看程序,马上醒悟了:每个case语句后面少加了break关键字。程序从“case 2”后面的语句开始执行,直到找到最近的break语句结束,但可惜的是我们的程序中没有break语句,于是在程序执行的过程中,chineseNumber的赋值语句会多次执行,会从等于“贰”、等于“叁”、等于“肆”,一直变换到等于“玖”,switch语句执行结束了,于是结果也就如此了。

  此类问题发生得非常频繁,但也很容易发现,只要做一下单元测试(Unit Test),问题立刻就会被发现并解决掉,但如果是在一堆的case语句中,其中某一条漏掉了break关键字,特别是在单元测试覆盖率不够高的时候(为什么不够高?在大点的项目中蹲过坑、打过仗的兄弟们可能都知道,项目质量是与项目工期息息相关的,而项目工期往往不是由项目人员决定的,所以如果一个项目的单元测试覆盖率能够达到60%,你就可以笑了),也就是说分支条件可能覆盖不到的时候,那就会在生产中出现大事故了。

  我曾遇到过一个类似的事故,那是开发一个通过会员等级决定相关费率的系统,由于会员等级有100多个,所以测试时就采用了抽样测试的方法,测试时一切顺利,直到系统上线后,财务报表系统发现一个小概率的会员费率竟然出奇的低,于是就跟踪分析,发现是少了一个break,此事不仅造成甲方经济上的损失,而且在外部也产生了不良的影响,最后该代码的作者被辞退了,测试人员、质量负责人、项目经理都做了相应的处罚。希望读者能引以为戒,记住在case语句后面随手写上break,养成良好的习惯。

  对于此类问题,还有一个最简单的解决办法:修改IDE的警告级别,例如在Eclipse中,可以依次点击Performaces→Java→Compiler→Errors/Warnings→Potential Programming problems,然后修改‘switch’case fall-through为Errors级别,如果你胆敢不在case语句中加入break,那Eclipse直接就报个红叉给你看,这样就可以完全避免该问题的发生了。

  相关链接:

  编写高质量代码:改善Java程序的151个建议(1)

  编写高质量代码:改善Java程序的151个建议(2)

  编写高质量代码:改善Java程序的151个建议(3)

你曾经说,等我们老的时候,

编写高质量代码:改善Java程序的151个建议(4)

相关文章:

你感兴趣的文章:

标签云: