写高质量的代码

  有人曾经说过我们正在临近代码的终点。很快代码就会自动生产出来,不需要再人工编写。程序员完全没用了,因为商务人士 可以从规约直接生成代码。除非我们的人工智能达到真的可以像人一样思考,否则说这些都是扯淡!就目前来看我们在未来好多好多年还是无法抛弃掉代码, 因为代码呈现了需求的细节。在某些层面上,这些细节无法被忽略或者抽象,必须明确之。将需求明确到机器可以执行的细节程度,就是编程所要做的事。而这种规约正是代码。
  我相信我们每个人都见过糟糕的代码, 看这些代码一般都令人痛苦不堪,到处都暗藏的沼泽地,一不留神就会陷入深渊。那么为什么要写这样糟糕的代码呢?是想快点完成?还是赶时间?都有可能。或许你觉得自己要干好所需的时间不够;假使花时间清理代码,老板就会大发雷霆。或许你只是不耐烦再搞这套程序,期望早点结束。或许你看了看自己承诺要做的其他事,意识到需要尽快完成手头上的工作,好接着完成下一件工作。虽然每个人的理由大概不会相同,但我相信这种事我们都干过。我们都曾经瞟过一眼自己亲手造成的混乱,决定弃之不顾,走向新的一天。我们都曾经看到自己的烂程序居然能运行,然后断言能运行的烂程序总比什么都没有强。但当我们自己看到别人写的烂程序时,肯定都说过这么烂的代码还不如干掉自己重新写。我们都曾经说过有朝一日再回头清理。但是对于终日忙碌的程序员来说:稍后等于永远不。因此我们必须时刻对自己严格要求,不仅仅只是按照需求完成开发任务,时刻保持代码的整洁,否则都最后肯定是自作自受。现在踩得坑,很多都是以前蹦跶留下的。

1. 有意义的命名

  我们的代码其实就是无数的类名,方法名和变量名的一个大集合。如果随心所欲的乱起名字,那么看你代码的人将相当痛苦,想必就是自己过一段时间来看自己的代码, 也得不会那么容易吧。好的命名使人读你的代码像是读文章一样简单明了,通过名字其他人就可以知道你的代码的业务逻辑是什么,不必再花费时间在看代码的具体实现细节。
  • 类名使用UpperCamelCase风格,必须遵从驼峰形式。但是一常见的英文缩写除外。例如: TCP/ DTO / AO /  UID等
  • 方法名、参数名、成员变量、局部变量都统一使用lowerCamelCase风格,必须遵从驼峰形式。
  • 常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
  • 杜绝完全不规范的缩写,避免望文不知义。例如: AbstractClass“缩写”命名成AbsClass;condition“缩写”命名成 condi,此类随意缩写严重降低了代码的可阅读
  • 接口类中的方法和属性不要加任何修饰符号,保持代码的简洁性。
  • 类中布尔类型的变量,都不要加is前缀。虽然在Hybris中不会引起错误,但是看看生成的get方法为isIsXXXXX,生成的set方法为setIsXXXXXX,不易阅读。
  • 杜绝使用让人产生误解的名字。例如: 之前我使用过一个叫做getConsultantForUID的一个方法,但是返回值确实一个CustomerData。
  上述这一些规则的最终目的只有一个,就是让人更容易阅读和理解你写的代码。我知道起一个恰当的命名确实是一件费时费力的事,但是花费一些时间在这些命名上确实是有意义的,这将大大提高之后维护这些代码人的工作效率, 避免一些错误的产生。

2.方法

  对于方法来说重要的一定是短,单一职权,不要做过多的事。我在之前项目见到几百行代码的方法以及上千行代码的类。我特别尤其讨厌那种我滚动了几下也没有到头的方法, 看了后边忘了前边,来来回回反复的滚动来查看代码,看这些代码使人抓狂。
  • 每个方法应该只做一件事,做好这件事,只做这一件事。这样可以是复杂的事情变得简单化,代码也更加清晰明了。
  • 方法的参数不要太多,一般如果超过三个参数我们就需要考虑封装参数对象。
  • 不要重复自己,不要写重复的代码。重复不仅后期代码难以维护,而且是很多Bug的源头。在之前的一个项目,做一件事的方法我曾经见过4种以上的实现,这些重复的逻辑分布在不同的层级中,不仅维护困难而且导致与这个逻辑相关的功能Bug不断。
  • 参数中不要传递标识参数如True或者False,这样不仅丑陋不堪,而且使方法签名立刻变得复杂,我们需要铭记方法只做一件事。
  • 抽离Try/Catch,将错误处理逻辑与业务逻辑分离,错误处理本就是另外一件事。

 3.注释

