代码之丑

151 阅读5分钟

愿大家都写出优雅知性的代码

提前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