聊聊写代码的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 + cctrl + 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 个小技巧

计算1-1/3+1/5-1/7+···的前n项和

换个角度聊聊Netty

22.天眼查cookie模拟登陆采集数据

这可能是最全的总结了,详解 20 个 pandas 读与写函数

聊聊sql优化的15个小技巧