关于注释,我想说是清晰明了的高质量代码根本就不需要写注释,如果你打算通过写注释来向别人解释你的代码,请在这之前考虑能否通过优化代码,通过代码来阐述你的意思而不是注解。保证方法和类的短小再加上有意义的命名,那么别人看我们的代码一目了然,根本就不需要注释。

  • 尽量不要写//TODO 如果非写不可,请加上作者名,和预计需要完成的时间,不然到了后期别人根本不知道这是谁写的,到底需不需要完成或者删除。
  • 如果写了注解,便要维护这个注解,不然改了代码,但是不改注解,这就是在挖坑。
  • 不需要的代码就直接删除,不要注解掉了事,这会使别人产生困惑。
  • 接口过时必须加@Deprecated注解,并清晰地说明采用的新接口或者新服务是什么。

 4.错误处理

  当我们讨论错误处理时,就一定要提及那些容易引发错误的做法。最常见的就是Null判断。我们可以想象得到我们的项目中有多少检查Null值得判断,有多少Bug是因为Null导致的。下面是我之前改的一个Bug:
public void removeUserGroup(String userGroupCode) {
        B2BCustomerModel b2bCustomer = (B2BCustomerModel)getUserService().getCurrentUser();
        UserGroupModel userGroup = getContactService().getUserGroupByCode(userGroupCode);
        Set<ContactModel> contacts = userGroup.getContacts();//throw a NPE
    }
        
    public UserGroupModel getUserGroupByCode(final B2BCustomerModel consultant, String code) {
        UserGroupModel userGroupModel = consultant.getContactGroups().stream()
                .filter(c -> StringUtils.equals(code, c.getCode())).findFirst().orElse(null);
        return userGroupModel;
     }
当时在执行contactGroup.getContacts()的时候出现了NPE, 我们一般的想法就是那就在加一个非空判断不就行了吗。但是这个Bug的真正原因却是因为我们在之前处理userGroupCode的时候代码出现了错误,导致传入了一个错误的userGroupCode。如果修改这个Bug的人并没有注意到这一点的话,就只是简单的添加了一个非空判断,这就使这个问题隐藏的更深。但是如果当时getUserGroupByCode的方法写的如下:

public UserGroupModel getUserGroupByCode(final B2BCustomerModel consultant, String code) {
    UserGroupModel userGroupModel = consultant.getContactGroups().stream()
                .filter(c -> StringUtils.equals(code, c.getCode())).findFirst().orElse(null);
  if(userGroupModel == null){
    throw new UserGroupNotFoundException(String.format("User Group[%s] not found", code))
  }
   return userGroupModel;
}
修改Bug的人很明显的就可以看到这个错误是因为userGroupCode传入错误导致的,并且找到这个Bug的真正原因。方法返回Null, 基本上是在给自己增加工作量,也是在给调用者添乱。只要有一处没有检查Null,应用程序就会失控。

5.控制语句

  在高并发场景中避免使用“等于”作为中断或者退出的条件。如果并发控制没有处理好,容易产生等值判断被击穿的情况,可以使用大于或者小于的区间判断条件来替代。
不要在判断条件中写入执行复杂业务逻辑的代码,将复杂业务逻辑的执行结果赋值给一个有意义的布尔变量名,可以大大提高代码的可阅读性。如下代码:
if ((isAfterPickUpTime && openCloseDayPart != null
      && !DAY_PART_STATUS_INSIDE.equals(openCloseDayPart.getDayPartStatus()))
      || (openCloseDayPart != null && DAY_PART_STATUS_ENDINGSOON.equals(openCloseDayPart.getDayPartStatus()))){..............}
这段代码对于阅读它的人来说简直是一种折磨。可以改成如下代码来提高阅读性:

boolean productIsAvailableInDayPart = (isAfterPickUpTime && openCloseDayPart != null
      && !DAY_PART_STATUS_INSIDE.equals(openCloseDayPart.getDayPartStatus()))
      || (openCloseDayPart != null && DAY_PART_STATUS_ENDINGSOON.equals(openCloseDayPart.getDayPartStatus()));
if(productIsAvailableInDayPart ){..............}
  循环体中执行的语句要考虑代码性能,尽量将定义对象、变量、获取数据库连接、try-catch等与循环变量无关的业务逻辑移至循环体外。
  对象应该在其需要使用的范围内在创建,例如:
for (final String code : pendingPromotionCodes){
   final PromotionSourceRuleModel sourceRule = commercePromotionService.getPromotionSourceRuleByCode(code);
   if (sourceRule != null) {
      final MyOfferData myOfferData = offerConverter.convert(sourceRule);
      if (commercePromotionService.isPromotionAvailable(sourceRule.getCode())) {
         pendingOffers.add(myOfferData);
      }
   }
}
当sourceRule不为空时offerConverter.convert(sourceRule)总会执行,但是我们只是在它有效的时候才需要执行它,因此可以做如下改变:

for (final String code : pendingPromotionCodes){
   final PromotionSourceRuleModel sourceRule = commercePromotionService.getPromotionSourceRuleByCode(code);
   if (sourceRule != null) {
      if (commercePromotionService.isPromotionAvailable(sourceRule.getCode())) {
          final MyOfferData myOfferData =offerConverter.convert(sourceRule);
         pendingOffers.add(myOfferData);
      }
   }
}

