前言
一六年,我参与了一个高大尚的项目,我在项目中负责某功能计费模块的开发,尽管总个项目投入了很多人力,还有引进了很多先进的工具和管理方式,但那还是我有生以来见过最糟的项目之一
一七年,我去朋友的创业公司,帮他组建团队,一九年至今年,我于一家物流公司担任架构方面的工作,在开发中,我常见那种写业务能力很强,但代码细节惨不忍睹的开发者,于是我想整理一些我见过的代码片断,并提供一些改进意见
提示:以下是本篇文章正文内容,下面案例可供参考
一.error日志
对异常catch后的处理已经是老生常谈的话题了,不能什么都不做,也不能用e.printStackTrace()直接打印。下面的代码是用slf4j打印日志:
try{
.....
}catch(XXException e){
logger.error("send data to XX failed: {}",e);
}
上面面代码看起来没问题吧,但其实上面的日志在很多情况下并不能打印出正常的堆栈信息,在日志中用到占位符是受到了其它级别日志的影响。 我们可以看一下文档中error几个方法的定义:
/**
* Log a message at the ERROR level.
*
* @param msg the message string to be logged
*/
public void error(String msg);
/**
* Log a message at the ERROR level according to the specified format
* and argument.
* <p/>
* <p>This form avoids superfluous object creation when the logger
* is disabled for the ERROR level. </p>
*
* @param format the format string
* @param arg the argument
*/
public void error(String format, Object arg);
/**
* Log a message at the ERROR level according to the specified format
* and arguments.
* <p/>
* <p>This form avoids superfluous object creation when the logger
* is disabled for the ERROR level. </p>
*
* @param format the format string
* @param arg1 the first argument
* @param arg2 the second argument
*/
public void error(String format, Object arg1, Object arg2);
/**
* Log a message at the ERROR level according to the specified format
* and arguments.
* <p/>
* <p>This form avoids superfluous string concatenation when the logger
* is disabled for the ERROR level. However, this variant incurs the hidden
* (and relatively small) cost of creating an <code>Object[]</code> before invoking the method,
* even if this logger is disabled for ERROR. The variants taking
* {@link #error(String, Object) one} and {@link #error(String, Object, Object) two}
* arguments exist solely in order to avoid this hidden cost.</p>
*
* @param format the format string
* @param arguments a list of 3 or more arguments
*/
public void error(String format, Object... arguments); //(1)适用于占位符,
/**
* Log an exception (throwable) at the ERROR level with an
* accompanying message.
*
* @param msg the message accompanying the exception
* @param t the exception (throwable) to log
*/
public void error(String msg, Throwable t); //(2)天生就有打印异常信息的本事
可以看到error也有多参调用(1),所以用占位符也没问题,但在调用时,会调用参数的toString方法,我看到很多error日志后面出现了一串莫名奇妙的内存地址,有一个项目为了正确打印堆栈信息,竟然大动干戈,弄了一个工具类,返回error的堆栈信息。
大可不必如此,eror有一个重载方法天生就是用来打印Exception信息,见2。但调用时我们不应该在msg字段中出现占位符。
所以只需要:
logger.error("send data to XX failed:",e)
二 装箱与拆箱
void funa(Boolean flag){
if(flag){
....
}
}
上面的代码看上去非常ok。但当flag为null时,会报空指针异常。
某天夜里,我接到公司的紧急电话,公司里的pda扫码上传出现异常,而PDA(Android)的代码也从没有人动过。我追踪代码到错误处,以下是代码片断。
List<Scan> list = .... //这是调用服务得到的数据
for(Scan s : list){
SaveEndtity e = new SaveEntity();
e.setCout(s.getCount); //这里出现了异常
....
}
这是一个最简单的拷贝操作,如果不看两个类的代码,你怎么也想不到问题会出在哪里。
class SaveEndtity{
int count;
}
class Scan{
Integer count;
}
仅仅是两个类,一个用了原始类型,一个用了包装类型,就在特定条件下,造成了一场灾难。
提示:上面的两个示例是我在上家工作时发现很多代码中存在的bug.以下我所举的示例不能算是bug,但它确实让我不爽
三 没完没了的字符串
interface UserService{
.....
List<User> queryByAge(String age);
String getTotalCount(...)
List<> query(String pageSize,String pageNo)
}
我曾在一家名企见过很多类似的代码,我在上家公司见到更多的是这样
String userStr = restTemplate.get("http://XXX/getUser?id="+id,String.class)
User u = JsonObject.readValue(userString.User.class);
....
andoid端可能有如下东西
inteface UserService{
@Get("/getUser")
Observerable<String> find(String userName); //声明式远程接口调用
}
//使用时
userService.get("wzp").subscribe((userStr)->{
User u = Json.readObject(userStr,User.class);
...
})
很多高级语言的程序员都非常迷恋字符串,觉得字符串就是erverything。恨不得把类里的所有字段类型都设成字符串,或数据库里的所有字段都统一用varchar。或者有些程序员,必需通过字符串才能找到安全感。这真悲哀。
建议规范参数类型,至少Service层暴露出来的接口,应该明确其类型。像RestTemplate 或一些接口申明式的调用工具,往往遵循最少知识原则,序列化工作不需要我们额外去做。
四 过多的嵌套
if(a==b){
if(check()){
doThings();
}
}else{
if(c == d){
doThings();//与上面的doThings是同一个方法
}
}
以上代码包括括号总共8行,但它其实与下面三行代码等价:
if((a==b && check()) || c==d){
doThings();
}
离散数学中的什么条件推导我几乎忘了,但有时候可以不用高深的理论也可以写出比较优雅的代码。只要我们留心就行了。
以下代码片断我在code review时经常见到
Result login(String userName,String password){
if(StringUtils.isBlank(userName)){
throw new LongException();
}else if(StringUtils.isBlank(password)){
throw new LongException();;
}else{
User user= dao.finduser(username)
if(user == null){
return new result(false);
}else{
....
}
}
}
以上代码还是比较简单的,还有比这更复杂的。我曾见过有人在方法里面抛了个异常,然又在同一个方法里try这个异常,我看的是一头雾水,都不知道目的何在。其实写程序就像是写作文,要掌握主干,如果我们围绕主干去写,然后再添枝加叶或许会更好一些,比如一写代码主要功能是查找用户并检查用户名密码是正正确然后返回结果。
第一步:
Result login(String userName,String password){
Result r =new Result();
return r;
}
第二 步:
Result login(String userName,String password){
Result r =new Result();
User user = dao.queryUser(userName)
if(user== null){
r.setRsult(fale)
}else if(!checkpassword)){
r.setResult(false)
}else (r.setResult(true))
return r;
}
第三 步:
Result login(String userName,String password){
if(StringUtils.isBlank(userName)){
throw new LongException();
}
if(StringUtils.isBlank(password)){
throw new LongException();;
}
Result r =new Result();
User user = dao.queryUser(userName)
if(user== null){
r.setRsult(fale)
}else if(!checkpassword)){
r.setResult(false)
}else {
r.setResult(true)
}
return r;
}
我并没有用线性的方式编写代码,我更喜欢首先关注业务主体,然后再添加细节。而且我也不喜欢用多层嵌套。在if中已抛出了异常,else 就没有必要使用了。我们的主要业务逻辑没有嵌套在某个if中,这样我们可以把验证移到其它方法中了。
五 乱拷贝
我想大多程序员用的最多的快捷键就是ctrl+c /ctrl+v ,以至于看键盘的磨损程度就能推断出某个的职业是程序员,我曾见过,明明只要拍两行代码就中了事的,还要来个粘贴复制。
原始代码往往来自如下途径:
1.网上查找的资料,或第三方提供的示例
对自己不熟悉领域查找资料是最好的方式,如果网上有现成的代码是最好的,对接第三方接口时,第三方会提供调用api的demo示例代码。无论如何,我是不建议大家直接拷贝到项目中。很多人拷贝了代码都不知道代码中有些什么东西,这些代码往往存在如下问题:- 方法变量名混乱
- 过多static修饰
- 多余的代码,有些会把main方法拷进工程,至于System.out.priint比比皆是了。
- 拷贝者自己不去了解代码的逻辑,更别说去发现可能存在的漏洞
2. 本项目的代码或公司其它项目中的代码
这里我说的拷贝除了狭义上(行为上的)还有广意上的(参照其它的代码)。我参与某一大型项目,我从中总结因为考贝而带来的两点问题:
1.重复代码,我曾在其项目中的几个包中看到分布式锁的实现,有些连文件名都相同,有些仅细微处不同,
2.将不良的代码进行到底。例如以下的分页操作
String jql = "sele ....."
int recordCout = jpaTemplate.query(jql).fetch().length(); //获取总记录数
List data = jpaTemplate.query(jql).fetch().subList(pageSize*pageNu,(pageNu+1)*pageSzite); //获取数分页的数据
以上代码虽然是我根据记忆写成,但绝非杜撰,确确实实如此。关键是这样的代码不仅存在于一处,而是所有的分页操作都是用这种方式。
如果一段逻辑多处要用到(比如分布式锁),我们可抽象出一个公有类,尽量使其灵活,能满足多种场景。
在参考别人代码时,应该弄清楚这些代码是怎么实现的,或者将参考源扩大,比如在网上查找类似的方案。
六 引用不该引用的包
在对接某银行接口时,因为时间紧急且我当时还身兼pda的开发任务,所以我只是在原Demo上进行了一些调试,然后制定了一些开发的规则。团员成员在开发时,没有遇到任何问题,但jekenis构建时通不过。提示少包,我看了错误提示,发现代码中import 了org.sum.xml,那么问题就很明显了。在N年前我就明白,sum包最好不要引用,这个包是jre底层的实现包,如果我们换了个jdk就可能发现少了这个包。
我接手android 代码后,发现项目上中的json工具就已用了两三种多了,更别说其它的网络框架。
待续……