聊聊写代码的20个反面教材
Posted 苏三说技术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊聊写代码的20个反面教材相关的知识,希望对你有一定的参考价值。
前言
今天跟大家聊一个有趣的话题:如何写出让人抓狂的代码?
大家看到这个标题,第一印象觉得这篇文章可能是一篇水文。但我很负责的告诉你,它是一篇有很多干货的技术文。
曾几何时,你在阅读别人代码的时候,有没有抓狂,想生气,想发火的时候?
今天就跟大家一起聊聊,这20种我看了会抓狂的代码,看看你中招了没?
1.不注重代码格式
代码格式说起来很虚,下面我用几个案例演示一下,不注重代码格式的效果。作为这篇文章的开胃小菜吧。
1.1 空格
有时候必要的空格没有加,比如:
@Service
@Slf4j
public class TestService1
public void test1()
addLog("test1");
if (condition1)
if (condition2)
if (condition3)
log.info("info:",info);
你看了这段代码有何感想,有没有血压飙升的感觉?
代码好像揉到一起去了。
那么,如何把血压降下来呢?
答:加上空格即可。
正解:
@Service
@Slf4j
public class TestService1
public void test1()
addLog("test1");
if (condition1)
if (condition2)
if (condition3)
log.info("info:", info);
只加了一些空格,稍微调整了一下,这段代码的层次结构一下子变得非常清晰了。
好吧,我又冷静下来了。
1.2 换行
写代码时,如果有些必要的换行没有加,可能会出现这样的代码:
public void update(User user)
if (null != user.getId())
User oldUser = userMapper.findUserById(user.getId());
if(null == oldUser)throw new RuntimeException("用户id不存在");
oldUser.setName(user.getName());oldUser.setAge(user.getAge());oldUser.setAddress(user.getAddress());
userMapper.updateUser(oldUser);
else userMapper.insertUser(user);
看了这段代码,是不是有点生无可恋的感觉?
简单的加点空格优化一下:
public void update(User user)
if (null != user.getId())
User oldUser = userMapper.findUserById(user.getId());
if(null == oldUser)
throw new RuntimeException("用户id不存在");
oldUser.setName(user.getName());
oldUser.setAge(user.getAge());
oldUser.setAddress(user.getAddress());
userMapper.updateUser(oldUser);
else
userMapper.insertUser(user);
代码逻辑一下子变得清晰了许多。
2.随意的命名
java中没有强制规定参数、方法、类或者包名该怎么起名。但如果我们没有养成良好的起名习惯,随意起名的话,可能会出现很多奇怪的代码。
2.1 有意义的参数名
有时候,我们写代码时为了省事(可以少敲几个字母),参数名起得越简单越好。假如同事A写的代码如下:
int a = 1;
int b = 2;
String c = "abc";
boolean b = false;
一段时间之后,同事A离职了,同事B接手了这段代码。
他此时一脸懵逼,a是什么意思,b又是什么意思,还有c…然后心里一万个草泥马。
给参数起一个有意义的名字,是非常重要的事情,避免给自己或者别人埋坑。
正解:
int supplierCount = 1;
int purchaserCount = 2;
String userName = "abc";
boolean hasSuccess = false;
2.2 见名知意
光起有意义的参数名还不够,我们不能就这点追求。我们起的参数名称最好能够见名知意
,不然就会出现这样的情况:
String yongHuMing = "苏三";
String 用户Name = "苏三";
String su3 = "苏三";
String suThree = "苏三";
这几种参数名看起来是不是有点怪怪的?
为啥不定义成国际上通用的(地球人都能看懂)英文单词呢?
String userName = "苏三";
String susan = "苏三";
上面的这两个参数名,基本上大家都能看懂,减少了好多沟通成本。
所以建议在定义不管是参数名、方法名、类名时,优先使用国际上通用的英文单词,更简单直观,减少沟通成本。少用汉子、拼音,或者数字定义名称。
2.3 参数名风格一致
参数名其实有多种风格,列如:
//字母全小写
int suppliercount = 1;
//字母全大写
int SUPPLIERCOUNT = 1;
//小写字母 + 下划线
int supplier_count = 1;
//大写字母 + 下划线
int SUPPLIER_COUNT = 1;
//驼峰标识
int supplierCount = 1;
如果某个类中定义了多种风格的参数名称,看起来是不是有点杂乱无章?
所以建议类的成员变量、局部变量和方法参数使用supplierCount,这种驼峰风格
,即:第一个字母小写,后面的每个单词首字母大写。例如:
int supplierCount = 1;
此外,为了好做区分,静态常量建议使用SUPPLIER_COUNT,即:大写字母
+
下划线
分隔的参数名。例如:
private static final int SUPPLIER_COUNT = 1;
3.出现大量重复代码
ctrl + c
和 ctrl + v
可能是程序员使用最多的快捷键了。
没错,我们是大自然的搬运工。哈哈哈。
在项目初期,我们使用这种工作模式,确实可以提高一些工作效率,可以少写(实际上是少敲)很多代码。
但它带来的问题是:会出现大量的代码重复。例如:
@Service
@Slf4j
public class TestService1
public void test1()
addLog("test1");
private void addLog(String info)
if (log.isInfoEnabled())
log.info("info:", info);
@Service
@Slf4j
public class TestService2
public void test2()
addLog("test2");
private void addLog(String info)
if (log.isInfoEnabled())
log.info("info:", info);
@Service
@Slf4j
public class TestService3
public void test3()
addLog("test3");
private void addLog(String info)
if (log.isInfoEnabled())
log.info("info:", info);
在TestService1、TestService2、TestService3类中,都有一个addLog方法用于添加日志。
本来该功能用得好好的,直到有一天,线上出现了一个事故:服务器磁盘满了。
原因是打印的日志太多,记了很多没必要的日志,比如:查询接口的所有返回值,大对象的具体打印等。
没办法,只能将addLog方法改成只记录debug日志。
于是乎,你需要全文搜索,addLog方法去修改,改成如下代码:
private void addLog(String info)
if (log.isDebugEnabled())
log.debug("debug:", info);
这里是有三个类中需要修改这段代码,但如果实际工作中有三十个、三百个类需要修改,会让你非常痛苦。改错了,或者改漏了,都会埋下隐患,把自己坑了。
为何不把这种功能的代码提取出来,放到某个工具类中呢?
@Slf4j
public class LogUtil
private LogUtil()
throw new RuntimeException("初始化失败");
public static void addLog(String info)
if (log.isDebugEnabled())
log.debug("debug:", info);
然后,在其他的地方,只需要调用。
@Service
@Slf4j
public class TestService1
public void test1()
LogUtil.addLog("test1");
如果哪天addLog的逻辑又要改了,只需要修改LogUtil类的addLog方法即可。你可以自信满满的修改,不需要再小心翼翼了。
我们写的代码,绝大多数是可维护性的代码,而非一次性的。所以,建议在写代码的过程中,如果出现重复的代码,尽量提取成公共方法。千万别因为项目初期一时的爽快,而给项目埋下隐患,后面的维护成本可能会非常高。
4.从不写注释
有时候,在项目时间比较紧张时,很多人为了快速开发完功能,在写代码时,经常不喜欢写注释。
此外,还有些技术书中说过:好的代码,不用写注释,因为代码即注释
。这也给那些不喜欢写代码注释的人,找了一个合理的理由。
但我个人觉得,在国内每个程序员的英文水平都不一样,思维方式和编码习惯也有很大区别。你要把前人某些复杂的代码逻辑真正搞懂,可能需要花费大量的时间。
我们看到spring
的核心方法refresh
,也是加了很多注释的:
public void refresh() throws BeansException, IllegalStateException
synchronized (this.startupShutdownMonitor)
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
catch (BeansException ex)
if (logger.isWarnEnabled())
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
finally
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
如果你写的代码完全不写注释,可能最近一个月、三个月、半年还记得其中的逻辑。但一年、两年,甚至更久的时间之后,你确定还能想起当初的逻辑,而不需要花费大量的时间去重新看自己的代码梳理逻辑?
说实话,不写注释,到了项目后期,不光是把自己坑了,还会坑队友。
为什么把这一条单独拿出来?
因为我遇到过,接过锅,被坑惨了。
5.方法过长
我们平时在写代码时,有时候思路来了,一气呵成,很快就把功能开发完了。但也可能会带来一个小问题,就是方法过长。
伪代码如下:
public void run()
List<User> userList = userMapper.getAll();
//经过一系列的数据过滤
//此处省略了50行代码
List<User> updateList = //最终获取到user集合
if(CollectionUtils.isEmpty(updateList))
return;
for(User user: updateList)
//经过一些复杂的过期时间计算
//此处省略30行代码
//分页更新用户的过期时间
//此处省略20行代码
//发mq消息通知用户
//此处省略30行代码
上面的run方法中包含了多种业务逻辑,虽说确实能够实现完整的业务功能,但却不能称之为好。
为什么呢?
答:该方法总长度超过150行,里面的代码逻辑很杂乱,包含了很多关联性不大的代码块。该方法的职责太不单一了,非常不利于代码复用和后期的维护。
那么,如何优化呢?
答:做方法拆分
,即把一个大方法拆分成多个小方法。
例如:
public void run()
List<User> userList = userMapper.getAll();
List<User> updateList = filterUser(userList);
if(CollectionUtils.isEmpty(updateList))
return;
for(User user: updateList)
clacExpireDay(user);
updateUser(updateList);
sendMq(updateList);
private List<User> filterUser(List<User> userList)
//经过一系列的数据过滤
//此处省略了50行代码
List<User> updateList = //最终获取到user集合
return updateList;
private void clacExpireDay(User user)
//经过一些复杂的过期时间计算
//此处省略30行代码
private void updateUser(List<User> updateList)
//分页更新用户的过期时间
//此处省略20行代码
private void sendMq(List<User> updateList)
//发mq消息通知用户
//此处省略30行代码
这样简单的优化之后,run方法的代码逻辑一下子变得清晰了许多,光看它调用的子方法的名字,都能猜到这些字方法是干什么的。
每个子方法只专注于自己的事情,别的事情交给其他方法处理,职责更单一了。
此外,如果此时业务上有一个新功能,也需要给用户发消息,那么上面定义的sendMq方法就能被直接调用了。岂不是爽歪歪?
换句话说,把大方法按功能模块拆分成N个小方法,更有利于代码的复用。
顺便说一句,Hotspot对字节码超过8000字节的大方法有JIT编译限制,超过了限制不会被编译。
6.参数过多
我们平常在定义某个方法时,可能并没注意参数个数的问题(其实是我猜的)。我的建议是方法的参数不要超过5
个。
先一起看看下面的例子:
public void fun(String a,
String b,
String c,
String d,
String e,
String f)
...
public void client()
fun("a","b","c","d",null,"f");
上面的fun方法中定义了6个参数,这样在调用该方面的所有地方都需要思考一下,这些参数该怎么传值,哪些参数可以为空,哪些参数不能为空。
方法的入参太多,也会导致该方法的职责不单一,方法存在风险的概率更大。
那么,如何优化参数过多问题呢?
答:可以将一部分参数迁移到新方法中。
这个例子中,可以把参数d,e,f迁移到otherFun方法。例如:
public Result fun(String a,
String b,
String c)
...
return result;
public void otherFun(Result result,
String d,
String e,
String f)
...
public void client()
Result result = fun("a","b","c");
otherFun(result, "d", null, "f");
这样优化之后,每个方法的逻辑更单一一些,更有利于方法的复用。
如果fun中还需要返回参数a、b、c,给下个方法继续使用,那么代码可以改为:
public Result fun(String a,
String b,
String c)
...
Result result = new Result();
result.setA(a);
result.setB(b);
result.setC(c);
return result;
在给Result对象赋值时,这里有个小技巧,可以使用lombok
的@Builder
注解,做成链式调用。例如:
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Data
public class Result
private String a;
private String b;
private String c;
这样在调用的地方,可以这样赋值:
Result result = Result.builder()
.a("a").b("b").c("c")
.build();
非常直观明了。
此时,有人可能会说,ThreadPoolExecutor
不也提供了7个参数的方法?
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
...
没错,不过它是构造方法
,我们这里主要讨论的是普通方法
。
7.代码层级太深
不知道你有没有见过类似这样的代码:
if (a == 1)
if(b == 2)
if(c == 3)
if(d == 4)
if(e == 5)
...
...
...
...
...
这段代码中有很多层if判断,是不是看得人有点眼花缭乱?
有同感的同学,请举个手。
如果你没啥感觉,那么接着往下看:
for(int i=0; i<100;i++)
for(int j=0; j<50;j++)
for(int m=0; m<200;m聊聊 sql 优化的 15 个小技巧