6.集合处理

  ArrayList的subList结果不可以强转成ArrayList,否则会抛出ClassCastException异常。subList 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList而是ArrayList 的一个视图,对于SubList子列表的所有操作最终会反映到原列表上。在subList场景中,高度注意对原集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生ConcurrentModificationException 异常。
  使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常。asList的返回对象是一个Arrays内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。
  集合泛型定义时,在JDK7及以上,使用diamond语法或全省略。菱形泛型,即diamond,直接使用<>来指代前边已经指定的类型。如下:
// <> diamond方式
HashMap<String, String> userCache = new HashMap<>(16);
// 全省略方式
ArrayList<User> users = new ArrayList(10);
  使用集合转数组的方法,必须使用集合的toArray(T[] array),传入的是类型完全一样的数组,大小就是list.size()。使用toArray带参方法,入参分配的数组空间不够大时,toArray方法内部将重新分配内存空间,并返回新数组地址;如果数组元素个数大于实际所需,下标为[ list.size() ]的数组元素将被置为null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素个数一致。直接使用toArray无参方法存在问题,此方法返回值只能是Object[]类,若强转其它类型数组将出现ClassCastException错误。
  集合初始化时,指定集合初始值大小。例如HashMap使用HashMap(int initialCapacity) 初始化。initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意 负载因子(即loader factor)默认为 0.75,如果 暂时无法 确定 初始值大小,请设置为 16(即默认值)。假如一个HashMap需要 放置 1024个元素, 由于 没有设置容量 初始大小,随着元素不断增加容 量 7次被迫扩大, resize需要重建 hash表,严重影响性能。
  Map类集合K/V能不能存储null值的情况,如下表格:
 
集合类 key value super 说明
Hashtable 不允许为null 不允许为null Dictionary 线程安全
ConcurrentHashMap 不允许为null 不允许为null AbstractMap 锁分段技术(JDK8:CAS)
TreeMap 不允许为null 允许为null AbstractMap 线程不安全
HashMap 允许为null 允许为null AbstractMap

线程不安全

合理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响。有序性是指遍历的结果是按某种比较规则依次排列的。稳定性指集合每次遍历的元素次序是一定的。例如:ArrayList是order/unsort;HashMap是unorder/unsort;TreeSet是order/sort。

7.并发处理

线程池不要使用 Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的人更加明确线程池运行规则,避资源耗尽风险。Executors返回的线程池对象的弊端 如下 :  
  • FixedThreadPool和SingleThreadPool线程的等待队列长度为Integer.MAX_VALUE,可能会堆积大量请求,从而导致OOM。
  • CachedThreadPool和ScheduledThreadPool的线程最大数量为Integer.MAX_VALUE,可能会创建大量请求,从而导致OOM。
  高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。尽可能使加锁的代码块工作量尽可能的小。
对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。线程一需要对表A、B、C依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是A、B、C,否则可能出现死锁。
并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用version作为更新依据。
使用CountDownLatch进行异步转同步操作,每个线程退出前必须调用countDown方法,线程执行代码注意catch异常,确保countDown方法被执行到,避免主线程无法执行至await方法,直到超时才返回结果。子线程抛出异常堆栈,不能在主线程try-catch到。
避免Random实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一实例导致的性能下降。Random实例包括java.util.Random 的实例或者 Math.random()的方式。在JDK7之后,可以直接使用API ThreadLocalRandom,而在 JDK7之前,需要编码保证每个线程持有一个实例。
在并发场景下,通过双重检查锁(double-checked locking)实现延迟初始化的优化问题隐患(可参考 The "Double-Checked Locking is Broken" Declaration),推荐解决方案中较为简单一种(适用于JDK5及以上版本),将目标属性声明为 volatile型。如下:
public class DoubleCheckedDemo {
    private DoubleCheckedDemo(){}
    private static volatile DoubleCheckedDemo doubleCheckedDemo = null;
    public static DoubleCheckedDemo getInstance()
    {
        if (doubleCheckedDemo == null){
            synchronized (DoubleCheckedDemo.class){
                if(doubleCheckedDemo == null)
                {
                    doubleCheckedDemo = new DoubleCheckedDemo();
                }
            }
        }
        return doubleCheckedDemo;
    }
}

  volatile解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。如果是count++操作,使用如下类实现:AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是JDK8,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)。

HashMap在容量不够进行resize时由于高并发可能出现死链,导致CPU飙升,在开发过程中可以使用其它数据结构或加锁来规避此风险。
 

参考资料:

  代码整洁之道 Clean Code

  https://www.jianshu.com/p/59f5a5cbdf7c

 

 

集合类

posted @ 2018-09-24 20:52  Dan_Go  阅读(892)  评论(0编辑  收藏  举报