其他
如何写出让人抓狂的代码?
The following article is from 苏三说技术 Author 苏三呀
前言
今天跟大家聊一个有趣的话题:如何写出让人抓狂的代码?大家看到这个标题,第一印象觉得这篇文章可能是一篇水文。但我很负责的告诉你,它是一篇有很多干货的技术文。曾几何时,你在阅读别人代码的时候,有没有抓狂,想生气,想发火的时候?今天就跟大家一起聊聊,这20种我看了会抓狂的代码,看看你中招了没?1.不注重代码格式
代码格式说起来很虚,下面我用几个案例演示一下,不注重代码格式的效果。作为这篇文章的开胃小菜吧。1.1 空格
有时候必要的空格没有加,比如:@Slf4j
public class TestService1{
public void test1(){
addLog("test1");
if (condition1){
if (condition2){
if (condition3){
log.info("info:{}",info);
}
}
}
}
}
@Slf4j
public class TestService1 {
public void test1() {
addLog("test1");
if (condition1) {
if (condition2) {
if (condition3) {
log.info("info:{}", info);
}
}
}
}
}
1.2 换行
写代码时,如果有些必要的换行没有加,可能会出现这样的代码: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);
}
}
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 b = 2;
String c = "abc";
boolean b = false;
int purchaserCount = 2;
String userName = "abc";
boolean hasSuccess = false;
2.2 见名知意
光起有意义的参数名还不够,我们不能就这点追求。我们起的参数名称最好能够见名知意
,不然就会出现这样的情况:String 用户Name = "苏三";
String su3 = "苏三";
String suThree = "苏三";
String susan = "苏三";
2.3 参数名风格一致
参数名其实有多种风格,列如:int suppliercount = 1;
//字母全大写
int SUPPLIERCOUNT = 1;
//小写字母 + 下划线
int supplier_count = 1;
//大写字母 + 下划线
int SUPPLIER_COUNT = 1;
//驼峰标识
int supplierCount = 1;
驼峰风格
,即:第一个字母小写,后面的每个单词首字母大写。例如:大写字母
+
下划线
分隔的参数名。例如:3.出现大量重复代码
ctrl + c
和 ctrl + v
可能是程序员使用最多的快捷键了。没错,我们是大自然的搬运工。哈哈哈。在项目初期,我们使用这种工作模式,确实可以提高一些工作效率,可以少写(实际上是少敲)很多代码。但它带来的问题是:会出现大量的代码重复。例如:@Slf4j
public class TestService1 {
public void test1() {
addLog("test1");
}
private void addLog(String info) {
if (log.isInfoEnabled()) {
log.info("info:{}", info);
}
}
}
@Slf4j
public class TestService2 {
public void test2() {
addLog("test2");
}
private void addLog(String info) {
if (log.isInfoEnabled()) {
log.info("info:{}", info);
}
}
}
@Slf4j
public class TestService3 {
public void test3() {
addLog("test3");
}
private void addLog(String info) {
if (log.isInfoEnabled()) {
log.info("info:{}", info);
}
}
}
if (log.isDebugEnabled()) {
log.debug("debug:{}", info);
}
}
public class LogUtil {
private LogUtil() {
throw new RuntimeException("初始化失败");
}
public static void addLog(String info) {
if (log.isDebugEnabled()) {
log.debug("debug:{}", info);
}
}
}
@Slf4j
public class TestService1 {
public void test1() {
LogUtil.addLog("test1");
}
}
4.从不写注释
有时候,在项目时间比较紧张时,很多人为了快速开发完功能,在写代码时,经常不喜欢写注释。此外,还有些技术书中说过:好的代码,不用写注释,因为代码即注释
。这也给那些不喜欢写代码注释的人,找了一个合理的理由。但我个人觉得,在国内每个程序员的英文水平都不一样,思维方式和编码习惯也有很大区别。你要把前人某些复杂的代码逻辑真正搞懂,可能需要花费大量的时间。我们看到spring
的核心方法refresh
,也是加了很多注释的: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.方法过长
我们平时在写代码时,有时候思路来了,一气呵成,很快就把功能开发完了。但也可能会带来一个小问题,就是方法过长。伪代码如下:List<User> userList = userMapper.getAll();
//经过一系列的数据过滤
//此处省略了50行代码
List<User> updateList = //最终获取到user集合
if(CollectionUtils.isEmpty(updateList)) {
return;
}
for(User user: updateList) {
//经过一些复杂的过期时间计算
//此处省略30行代码
}
//分页更新用户的过期时间
//此处省略20行代码
//发mq消息通知用户
//此处省略30行代码
}
方法拆分
,即把一个大方法拆分成多个小方法。例如: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行代码
}
顺便说一句,Hotspot对字节码超过8000字节的大方法有JIT编译限制,超过了限制不会被编译。
6.参数过多
我们平常在定义某个方法时,可能并没注意参数个数的问题(其实是我猜的)。我的建议是方法的参数不要超过5
个。先一起看看下面的例子:String b,
String c,
String d,
String e,
String f) {
...
}
public void client() {
fun("a","b","c","d",null,"f");
}
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");
}
String b,
String c) {
...
Result result = new Result();
result.setA(a);
result.setB(b);
result.setC(c);
return result;
}
lombok
的@Builder
注解,做成链式调用。例如:@AllArgsConstructor
@Builder
@Data
public class Result {
private String a;
private String b;
private String c;
}
.a("a").b("b").c("c")
.build();
ThreadPoolExecutor
不也提供了7个参数的方法?int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
...
}
构造方法
,我们这里主要讨论的是普通方法
。7.代码层级太深
不知道你有没有见过类似这样的代码:if(b == 2) {
if(c == 3) {
if(d == 4) {
if(e == 5) {
...
}
...
}
...
}
...
}
...
}
for(int j=0; j<50;j++) {
for(int m=0; m<200;m++) {
for(int n=0; n<100;n++) {
for(int k=0; k<50; k++) {
...
}
}
}
}
}
代码层级太深
。代码层级太深导致的问题是代码变得非常不好维护,不容易理清逻辑,有时候代码的性能也可能因此变差。那么关键问题来了,如何解决代码层级较深的问题呢?对于if判断层级比较多的情况:...
return;
}
doConditionB();
if(b!=2) {
...
return;
}
doConditionC();
}
面向防御式编程
的一种,即先把不满足条件的代码先执行,然后才执行满足条件的代码。此外别忘了,把满足条件的代码抽取到一个新的方法中喔。对于for循环层级太深的优化方案,一般推荐使用map
。例如:for(OrderDetail detail: detailList) {
if(order.getId().equals(detail.getOrderId())) {
doSamething();
}
}
}
for(Order order:orderList) {
List<OrderDetail> detailList = detailMap.get(order.getId());
if(CollectionUtils.isNotEmpty) {
doSamething();
}
}
8.判断条件太多
我们在写代码的时候,判断条件是必不可少的。不同的判断条件,走的代码逻辑通常会不一样。废话不多说,先看看下面的代码。void pay();
}
@Service
public class AliaPay implements IPay {
@Override
public void pay() {
System.out.println("===发起支付宝支付===");
}
}
@Service
public class WeixinPay implements IPay {
@Override
public void pay() {
System.out.println("===发起微信支付===");
}
}
@Service
public class JingDongPay implements IPay {
@Override
public void pay() {
System.out.println("===发起京东支付===");
}
}
@Service
public class PayService {
@Autowired
private AliaPay aliaPay;
@Autowired
private WeixinPay weixinPay;
@Autowired
private JingDongPay jingDongPay;
public void toPay(String code) {
if ("alia".equals(code)) {
aliaPay.pay();
} elseif ("weixin".equals(code)) {
weixinPay.pay();
} elseif ("jingdong".equals(code)) {
jingDongPay.pay();
} else {
System.out.println("找不到支付方式");
}
}
}
开闭原则:对扩展开放,对修改关闭。就是说增加新功能要尽量少改动已有代码。
那么,如何优化if...else判断呢?答:使用单一职责原则:顾名思义,要求逻辑尽量单一,不要太复杂,便于复用。
策略模式
+工厂模式
。策略模式定义了一组算法,把它们一个个封装起来, 并且使它们可相互替换。 工厂模式用于封装和管理对象的创建,是一种创建型模式。void pay();
}
@Service
public class AliaPay implements IPay {
@PostConstruct
public void init() {
PayStrategyFactory.register("aliaPay", this);
}
@Override
public void pay() {
System.out.println("===发起支付宝支付===");
}
}
@Service
public class WeixinPay implements IPay {
@PostConstruct
public void init() {
PayStrategyFactory.register("weixinPay", this);
}
@Override
public void pay() {
System.out.println("===发起微信支付===");
}
}
@Service
public class JingDongPay implements IPay {
@PostConstruct
public void init() {
PayStrategyFactory.register("jingDongPay", this);
}
@Override
public void pay() {
System.out.println("===发起京东支付===");
}
}
public class PayStrategyFactory {
private static Map<String, IPay> PAY_REGISTERS = new HashMap<>();
public static void register(String code, IPay iPay) {
if (null != code && !"".equals(code)) {
PAY_REGISTERS.put(code, iPay);
}
}
public static IPay get(String code) {
return PAY_REGISTERS.get(code);
}
}
@Service
public class PayService3 {
public void toPay(String code) {
PayStrategyFactory.get(code).pay();
}
}
9.硬编码
不知道你有没有遇到过这类需求:限制批量订单上传接口,一次性只能上传200条数据。 在job中分页查询用户,一页查询100个用户,然后计算用户的等级。
public void upload(List<Order> orderList) {
if(CollectionUtils.isEmpty(orderList)) {
throw new BusinessException("订单不能为空");
}
if(orderList.size() > MAX_LIMIT) {
throw new BusinessException("超过单次请求的数量限制");
}
}
静态常量
。上线之后,你发现上传历史数据时速度太慢了,需要把限制调大一点。我擦。。。这种小小的参数改动,还需要改源代码,重新编译,重新打包,重新部署。。。但如果你当初把这些公共参数,设置成可配置的,例如:private int maxLimit = 200;
public void upload(List<Order> orderList) {
if(CollectionUtils.isEmpty(orderList)) {
throw new BusinessException("订单不能为空");
}
if(orderList.size() > maxLimit) {
throw new BusinessException("超过单次请求的数量限制");
}
}
我们在前期开发的时候,宁可多花一分钟思考一下,这个参数后面是否会被修改,是否可以定义成可配置的参数。也比后期修改代码,重新编译,重新打包,重新上线花的时间少得多。
10.事务过大
我们平时在使用spring框架开发项目时,喜欢用@Transactional
注解声明事务。例如:public void updateUser(User user) {
System.out.println("update");
}
@Transactional
注解声明一下,该方法通过AOP就自动拥有了事务的功能。没错,这种做法给我们带来了极大的便利,开发效率更高了。但也给我们带来了很多隐患,比如大事务的问题。我们一起看看下面的这段代码:public void updateUser(User user) {
User oldUser = userMapper.getUserById(user.getId());
if(null != oldUser) {
userMapper.update(user);
} else {
userMapper.insert(user);
}
sendMq(user);
}
TransactionTemplate
的编程式事务优化代码。private TransactionTemplate transactionTemplate;
....
public void updateUser(User user) {
User oldUser = userMapper.getUserById(user.getId());
transactionTemplate.execute((status) => {
if(null != oldUser) {
userMapper.update(user);
} else {
userMapper.insert(user);
}
return Boolean.TRUE;
})
sendMq(user);
}
execute
方法中的代码块才真正需要事务,其余的方法,可以非事务执行,这样就能缩小事务的范围,避免大事务。当然使用TransactionTemplate
这种编程式事务,缩小事务范围,来解决大事务问题,只是其中一种手段。11.在循环中远程调用
有时候,我们需要在某个接口中,远程调用第三方的某个接口。比如:在注册企业时,需要调用天眼查接口,查一下该企业的名称和统一社会信用代码是否正确。这时候在企业注册接口中,不得不先调用天眼查接口校验数据。如果校验失败,则直接返回。如果校验成功,才允许注册。如果只是一个企业还好,但如果某个请求有10个企业需要注册,是不是要在企业注册接口中,循环调用10次天眼查接口才能判断所有企业是否正常呢?for(Corp corp: corpList) {
CorpInfo info = tianyanchaService.query(corp);
if(null == info) {
throw new RuntimeException("企业名称或统一社会信用代码不正确");
}
}
doRegister(corpList);
}
11.1 批量操作
远程接口支持批量操作,比如天眼查支持一次性查询多个企业的数据,这样就无需在循环中查询该接口了。但实际场景中,有些第三方不愿意提供第三方接口。11.2 并发操作
java8以后通过CompleteFuture
类,实现多个线程查天眼查接口,并且把查询结果统一汇总到一起。12.频繁捕获异常
通常情况下,为了在程序中抛出异常时,任然能够继续运行,不至于中断整个程序,我们可以选择手动捕获异常
。例如:try {
doSameThing();
} catch (Exception e) {
//ignore
}
doOtherThing();
}
12.1 滥用场景1
不知道你在打印异常日志时,有没有写过类似这样的代码:try {
doSameThing();
} catch (Exception e) {
log.error(e.getMessage(), e);
throw e;
}
doOtherThing();
}
12.2 滥用场景2
在写controller层接口方法时,为了保证接口有统一的返回值,你有没有写过类似这样的代码:public List<User> query(@RequestBody List<Long> ids) {
try {
List<User> userList = userService.query(ids);
return Result.ok(userList);
} catch (Exception e) {
log.error(e.getMessage(), e);
return Result.fature(500, "服务器内部错误");
}
}
13.不正确的日志打印
在我们写代码的时候,打印日志是必不可少的工作之一。因为日志可以帮我们快速定位问题,判断代码当时真正的执行逻辑。但打印日志的时候也需要注意,不是说任何时候都要打印日志,比如:public List<User> query(@RequestBody List<Long> ids) {
log.info("request params:{}", ids);
List<User> userList = userService.query(ids);
log.info("response:{}", userList);
return userList;
}
public List<User> query(@RequestBody List<Long> ids) {
if (log.isDebugEnabled()) {
log.debug("request params:{}", ids);
}
List<User> userList = userService.query(ids);
if (log.isDebugEnabled()) {
log.debug("response:{}", userList);
}
return userList;
}
isDebugEnabled
判断一下,如果当前的日志级别是debug才打印日志。生产环境默认日志级别是info,在有些紧急情况下,把某个接口或者方法的日志级别改成debug,打印完我们需要的日志后,又调整回去。方便我们定位问题,又不会产生大量的垃圾日志,一举两得。14.没校验入参
参数校验是接口必不可少的功能之一,一般情况下,提供给第三方调用的接口,需要做严格的参数校验。以前我们是这样校验参数的:public void add(@RequestBody User user) {
if(StringUtils.isEmpty(user.getName())) {
throw new RuntimeException("name不能为空");
}
if(null != user.getAge()) {
throw new RuntimeException("age不能为空");
}
if(StringUtils.isEmpty(user.getAddress())) {
throw new RuntimeException("address不能为空");
}
userService.add(user);
}
hibernate
的参数校验框架validate
之后,参数校验一下子变得简单多了。我们只需要校验的实体类User中使用validation框架的相关注解,比如:@NotEmpty、@NotNull等,定义需要校验的字段即可。@AllArgsConstructor
@Data
public class User {
private Long id;
@NotEmpty
private String name;
@NotNull
private Integer age;
@NotEmpty
private String address;
}
@Validated
注解,在接口方法上加上@Valid
注解。@Validated
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/add")
public void add(@RequestBody @Valid User user) {
userService.add(user);
}
}
@AllArgsConstructor
@Data
public class User {
private Long id;
@NotEmpty
private String name;
@NotNull
private Integer age;
@NotEmpty
private String address;
@NotNull
private Role role;
}
@NoArgsConstructor
@AllArgsConstructor
public class Role {
@NotEmpty
private String roleName;
@NotEmpty
private String tag;
}
"name": "tom",
"age":1,
"address":"123",
"role":{}
}
@AllArgsConstructor
@Data
public class User {
private Long id;
@NotEmpty
private String name;
@NotNull
private Integer age;
@NotEmpty
private String address;
@NotNull
@Valid
private Role role;
}
@Valid
注解。温馨的提醒一声,使用validate框架校验参数一定要自测,因为很容易踩坑。
15.返回值格式不统一
我之前对接某个第三方时,他们有部分接口的返回值结构是这样的:"ret":0,
"message":null,
"data":[]
}
"code":0,
"msg":null,
"success":true,
"result":[]
}
"code":0,
"message":null,
"data":[]
}
温馨的提醒一下,业务服务不要捕获异常,直接把异常抛给网关服务,由它来统一全局捕获异常,这样就能统一异常的返回值结构。
16.提交到git的代码不完整
我们写完代码之后,把代码提交到gitlab
上,也有一些讲究。最最忌讳的是代码还没有写完,因为赶时间(着急下班),就用git
把代码提交了。例如:String userName="苏三";
String password=
}
宁可不提交代码到远程仓库,切勿因为一时赶时间,提交了不完整的代码,导致团队的队友们项目都启动不了。好的习惯是:用git提交代码之前,一定要在本地运行一下,确保项目能正常启动才能提交。
17.不处理没用的代码
有些时候,我们为了偷懒,对有些没用的代码不做任何处理。比如:@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void add(User user) {
System.out.println("add");
}
public void update(User user) {
System.out.println("update");
}
public void query(User user) {
System.out.println("query");
}
}
@Deprecated
表示这个类或者方法没在使用了,例如:@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public void add(User user) {
System.out.println("add");
}
@Deprecated
public void update(User user) {
System.out.println("update");
}
@Deprecated
public void query(User user) {
System.out.println("query");
}
}
@Deprecated
注解的方法。这样一个看似简单的举手之劳,可以给自己,或者接手该代码的人,节省很多重复查代码的时间。建议我们把没用的代码优先删除掉,因为gitlab中是有历史记录的,可以找回。但如果有些为了兼容调用方老版本的代码,不能删除的情况,建议使用 @Deprecated
注解相关类或者接口。
18.随意修改接口名和参数名
不知道你有没有遇到过这种场景:你写了一个接口,本来以为没人使用,后来觉得接口名或参数名不对,偷偷把它们改了。比如:public List<User> query(@RequestBody List<Long> ids) {
return userService.query(ids);
}
public List<User> queryUser(@RequestBody List<Long> ids) {
return userService.query(ids);
}
对于已经在线上使用的接口,尽量不要修改接口名、参数名、修改参数类型、修改参数个数,还有请求方式,比如:get改成post等。宁可新加一个接口,也尽量不要影响线上功能。
19.使用map接收参数
我之前见过有些小伙伴,在代码中使用map接收参数的。例如:public void map(@RequestBody Map<String, Object> mapParam){
System.out.println(mapParam);
}
"id":123,
"name":"苏三",
"age":18,
"address":"成都"
}
"id":123,
"name":"苏三",
"age":18,
"address":"成都",
"role": {
"roleName":"角色",
"tag":"t1"
}
}
public void add(@RequestBody @Valid User user){
System.out.println(user);
}
20.从不写单元测试
因为项目时间实在太紧了,系统功能都开发不完,更何况是单元测试呢?大部分人不写单元测试的原因,可能也是这个吧。但我想告诉你的是,不写单元测试并不是个好习惯。我见过有些编程高手是测试驱动开发
,他们会先把单元测试写好,再写具体的业务逻辑。那么,我们为什么要写单元测试呢?我们写的代码大多数是可维护的代码,很有可能在未来的某一天需要被重构。试想一下,如果有些业务逻辑非常复杂,你敢轻易重构不?如果有单元测试就不一样了,每次重构完,跑一次单元测试,就知道新写的代码有没有问题。
我们新写的对外接口,测试同学不可能完全知道逻辑,只有开发自己最清楚。不像页面功能,可以在页面上操作。他们在测试接口时,很有可能覆盖不到位,很多bug测不出来。
本文结合自己的实际工作经验,用调侃的方式,介绍了在编写代码的过程中,不太好的地方和一些优化技巧,给用需要的朋友们一个参考。建议由于项目时间非常紧张,在开发时确实没有写单元测试,但在项目后期的空闲时间也建议补上。
程序员专属卫衣
推荐阅读:
每日打卡赢积分兑换书籍入口