洞察探索如何利用兼容微信生态的小程序容器,实现跨平台开发,助力金融和车联网行业的数字化转型。
711
2022-11-23
代码重构,高效编码,写出好代码是有套路的!
重构的方法往往是零散的,大家在记忆时也是零散的,不系统。代码怎么写更好,你肯定能说出几点来,但还不够系统。
这里梳理记录一些重构方法与案例,意义在于系统的梳理后可以在平时写代码时做参照,写出更好的代码。
你想想每次重构的时候都能在这篇文章里找到对应的套路那该多爽。知道的套路多了,自然能写出更好的代码,毕竟“套路得人心”嘛。
所以本文刻意追求广而全,也将不断补充更新相关案例,可以关注或收藏,不时翻出来温故下。
一、开发界的墨菲定律
下面的或许我们都经历过:
你写的bug,迟早会被后面接收的程序员发现如果你对你的代码没信心觉得有可能出错,往往就真的出错了实际开发周期总是比你预计的长用户需求永远没有表面看起来那么简单
任何事情虽然发生大的概率很低,但哪怕只有0.0000000000000000000001%的概率,它也终有一天会发生。
出来混迟早是要还的,技术上的债尽量少欠一点。要做到就需要平时写代码时知道一些通用且科学的方法套路,积累到一定程度,代码质量也将越来越高且优雅。
二、坏代码有哪些?对应的Refactoring?
有哪些常见的坏代码?
神秘命名代码重复过长方法过长参数列表全局数据可变数据过大的类注释冗余
.....
三、可读性
实际上开发过程中80%的时间是在读代码,可能是读自己的也可能是读别人的;只有20%是在写代码, 代码的可读性直接影响效率和准确性。
可读性差:
优化:
四、关于命名
4.1、选择专业的词
这里的size()若用hight()或者numNodes()替换会更好。
4.2、用具体的名字代替抽象的名字
若有一个方法用于检测服务示范可以监听某个给定的TCP/IP端口。
不好的:
优化:
4.3、见名知意
给名字附加更多的信息,就可以做到见名知意。
一个名字就是一个小小的注释,可以给名字附加上有意义的前后缀,例如:
4.4、名字尽量不缩写
关于缩写,遵循团队新成员是否能理解这个名字的含义。
例如把类命名为BEManager而不是BackEndManager,新来的团队成员就可能不知道含义了,这种需要避免。
但是那种约定俗成的缩写,大部分人都一致知道的除外,比如:
专业术语的缩写addr、msg、btn、str、pm、am等
4.5、范围类命名
用first和last表示包含的范围begin和end表示包含、排除的范围
4.6、布尔值命名
1、确保返回true和false的意义明确
2、可以加上is、has、can、should
五、关于注释
实际上开发过程中80%的时间是在读代码,可能是自己的也可能是别人的;只有20%是在写代码,
5.1、不该注释的不注释
(1)不要为了注释而注释
下面的注释完全没必要:
(2)不给不好的名字加注释
为啥?因为名字不好那就要先取个好的名字,好的名字本身就是注释,不需要再注释。这又回到了前面命名章节里的提到的见名知意。
5.2、注释记录的是你的思想
加入“导演评论”为代码中存在的缺陷说明清楚给常量加注释
5.3、 站在读者角度写注释
(1)为什么这样做
(2)公布代码陷阱
5.4、言简意赅,注意排版
(1)言简意赅、保持紧凑
(2)语义清晰具体
好的正面例子:
六、条件判断优化
6.1、变化值在左更易读
不好:
好:
6.2、if/else 语句的顺序
尽量改写成卫语句先处理正逻辑的情况先处理简单的情况先处理有趣或简单的情况简单的优先使用三目运算
可以调整顺序,让程序更高效。例如:如果用户是会员,并且第一次登陆时,需要发一条通知的短信。代码很可能直接这样写:
if(isUserVip && isFirstLogin){ sendMsg();}
假设总共有5个请求进来,isUserVip通过的有3个请求,isFirstLogin通过的有1个请求。
那么以上代码,isUserVip执行的次数为5次,isFirstLogin执行的次数也是3次。
如果调整一下isUserVip和isFirstLogin的顺序呢?
if(isFirstLogin && isUserVip ){ sendMsg();}
那么isFirstLogin执行的次数是5次,isUserVip执行的次数是1次。
假如isUserVip和isFirstLogin的复杂度或者耗时一样,那么调换一下顺序,已经节省了2次运算。
七、循环优化
7.1、提前返回减少嵌套
实际就是优先使用卫语句。
八、删除不必要的变量
8.1、移除低价值变量
上面第二句的now这个变量价值较低,可以直接移除。
8.2、减少中间结果变量
这里indexToRemove是不需要的。
不好的:
好的:
8.3、减少控制变量
不好的:
好的:
九、重复代码——提炼函数
提炼函数:将重复的代码片段提取出来,IDEA内置了该重构功能。
十、方法太简单——内联函数
内联函数:若一个方法逻辑太简单,则直接把其中的代码移到调用处。
十一、复杂表达式——提炼变量
提炼变量:如果表达式复杂,难以阅读,可以通过合理拆分, 引入局部变量,使代码易读。
十二、简单表达式——内联变量
内联变量:和提炼变量正好相反,若表达式很简单,就没必要再提取成局部变量。
修改参数:尽量是传参变成适用性更好的形式。
这里若只是需要电话号码,但却传了个person对象,违背了迪米特原则,也就是说每次调用这个方法还要引入一个Person类,显然不合适,修改下:
这样适用性更好。
十四、参数列表过长——引入参数对象
封装后:
十五、基本类型偏执——以对象取代基本类型
十六、以查询替代临时变量(replace temp with query)
临时变量提到单独的查询方法中,提升可读性和复用性。
refactoring:
十七、过大的类 —— 提炼类
类要注意单一原则,一个类责任太多就要提炼出去。
refactoring:
十八、重复造轮子——api调用取代内联代码
熟悉常用的api,避免重复造轮子。
refactoring:
十九、创建不必要的对象
下面两种情况下,不需要创建新的对象,即没必要new:
如果一个变量,后面的逻辑判断,一定会被赋值;只是一个字符串变量(原因参考
反例:
String s = new String ("你大爷"); // 这是创建了两个对象
正例:
String s= "你大爷”;
二十、初始化集合时,不指定容量
假设你的map要存储的元素个数是15个左右,最优写法如下:
//initialCapacity = 15/0.75+1=21 Map map = new HashMap(21); 又因为hashMap的容量跟2的幂有关,所以可以取32的容量 Map map = new HashMap(32);
二十一、catch后没打印出具体的exception
反例:
try{ // do something}catch(Exception e){ log.info("有异常!");}
正例:
try{ // do something}catch(Exception e){ log.info("有异常了:",e); //把exception打印出来}
反例中,并没有把exception出来,到时候排查问题就不好查了啦,到底是SQl写错的异常还是IO异常,还是其他呢?打印出异常好排查问题。
并且, catch住异常后,尽量不要使用e.printStackTrace(),而是使用log打印。
二十二、打印日志的时候,对象没有覆盖Object的toString的方法
publick Response dealWithRequest(Request request){ log.info("请求参数是:".request.toString)}
打印日志的时候,若对象没有覆盖Object的toString的方法,那只会打印出类名:
请求参数是:local.Request@49476842
因此在打印对象前先确认这个对象是否重写了toString()方法。
二十三、重复查询
前面已经查到的数据,在后面的方法也用到的话,可以透传,减少方法调用/查表。
反例:
public Response dealRequest(Request request){ UserInfo userInfo = userInfoDao.selectUserByUserId(request.getUserId); if(Objects.isNull(request)){ return ; } insertUserVip(request.getUserId);}private int insertUserVip(String userId){ //又查了一次 UserInfo userInfo = userInfoDao.selectUserByUserId(request.getUserId); //插入用户vip流水 insertUserVipFlow(userInfo); ....}
正例:
public Response dealRequest(Request request){ UserInfo userInfo = userInfoDao.selectUserByUserId(request.getUserId); if(Objects.isNull(request)){ return ; } insertUserVip(userInfo);}private int insertUserVip(UserInfo userInfo){ //插入用户vip流水 insertUserVipFlow(userInfo); ....}
二十四、使用魔法值
魔法值,应该要用enum枚举或常量代替。
反例:
if("0".equals(userInfo.getVipFlag)){ //非会员,提示去开通会员 tipOpenVip(userInfo);}else if("1".equals(userInfo.getVipFlag)){ //会员,加勋章返回 addMedal(userInfo);}
正例:
if(UserVipEnum.NOT_VIP.getCode.equals(userInfo.getVipFlag)){ //非会员,提示去开通会员 tipOpenVip(userInfo);}else if(UserVipEnum.VIP.getCode.equals(userInfo.getVipFlag)){ //会员,加勋章返回 addMedal(userInfo);}public enum UserVipEnum { NOT_VIP("0","非会员"), VIP("1","会员"), ; private String code; private String desc; UserVipEnum(String code, String desc) { this.code = code; this.desc = desc; }}
二十五、值不变的变量不定义成静态变量
当成员变量值不会改变时,优先定义为静态常量。
因为如果定义为static,即类静态常量,在每个实例对象中,它只有一份副本。如果是成员变量,每个实例对象中,都各有一份副本。
反例:
public class Task { private final long timeout = 10L; ...}
正例:
public class Task { private static final long TIMEOUT = 10L; ...}
二十六、不考虑异步处理
通知类(如发邮件,有短信)的代码,建议异步处理。
添加通知类等不是非主要,可降级的接口时,应该静下心来考虑是否会影响主要流程,思考怎么处理最好。
反例:
public class BigDecimalUtils { public BigDecimal ifNullSetZERO(BigDecimal in) { return in != null ? in : BigDecimal.ZERO; } public BigDecimal sum(BigDecimal ...in){ BigDecimal result = BigDecimal.ZERO; for (int i = 0; i < in.length; i++){ result = result.add(ifNullSetZERO(in[i])); } return result; }}
正例:
public class BigDecimalUtils { public static BigDecimal ifNullSetZERO(BigDecimal in) { return in != null ? in : BigDecimal.ZERO; } public static BigDecimal sum(BigDecimal ...in){ BigDecimal result = BigDecimal.ZERO; for (int i = 0; i < in.length; i++){ result = result.add(ifNullSetZERO(in[i])); } return result; }}
工具类的方法使用static修饰,每次使用就无须创建对象。
二十八、用一个Exception捕捉所有可能的异常
反例:
public void test(){ try{ //…抛出 IOException 的代码调用 //…抛出 SQLException 的代码调用 }catch(Exception e){ //用基类 Exception 捕捉的所有可能的异常,如果多个层次都这样捕捉,会丢失原始异常的有效信息哦 log.info(“Exception in test,exception:{}”, e); }}
正例:
public void test(){ try{ //…抛出 IOException 的代码调用 //…抛出 SQLException 的代码调用 }catch(IOException e){ //仅仅捕捉 IOException log.info(“IOException in test,exception:{}”, e); }catch(SQLException e){ //仅仅捕捉 SQLException log.info(“SQLException in test,exception:{}”, e); }}
二十九、随意创建对象占用堆内存
如果变量的初值一定会被覆盖,就没有必要给变量赋初值。
反例:
List
正例:
List
三十、乱用Arrays.asList
30.1、基本类型不能作为 Arrays.asList方法的参数,否则会被当做一个参数。
public class ArrayAsListTest { public static void main(String[] args) { int[] array = {1, 2, 3}; List list = Arrays.asList(array); System.out.println(list.size()); }}//运行结果1
30.2、Arrays.asList 返回的 List 不支持增删操作。
public class ArrayAsListTest { public static void main(String[] args) { String[] array = {"1", "2", "3"}; List list = Arrays.asList(array); list.add("5"); System.out.println(list.size()); }}// 运行结果Exception in thread "main" java.lang.UnsupportedOperationException at java.util.AbstractList.add(AbstractList.java:148) at java.util.AbstractList.add(AbstractList.java:108) at object.ArrayAsListTest.main(ArrayAsListTest.java:11)
Arrays.asList 返回的 List 并不是我们期望的 java.util.ArrayList,而是 Arrays 的内部类ArrayList。内部类的ArrayList没有实现add方法。
30.3、对原始数组的修改会影响到Arrays.asLis的结果
public class ArrayAsListTest { public static void main(String[] args) { String[] arr = {"1", "2", "3"}; List list = Arrays.asList(arr); arr[1] = "4"; System.out.println("原始数组"+Arrays.toString(arr)); System.out.println("list数组" + list); }}//运行结果原始数组[1, 4, 3]list数组[1, 4, 3]
三十一、调用第三方接口,不考虑异常处理,安全性,超时重试
调用第三方服务,或者分布式远程服务的的话,需要考虑:
异常处理(比如调别人的接口,如果异常了,怎么处理,是重试还是当做失败)超时(没法预估对方接口一般多久返回,一般设置个超时断开时间,以保护你的接口)重试次数(接口调失败,需不需要重试,需要站在业务上角度思考这个问题)
三十二、没考虑接口幂等性
接口是需要考虑幂等性的,尤其抢红包、转账这些重要接口。最直观的业务场景,就是用户连着点两次,只能有点一次的效果。
一般幂等技术方案有这几种:
查询操作唯一索引token机制,防止重复提交数据库的delete/update操作乐观锁悲观锁Redis、zookeeper 分布式锁(以前抢红包需求,用了Redis分布式锁)状态机幂等
三十三、循环体内 慎用异常
在Java开发中,经常使用try-catch进行错误捕获,但是try-catch语句对系统性能而言是非常糟糕的。虽然一次try-catch中,无法察觉到它对性能带来的损失,但是一旦try-catch语句被应用于循环或是遍历体内,就会给系统性能带来极大的伤害。
以下是一段将try-catch应用于循环体内的示例代码:
@Test public void test11() { long start = System.currentTimeMillis(); int a = 0; for(int i=0;i<1000000000;i++){ try { a++; }catch (Exception e){ e.printStackTrace(); } } long useTime = System.currentTimeMillis()-start; System.out.println("useTime:"+useTime); }
useTime:10
下面是一段将try-catch移到循环体外的代码,那么性能就提升了将近一半。如下:
@Test public void test(){ long start = System.currentTimeMillis(); int a = 0; try { for (int i=0;i<1000000000;i++){ a++; } }catch (Exception e){ e.printStackTrace(); } long useTime = System.currentTimeMillis()-start; System.out.println(useTime); }
useTime:6
三十四、不使用局部变量
调用方法时传递的参数以及在调用中创建的临时变量都保存在栈(Stack)中,速度快。
其他变量,如静态变量、实例变量等,都在堆(Heap)中创建,速度较慢。
下面是一段使用局部变量进行计算的代码:
@Test public void test11() { long start = System.currentTimeMillis(); int a = 0; for(int i=0;i<1000000000;i++){ a++; } long useTime = System.currentTimeMillis()-start; System.out.println("useTime:"+useTime); }
useTime:5
将局部变量替换为类的静态变量:
static int aa = 0; @Test public void test(){ long start = System.currentTimeMillis(); for (int i=0;i<1000000000;i++){ aa++; } long useTime = System.currentTimeMillis()-start; System.out.println("useTime:"+useTime); }
useTime:94
通过上面两次的运行结果,可以看出来局部变量的访问速度远远高于类成员变量。
三十五、乘除法没有考虑使用 位运算 代替
在所有的运算中,位运算是最为高效的。因此,可以尝试使用位运算代替部分算术运算,来提高系统的运行速度。最典型的就是对于整数的乘除运算优化。
下面是一段使用算术运算的代码:
@Test public void test11() { long start = System.currentTimeMillis(); int a = 0; for(int i=0;i<1000000000;i++){ a*=2; a/=2; } long useTime = System.currentTimeMillis()-start; System.out.println("useTime:"+useTime); }
useTime:1451
将循环体中的乘除运算改为等价的位运算,代码如下:
@Test public void test(){ long start = System.currentTimeMillis(); int aa = 0; for (int i=0;i<1000000000;i++){ aa<<=1; aa>>=1; } long useTime = System.currentTimeMillis()-start; System.out.println("useTime:"+useTime); }
useTime:10
上两段代码执行了完全相同的功能,在每次循环中,都将整数乘以2,并除以2。但是运行结果耗时相差非常大,所以位运算的效率还是显而易见的。
三十六、复制数组未使用arrayCopy()
如果在应用程序中需要进行数组复制,应该使用这个JDK中提供arrayCopy(),而不是自己实现。
因为System.arraycopy()函数是native函数,通常native函数的性能要优于普通函数。仅出于性能考虑,在程序开发时,应尽可能调用native函数。
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~