前言
本文主要针对日常在进行代码review过程中发现的一些关于方法设计上的一些常见问题,改进这些问题能够有效的提升方法的可用性、健壮性、可维护性等方面问题。
方法的基本要求
方法名应该见名知意。
我们看如下一段方法,想要完成的行为就是把订单更新为充电中,但我们无法直接从方法名理解方法意图。
public void dealOrder(long orderId){
Order order = orderMapper.getOrderById();
order.setStatus(OrderStatus.CHARGING);
orderMapper.update(order);
}
让我们给方法名加上具体目的
public void dealOrderStatus(long orderId){
Order order = orderMapper.getOrderById();
order.setStatus(OrderStatus.CHARGING);
orderMapper.update(order);
}
实际上,只有从业务含义出发才更能提现出方法的含义。
public void startCharging(long orderId){
Order order = orderMapper.getOrderById();
order.setStatus(OrderStatus.CHARGING);
orderMapper.update(order);
}
方法参数不要太多
建议参数不能超过5
个,太多的参数不但记不住,也不利于代码维护,还容易出错,尤其是那种相同类型的参数但顺序不一样,这让使用者很难记住这些参数的顺序,而且有时候在弄错顺序的情况下,并不影响编译和运行,但潜在的线上问题却已经开始了。
解决长参数的方式通常有下面几种:
封装成对象使用
public static void longParam(String userName,
String userId,
String userAccount,
String phone,
String address) {
User user = new User();
user.setUserName(userName);
user.setUserId(userId);
user.setUserAccount(userAccount);
user.setPhone(phone);
user.setAddress(address);
// ...
}
// 改成对象
public static void longParam(User user) {
// ...
}
混合类型的长参数列表
public static void longParam(String userAccount,
String userName,
String orderId,
BigDecimal amount,
String orderChannel) {
}
// 封装成一个混合类型的对象
@Data
class RequestParam{
private String userAccount;
private String userName;
private String orderId;
private BigDecimal amount;
private String orderChannel;
}
// 使用时再一个个取出来
public static void longParam(RequestParam requestParam){
User user = new User();
user.setUserAccount(requestParam.getUserAccount());
user.setUserName(requestParam.getUserName());
Order order = new Order();
order.setOrderId(requestParam.getOrderId());
order.setOrderChannel(requestParam.getOrderChannel());
order.setAmount(requestParam.getAmount());
}
如果感觉别扭,当然可以进行优化,比如像下面这样: 给RequestParam
分别封装一个构建Order
和User
对象的方法。
@Data
class RequestParam {
private String userAccount;
private String userName;
private String orderId;
private BigDecimal amount;
private String orderChannel;
public Order newOrder() {
return Order.builder()
.amount(amount)
.orderChannel(orderChannel)
.orderId(orderId)
.build();
}
public User newUser() {
return User.builder()
.userAccount(userAccount)
.userName(userName)
.build();
}
}
// 在使用时就可以像这样
public static void longParam(RequestParam requestParam){
User user = requestParam.newUser();
Order order = requestParam.newOrder();
}
将多个参数封装成对象的方式还有个非常重要的原因就是可以有效地提升方法的兼容性,在方法作为接口对外暴露时,一旦添加或者删除了新的参数,如果没有封装成对象,那么调用者是一定要改的,但如果封装成了一个对象,那么老的远程调用者就很有可能不需要做任何修改。
单个方法不要长
我知道有很多公司都有对一个方法的行数限制要求,比如不能超过50行、甚至20行,我觉得这样不分场合的硬性要求并不合理,我们应该搞清楚长函数会带来哪些问题?由问题为出发点,去检查方法的行数,有时候10行可能都写得不好,有时候即使50行了但也没问题,像Sping、MyBatis源码中超过50行的方法也有很多。
其实大多数造成长函数的原因就是封装、设计得不够,违背了单一职责的原则,导致代码的可维护性变差,对重复的逻辑没有封装,就像看C语言的代码一样,面向过程,平铺直叙地完成整个业务逻辑。
几行代码也并不一定表示没问题。
public static void longMethod(String userId, BigDecimal price) {
// 判断用户是否是VIP
User user = userDao.getUserByUserId(userId);
int memberAttr = user.getUserMemberAttr();
double discountPrice;
// VIP用户打8折,其他用户打9折
if (memberAttr == 1) {
discountPrice = price.multiply(new BigDecimal(0.8)).doubleValue();
} else {
discountPrice = price.multiply(new BigDecimal(0.9)).doubleValue();
}
}
抽象出通用逻辑
// 根据用户ID获取用户会员属性
private static int getUserMemberAttr(String userId) {
User user = userDao.getUserByUserId(userId);
return user.getUserMemberAttr();
}
// 根据会员属性计算折扣价
private static double getDiscountPrice(int memberAttr, BigDecimal price) {
double discountPrice;
if (memberAttr == 1) {
discountPrice = price.multiply(new BigDecimal(0.8)).doubleValue();
} else {
discountPrice = price.multiply(new BigDecimal(0.9)).doubleValue();
}
return discountPrice;
}
// 最后具体业务逻辑应该是这样
public static void longMethod(String userId, BigDecimal price) {
// 获取用户会员属性
int memberAttr = getUserMemberAttr(userId);
// 根据会员属性获取折扣价
double discountPrice = getDiscountPrice(memberAttr, price);
}
关于参数的校验
首先我们来谈谈关于方法参数合法性校验的问题,很明显这样做的目的是为了能够以最快的方式的返回失败或者将错误检测出来,如果方法在一开始没有进行参数的合法性校验,那么也就意味着会在之后的方法执行时的某个过程中产生失败,而如果这个时候再来排查则会显得比较麻烦,更坏的情况是,可能这个方法也能正常的返回了,但是由于未考虑到非法参数的场景而导致计算出了错误的结果,又或者是其他不可预知的结果。
在JDK中实际上定义很多关于方法参数校验的异常,比如下面的几个示例:
HashMap
构造方法中的IllegalArgumentException
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
ArrayList get()
方法中的IndexOutOfBoundsException
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
ArrayList removeAll()
方法中的NullPointerException
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
public static <T> T requireNonNull(T obj) {
if (obj == null)
throw new NullPointerException();
return obj;
}
上述几个例子中,无一例外地都在方法执行之前对参数进行校验,当然这也并非没有例外情况,本条说明只是希望读者能够仔细考虑关于入参的校验问题,并最好能在方法的文档中说明清楚。
注意返回值为null的情况
非必要情况时,不建议方法直接返回null
值,尤其返回类型时数组或者集合时。
返回空集合,而不是返回null
public List<String> demo() {
return Collections.emptyList();
}
很明显,这样做的目的是为了能够让使用者减少因为忘记处理null
情况而产生空指针异常。
谨慎使用重载方法
以下是一ArrayList
集合中一个比较坑的重载方法,可能很多人都有遇到过。
执行下面这段代码,如果你期望打印出的结果是[2,3]
,则会让你失望了,实际上程序打印的结果为[1,3]
。
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 1; i <= 3; i++) {
list.add(i);
}
list.remove(1);
System.out.println(list);
}
如果想要结果是[2,3]
,则应该改为如下这样:
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
for (int i = 1; i <= 3; i++) {
list.add(i);
}
list.remove((Integer)1);
System.out.println(list);
}
如果一定要使用重载方法,则至少应该保证,当入参为同一个值时,所有重载方法的执行逻辑必须是一致的,否则就很容易出现像上面remove()
方法那样的问题。