愿大家都写出优雅知性的代码
提前return
丑
if``(condition){
//do something
}``else``{
return ;
}
优雅
if``(!condition){
return ;
}
//do something
条件分支如果有只return的逻辑,考虑可以提前return,简化代码结构
使用三目表达式
丑
String title = ""``;
if (condition) {
title = "hello"``;
} else {
title = "world"``;
}
优雅
String title = condition ? "hello" : "world"
使用条件三目运算符可以简化某些if-else,使代码更加简洁,更具有可读性
让判断条件做真正的选择
if (``0 == retCode) {
SendMsg(``"000"``, "Process Success"``, outResult);
} else {
SendMsg(``"000"``, "Process Failure"``, outResult);
}
乍一看,这段代码还算比较简短。那下面这段呢?
if``(!strcmp(pRec->GetType(), RECTYPE::INSTALL)) {
CommDM.ChangGroupInfo(const_cast(CommDM.GetAttr(``"IPAddress"``, &(pGroup->m_Attr))), true``);
} else {
CommDM.ChangGroupInfo(const_cast(CommDM.GetAttr(``"IPAddress"``, &(pGroup->m_Attr))), false``);
}
看出来问题了吗?经过仔细的对比,我们发现,如此华丽的代码,if/else 的执行语句真正的差异只在于一个参数。第一段代码,二者的差异只是发送的消息,第二段代码,差异在于最后那个参数。
看破这个差异之后,新的写法就呼之欲出了,以第一段代码为例:
const char``* msg = (``0 == retCode ? "Process Success" : "Process Failure"``);
SendMsg(``"000"``, msg, outResult);
为了节省篇幅,我选择了条件表达式。我知道,很多人不是那么喜欢它。如果 if/else 依旧是你的大爱,勇敢追求去吧!
由这段代码调整过程,我们得出一个简单的规则:
让判断条件做真正的选择。
对于前面调整的代码,判断条件真正判断的内容是消息的内容,而不是消息发送的过程。经过我们的调整,获取消息内容和发送消息的过程严格分离开来。
消除了代码中的冗余,代码也更容易理解,同时,给未来留出了可扩展性。如果将来 retCode 还有更多的情形,我们只要调整消息获取的部分进行调整就好了。当然,封装成函数是一个更好的选择,这样代码就变成了:
SendMsg(``"000"``, msgFromRetCode(retCode),outResult);
至于第二段代码的调整,留给你练手了。
这样丑陋的代码是如何从众多代码中脱颖而出的呢?很简单,只要看到,if/else 执行块里面的内容相差无几,需要我们人工比字符寻找差异,恭喜你,你找到它了。
长长的条件
if (strcmp(type, “DropGroup") == 0
|| strcmp(type, "CancelUserGroup"``) == 0
|| strcmp(type, "QFUserGroup"``) == 0
|| strcmp(type, "CancelQFUserGroup"``) == 0
|| strcmp(type, "QZUserGroup"``) == 0
|| strcmp(type, "CancelQZUserGroup"``) == 0
|| strcmp(type, "SQUserGroup"``) == 0
|| strcmp(type, "CancelSQUserGroup"``) == 0
|| strcmp(type, “UseGroup") == 0
|| strcmp(type, "CancelGroup"``) == 0``)
就我接触过的代码而言,这并不是最长的判断条件。这种代码极大的开拓了我的视野,现在的我,即便面前是一屏无法容纳的条件,也可以坦然面对了,虽然显示器越来越大。
其实,如果这个判断条件是这个函数里仅有的东西,我也是可以接受的。遗憾的是,大多数情况下,这只不过是一个更大函数中的一小段而已。为了让这段代码可以接受一些,我们不妨稍做封装:
boolean shouldExecute(``const char``* type) {
return (strcmp(type, “DropGroup") == 0
|| strcmp(type, "CancelUserGroup"``) == 0
|| strcmp(type, "QFUserGroup"``) == 0
|| strcmp(type, "CancelQFUserGroup"``) == 0
|| strcmp(type, "QZUserGroup"``) == 0
|| strcmp(type, "CancelQZUserGroup"``) == 0
|| strcmp(type, "SQUserGroup"``) == 0
|| strcmp(type, "CancelSQUserGroup"``) == 0
|| strcmp(type, “UseGroup") == 0
|| strcmp(type, "CancelGroup"``) == 0``);
}
if (shouldExecute(type)) {
...
}
现在,虽然条件依然还是很多,但比起原来庞大的函数,至少它已经被控制在一个相对较小的函数里了。更重要的是,通过函数名,我们终于有机会告诉世人这段代码判断的是什么了。
虽然提取函数把这段代码混乱的条件分离开来,它还是可以继续改进的。比如,我们把判断的条件进一步提取
boolean shouldExecute(``const char``* type) {
static const char``* execute_type[] = {
"DropGroup"``,
"CancelUserGroup"``,
"QFUserGroup"``,
"CancelQFUserGroup"``,
"QZUserGroup"``,
"CancelQZUserGroup"``,
"SQUserGroup"``,
"CancelSQUserGroup"``,
"UseGroup"``,
"CancelGroup"
};
int size = ARRAY_SIZE(execute_type);
for (``int i = 0``; i < size; i++) {
if (strcmp(type, execute_type[i]) == 0``) {
return true``;
}
}
return false``;
}
这样的话,如果以后要加一个新的 type,只要在数组中增加一个新的元素即可。这段代码还可以进一步封装,把这个 type 列表变成声明式,进一步提高代码的可读性。
简单的理解声明式的风格,就是描述做什么,而不是怎么做。
发现这种代码很容易,只要看到在长长的判断条件,就是它了。要限制这种代码的存在,只要以设定一个简单的规则:
判断条件里面不允许多个条件的组合
在实际的应用中,我们会把“3”定义为“多”,也就是如果有两个条件的组合,可以接受,如果是三个,还是改吧!
虽然通过不断调整,这段代码已经不同于之前,但它依然不是我们心目中的理想代码。出现这种代码,往往意味背后有更严重的设计问题。
switch 陷阱
switch``(firstChar) {
case ‘N’:
nextFirstChar = ‘O’;
break``;
case ‘O’:
nextFirstChar = ‘P’;
break``;
case ‘P’:
nextFirstChar = ‘Q’;
break``;
case ‘Q’:
nextFirstChar = ‘R’;
break``;
case ‘R’:
nextFirstChar = ‘S’;
break``;
case ‘S’:
nextFirstChar = ‘T’;
break``;
case ‘T’:
throw new IllegalArgument();
default``:
}
出于多年编程养成的条件反射,我对于 switch 总会给予更多的关照。在那本大名鼎鼎《重构》里,Martin Fowler 专门把 switch 语句单独拿出来作为一种坏味道来讨论。研习面向对象编程之后,看见 switch,我就会联想到多态,遗憾的是,这段代码和多态没什么关系。仔细阅读这段代码,我找出了其中的规律,nextFirstChar 就是 firstChar 的下一个字符。于是,我改写了这段代码:
switch``(firstChar) {
case ‘N’:
case ‘O’:
case ‘P’:
case ‘Q’:
case ‘R’:
nextFirstChar = firstChar + 1``;
break``;
case ‘T’:
throw new IllegalArgument();
default``:
}
现在,至少看起来,这段代码已经比原来短了不少。当然这么做基于一个前提,就是这些字母编码的顺序确确实实是连续的。从理论上说,开始那段代码适用性更强。但在实际开发中,我们碰到字母不连续编码的概率趋近于 0。
但这段代码究竟是如何产生的呢?我开始研读上下文,原来这段代码是用当前 ID 产生下一个 ID 的,比如当前是 N0000,下一个就是 N0001。如果数字满了,就改变字母,比如当前 ID 是 R9999,下一个就是 T0000。在这里,字母也就相当于一位数字,根据情况进行进位,所以有了这段代码。
代码上的注释告诉我,字母的序列只有从 N 到 T,根据这个提示,我再次改写了这段代码:
if (firstChar >= ‘N’ && firstChar <= ‘S‘) {
nextFirstChar = firstChar + 1``;
} else {
throw new IllegalArgument();
}
这里统一处理了字母为 T 和 default 的情形,严格说来,这和原有代码并不完全等价。但这是了解了需求后做出的决定,换句话说,原有代码在这里的处理中存在漏洞。
修改这段代码,只是运用了非常简单的编程技巧。遗憾的是,即便如此简单的编程技巧,也不是所有开发人员都驾轻就熟的,很多人更习惯于“平铺直叙”。 这种直白造就了代码中的许多鸿篇巨制。我听过不少“编程是体力活”的抱怨,不过,能把写程序干成体力活,也着实不值得同情。写程序,不动脑子,不体力才怪。
无论何时何地,只要 switch 出现在眼前,请提高警惕,那里多半有坑。
分家的声明和使用
经过认真仔细的查看,或是使用传说的中“查找”功能,我们发现上面提到的那个变量间隔9行才使用到。
代码的声明和使用应尽量接近
类型转换和业务处理逻辑揉杂在一起
/**
* 扣款结果推送通知
*
* @param data
* @return
* @throws Exception
*/
@Override
public Result<?> process(DeductPushResultNotifyRequestDTO data) throws Exception {
String vbsSerialNumber = data.getVbsSerialNumber();
Long businessId = data.getBusinessId();
String title = String.format(``"【扣款结果推送通知】扣款分配指令流水【%s】订单号【%s】"``, vbsSerialNumber, businessId);
// 参数校验
if (StrUtil.isBlank(vbsSerialNumber)) {
log.warn(``"{} 参数为空,处理流程结束。"``, title);
return Result.fail(ResultCodeEnum.ILLEGAL_PARAMS);
}
if (!DeductionResultStatusEnum.SUCCESS.getCode().equals(data.getResult()) && !DeductionResultStatusEnum.FAIL.getCode().equals(data.getResult())) {
log.warn(``"{} 扣款未获取到最终结果,处理流程结束。"``, title);
return Result.fail(ResultCodeEnum.ILLEGAL_PARAMS);
}
// 扣款结果保存
if (``"manualRepay"``.equals(data.getType())) {
BillRepayManualEntity repayManualEntity = repayManualDAO.queryByVbsSerialNumber(vbsSerialNumber);
if (``null == repayManualEntity) {
log.warn(``"{} 手工扣款信息不存在,处理流程结束。"``, title);
return Result.fail(ResultCodeEnum.DATA_NOT_EXIST);
}
DeductQueryData deductResult = new DeductQueryData();
deductResult.setDeductTime(DateUtils.dateToString(repayManualEntity.getCreatedTime(), DateUtils.DATE_PATTERN_YYYY_MM_DD_HH_MM_SS));
deductResult.setExternalDeductAmount(data.getAmount());
deductResult.setRemark(repayManualEntity.getRemark());
deductResult.setExternalDesc(data.getDesc());
deductResult.setExternalNo(repayManualEntity.getRepayApplyNo());
deductResult.setType(``1``);
deductResult.setReduceAmount(data.getDeductionAmount());
deductResult.setStatus(DeductionResultStatusEnum.getStatusByCode(data.getResult()).getCode());
deductResult.setSuccessAmount(data.getSuccessAmount());
deductResult.setMessage(data.getDesc());
deductResult.setExternalCode(``""``);
deductResult.setPlatformBackTime(DateUtils.dateToString(LocalDateTime.now(), DateUtils.DATE_PATTERN_YYYY_MM_DD_HH_MM_SS));
deductResult.setPlatformSerialNum(``""``);
deductResult.setReasonCode(``""``);
deductResult.setSentTime(DateUtils.dateToString(LocalDateTime.now(), DateUtils.DATE_PATTERN_YYYY_MM_DD_HH_MM_SS));
deductHandler.deductFill(repayManualEntity, deductResult);
} else if (``"offlineRepay"``.equals(data.getType())) {
BillRepayManualEntity repayManualEntity = repayManualDAO.queryByVbsSerialNumber(vbsSerialNumber);
if (``null == repayManualEntity) {
BillRepayManualPO billRepayManualPO = billRepayManualRepository.findByVbsSerialNumber(vbsSerialNumber);
if (billRepayManualPO != null``) {
repayManualEntity = new BillRepayManualEntity();
BeanUtil.copyProperties(billRepayManualPO, repayManualEntity);
repayManualEntity.setState(DeductionResultStatusEnum.getStatusByCode(data.getResult()).getCode());
repayManualEntity.setStep(``3``);
repayManualEntity.setDeductAmount(data.getSuccessAmount());
if (ObjectUtil.isNotNull(data.getAmount()) && ObjectUtil.isNotNull(data.getDeductionAmount())) {
repayManualEntity.setBalance(data.getAmount().subtract(repayManualEntity.getDeductAmount()));
if (repayManualEntity.getState().equals(DeductionStatusEnum.SUCCESS.getCode())) {
repayManualEntity.setSuccessAmount(data.getSuccessAmount());
}
}
repayManualDAO.save(repayManualEntity);
ManualDeductResultMsgDTO manualDeductResultMsgDTO = new ManualDeductResultMsgDTO();
manualDeductResultMsgDTO.setRepayApplyNo(repayManualEntity.getRepayApplyNo());
manualDeductResultMsgDTO.setSource(repayManualEntity.getSource());
manualDeductResultMsgDTO.setRepayApplyNo(repayManualEntity.getRepayApplyNo());
manualDeductResultMsgDTO.setOrderId(repayManualEntity.getOrderId());
manualDeductResultMsgDTO.setAmount(BigDecimalVOSUtils.setScale(repayManualEntity.getSuccessAmount()));
manualDeductResultMsgDTO.setDeductStatus(DeductionResultStatusEnum.getStatusByCode(data.getResult()).getCode());
OrderDTO orderDTO = orderTagDAO.buildOrder(repayManualEntity.getOrderId(), OrderInfoEnum.ORDER_CREDIT);
manualDeductResultMsgDTO.setProductCode(orderDTO.getOrderCredit().getProductCode());
if (StrUtil.equals(orderDTO.getOrderCredit().getOrderStatus(), OrderStatusEnum.NORMAL_REPAYMENT.getCode())) {
manualDeductResultMsgDTO.setOrderStatus(``1``);
} else if (StrUtil.equals(orderDTO.getOrderCredit().getOrderStatus(), OrderStatusEnum.CLEAR_UP.getCode())) {
manualDeductResultMsgDTO.setOrderStatus(``2``);
}
manualDeductResultMsgDTO.setDeductTime(DateUtils.dateToString(repayManualEntity.getCreatedTime(), DateUtils.DATE_PATTERN_YYYY_MM_DD_HH_MM_SS));
manualDeductResultNotifyService.process(manualDeductResultMsgDTO);
}
}
}
return Result.success();
}
大家发现没有,一个方法如此之长,80%的代码是在做对象赋值,无任何业务逻辑处理,放在和业务逻辑处理在一起,可读性非常差。我们把和业务逻辑不相关的代码给剥离出去,要么放在一个单独的转换类进行调用,要么单独抽取一个方法进行调用。我们认为赋值超过4-5行就是多,就应该从业务剥离出。
你有多少if缩紧
if (condition1) {
//do something1
if (condition2) {
//do something2
if (condition3) {
//do something3
} else {
//do other something3
}
} else {
//do othersomething2
}
}
多层缩进是那种放在代码海一眼就可以认出来的代码,用一条简单的规则就可以限制它:
不允许出现多层缩进。
按照我的喜好,3 就意味着“多”了。对于 switch,我会给予特别的关照,因为 switch 一旦出场,条件少了,你都不好意思和人打招呼,再缩进就找不到北了。于是,对 switch 而言,我以为 2 就是多了,也就是说,switch 里面就别再缩进了。
写代码,千万别退让太多。
封装全局变量
尽量不要访问全局变量,使用函数代替
丑
private String status = 1``;
if (entity.getStatus == status) {
//doSomeThing
}
优雅
private boolean isCurrentStatus(``int status) {
return status = gStatus;
}
if``(isCurrentStatus(entity.getStatus())){
//do Something
}
封装代替嵌套容器
当迭代开始嵌套,考虑封装
丑
List<Map<String, String>> configurations;
for (Map<String, String> configuration : configurations) {
for (Map.Entry<String, String> entry : configuration.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
优雅
public class ConfigurationItem{
private String key;
private String value;
//....
}
List<ConfigurationItem> configurations;
for``(ConfigurationItem item:configurations){
System.out.println(item.getKey() + ":" + item.getValue());
}
IDEA 黄金油插件
阿里代码检查规范-P3C,每次提交都看一下控制台的check结果
Alibaba Java Coding Guidelines