前言
本次分享主要是第十一章,迭进,通过跌进设计达到整洁的目的。
根据书中的描述,只要遵循一下规则,设计就能变得“简单”:
1.运行所有测试;
2.不可重复;
3.表达了程序员的意图;
4.尽可能减少类和方法的数量;
以上规则按其重要程度排列。
1.运行所有测试
代码编写首先得保证它是能按照需求实现功能的,而检验这一标准的方法就是测试。测试编写得越多,越能帮助我们创建更好的设计。
2.重构
提升内聚性,降低耦合度,切分关注面,模块化系统性关注面,缩小函数和类的尺寸,选用更好的名称,如此等等。
3.不可重复
它代表着额外的工作、额外的风险和额外且不必要的复杂度。在遇到这种情况我们一般首先会想到共性抽取,抽出公用的方法。
4.表达力
把代码写的越清晰,其他人在理解代码上花的时间越少,从而减少缺陷,缩减维护成本。
可以通过一下方式提高表达准确性:
(1)选用好的名称来表达
(2)保持函数和类尺寸短小
(3)采用标准命名法
(4)编写良好的单元测试
(5)尝试
5.尽可能减少类和方法
这条规则优先级最低,但也很重要
6.代码分析
源码:
**
* 根据条件进行渠道路由,并记录在transfer的extraParams中
* @param transfer
*/
private void routeMethod(Transfer transfer) {
BaseTransferParam transferParam;
BankAccountInfo bankAccountInfo;
if (TransferTypeEnum.WITHDRAW == transfer.getTransferType() ||
TransferTypeEnum.DISTRIBUTE == transfer.getTransferType()) {
transferParam = JSON.parseObject(transfer.getExtraParams(), WithdrawTransferParam.class);
WithdrawTransferParam withdrawParam = (WithdrawTransferParam) transferParam;
bankAccountInfo = convert2BankAccountInfo(withdrawParam);
// 转账参数有cardToken,并且参数中没有收款银行信息时,通过pci查询卡资产
if (StringUtils.isNotBlank(withdrawParam.getCardToken()) && StringUtils.isBlank(bankAccountInfo.getBankCode()) &&
StringUtils.isBlank(bankAccountInfo.getSwiftCode())) {
FundResult<BankAccountInfoDTO> result = pciExternalService.queryBankAccountInfo(withdrawParam.getCardToken(), transfer.getUserId());
AssertUtil.isTrue(result.isSuccess(), FinResultCode.ACCOUNT_NOT_EXIST, "queryBankAccountInfo failed,cardToken:%s,errorMsg:%s",
withdrawParam.getCardToken(), result.getErrorMessage());
bankAccountInfo = convert2BankAccountInfo(result.getModule());
transferParam.setBankCode(bankAccountInfo.getBankCode());
transferParam.setSwiftCode(bankAccountInfo.getSwiftCode());
transfer.setExtraParams(JSON.toJSONString(transferParam));
}
} else if (TransferTypeEnum.ALLOCATE == transfer.getTransferType()) {
transferParam = JSON.parseObject(transfer.getExtraParams(), AllocateTransferParam.class);
bankAccountInfo = convert2BankAccountInfo(transferParam);
} else {
throw FundException.build(FinResultCode.BUSINESS_NOT_SUPPORT, "unknown transferType={}", transfer.getTransferType());
}
/**
* 1. 根据收款账号信息,寻找同行的serviceChannel
*/
if(StringUtils.isBlank(transfer.getServiceChannel())) {
String beneficiaryBankName = transferParam.getBankName();
List<String> serviceChannels = serviceChannelRoutingEngine.getServiceChannels(
beneficiaryBankName,
transferParam instanceof WithdrawTransferParam? ((WithdrawTransferParam) transferParam).getCardToken() : null
);
if(serviceChannels.size() == 1) {
transfer.setServiceChannel(serviceChannels.get(0));
}
}
/**2. 通过serviceChannel、转账条件,筛选可用的paymentMethod */
if(IPAY_SERVICE_CHANNEL.equals(transfer.getServiceChannel())) {
/** citi.transfer.001固定使用ipay.Promptpay */
transfer.setPaymentMethod(PayMethodType.PromptPay);
transfer.setExchangeType(ExchangeType.RT_BRIDGE);
} else if (TransferTypeEnum.DISTRIBUTE == transfer.getTransferType()
&& StringUtils.isNotBlank(transfer.getUserId())
&& FluxTransferDiamondCache.getTransferDftMidList().contains(transfer.getUserId())) {
/** 开环针对提现代发商户userId进行判断,如果是白名单内的商户,都走DFT */
transfer.setPaymentMethod(PayMethodType.DFT);
transfer.setExchangeType(ExchangeType.FLUX_BATCH);
} else {
boolean isBkt = serviceChannelRoutingEngine.isBkt(bankAccountInfo.getSwiftCode());
int promptPayStatus = serviceChannelRoutingEngine.promptPayStatus(transfer.getUserId(), transfer.getServiceChannel(), bankAccountInfo.getBankCode(),
transfer.getTransferAmount());
int fastStatus = serviceChannelRoutingEngine.fastStatus(bankAccountInfo.getSwiftCode());
String paymentMethodRoute = serviceChannelRoutingEngine.paymentMethodRoute();
Money moneyBoundDFT = serviceChannelRoutingEngine.getDFTMoneyConfig();
if (isBkt) {
//同行转账优先
transfer.setPaymentMethod(PayMethodType.BKT);
transfer.setExchangeType(ExchangeType.FLUX_BATCH);
} else if(promptPayStatus >= 0) {
transfer.setPaymentMethod(PayMethodType.PromptPay);
transfer.setExchangeType(ExchangeType.RT_BRIDGE);
//通过开关判断promptPay是否在维护,需要hold
if(promptPayStatus == 0) {
transfer.setStatus(TransferStatus.HOLD);
}
} else if (StringUtils.isNotEmpty(paymentMethodRoute)) {
transfer.setPaymentMethod(paymentMethodRoute);
transfer.setExchangeType(ExchangeType.FLUX_BATCH);
} else if (transfer.getTransferAmount().isGreaterThan(moneyBoundDFT)) {
//超过DFT阈值的使用DFT
transfer.setPaymentMethod(PayMethodType.DFT);
transfer.setExchangeType(ExchangeType.FLUX_BATCH);
} else if(fastStatus >= 0){
transfer.setPaymentMethod(PayMethodType.FAST);
transfer.setExchangeType(ExchangeType.FLUX_BATCH);
} else{
//其他使用ACH
transfer.setPaymentMethod(PayMethodType.ACH);
transfer.setExchangeType(ExchangeType.FLUX_BATCH);
}
}
String exchangeCode = serviceChannelRoutingEngine.getExchangeCode(transfer.getServiceChannel(), transfer.getPaymentMethod());
transfer.setExchangeCode(exchangeCode);
transferRepository.update(transfer);
transfer.setVersion(transfer.getVersion() + 1);
}
问题:
1.方法代码过长
2.部分if…else缺少注释
3.没有做方法抽取
上面的代码因为方法里面包含很多if…else,所以对这个方法的测试也跟着些得很复杂,有很多重复代码。
@Test
public void testCreateTransferIfRouteMethod1() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
MockitoAnnotations.initMocks(this);
Transfer transfer = getTransfer();
transfer.setStatus(TransferStatus.ACCEPTED);
transfer.setTransferId("test");
FundResult<BankAccountInfoDTO> mockResult = mock(FundResult.class);
Mockito.doReturn(mockResult).when(pciExternalService).queryBankAccountInfo(any(String.class), any(String.class));
Mockito.when(transferDomainService.create(transfer)).thenReturn(any());
Mockito.when(serviceChannelRoutingEngine.isBkt("test")).thenReturn(true);
Mockito.when(serviceChannelRoutingEngine.promptPayStatus("1", "2", "3", Money.ofMinorUnit("THB", 1))).thenReturn(1);
Mockito.when(serviceChannelRoutingEngine.fastStatus("1")).thenReturn(1);
Mockito.when(serviceChannelRoutingEngine.getDFTMoneyConfig()).thenReturn(Money.ofMinorUnit("THB", 1));
Mockito.when(serviceChannelRoutingEngine.getExchangeCode("1", "2")).thenReturn("any");
//convert2BankAccountInfo()
CreateTransferRequest transferRequest = getCreateTransferRequest();
transferRequest.setTransferTypeEnum(TransferTypeEnum.DISTRIBUTE);
String result = transferService.createTransfer(transferRequest);
Assert.assertEquals(result, null);
}
@Test
public void testCreateTransferIfRouteMethod2() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
MockitoAnnotations.initMocks(this);
Transfer transfer = getTransfer();
transfer.setStatus(TransferStatus.ACCEPTED);
FundResult<BankAccountInfoDTO> mockResult = mock(FundResult.class);
Mockito.doReturn(mockResult).when(pciExternalService).queryBankAccountInfo(any(String.class), any(String.class));
Mockito.when(transferDomainService.create(transfer)).thenReturn(any());
Mockito.when(serviceChannelRoutingEngine.isBkt("test")).thenReturn(true);
Mockito.when(serviceChannelRoutingEngine.promptPayStatus("1", "2", "3", Money.ofMinorUnit("THB", 1))).thenReturn(1);
Mockito.when(serviceChannelRoutingEngine.fastStatus("1")).thenReturn(1);
Mockito.when(serviceChannelRoutingEngine.getDFTMoneyConfig()).thenReturn(Money.ofMinorUnit("THB", 1));
Mockito.when(serviceChannelRoutingEngine.getExchangeCode("1", "2")).thenReturn("any");
List<String> serviceChannels = new ArrayList<>();
serviceChannels.add("citi.transfer.001");
Mockito.when(serviceChannelRoutingEngine.getServiceChannels("BankName_test",null )).thenReturn(serviceChannels);
CreateTransferRequest transferRequest = getCreateTransferRequest();
transferRequest.setServiceChannel("");
transferRequest.setTransferTypeEnum(TransferTypeEnum.ALLOCATE);
String result = transferService.createTransfer(transferRequest);
Assert.assertEquals(result, null);
}
@Test
public void testCreateTransferIfRouteMethod3() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
MockitoAnnotations.initMocks(this);
Transfer transfer = getTransfer();
transfer.setStatus(TransferStatus.ACCEPTED);
FundResult<BankAccountInfoDTO> mockResult = mock(FundResult.class);
Mockito.doReturn(mockResult).when(pciExternalService).queryBankAccountInfo(any(String.class), any(String.class));
Mockito.when(transferDomainService.create(transfer)).thenReturn(any());
Mockito.when(serviceChannelRoutingEngine.isBkt("test")).thenReturn(true);
Mockito.when(serviceChannelRoutingEngine.promptPayStatus("1", "2", "3", Money.ofMinorUnit("THB", 1))).thenReturn(1);
Mockito.when(serviceChannelRoutingEngine.fastStatus("1")).thenReturn(1);
Mockito.when(serviceChannelRoutingEngine.getDFTMoneyConfig()).thenReturn(Money.ofMinorUnit("THB", 1));
Mockito.when(serviceChannelRoutingEngine.getExchangeCode("1", "2")).thenReturn("any");
List<String> serviceChannels = new ArrayList<>();
serviceChannels.add("citi.transfer.001");
Mockito.when(serviceChannelRoutingEngine.getServiceChannels("BankName_test",null )).thenReturn(serviceChannels);
CreateTransferRequest transferRequest = getCreateTransferRequest();
transferRequest.setServiceChannel("");
transferRequest.setTransferTypeEnum(mock(TransferTypeEnum.class));
try{
transferService.createTransfer(transferRequest);
}catch (FundException e){
Assert.assertEquals("BUSINESS_NOT_SUPPORT", String.valueOf(e.getErrorCode()));
}
}
@Test
public void testCreateTransferIfRouteMethod4() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
MockitoAnnotations.initMocks(this);
Transfer transfer = getTransfer();
transfer.setStatus(TransferStatus.CLEARED);
FundResult<BankAccountInfoDTO> mockResult = mock(FundResult.class);
Mockito.doReturn(mockResult).when(pciExternalService).queryBankAccountInfo(any(String.class), any(String.class));
Mockito.when(transferDomainService.create(transfer)).thenReturn(any());
Mockito.when(serviceChannelRoutingEngine.isBkt("test")).thenReturn(false);
Mockito.when(serviceChannelRoutingEngine.promptPayStatus(null, "PAYMENT_WALLET_TMN_001", "BankCaode_test", Money.ofMinorUnit("THB", 1))).thenReturn(0);
Mockito.when(serviceChannelRoutingEngine.fastStatus("1")).thenReturn(1);
Mockito.when(serviceChannelRoutingEngine.getDFTMoneyConfig()).thenReturn(Money.ofMinorUnit("THB", 1));
Mockito.when(serviceChannelRoutingEngine.getExchangeCode("1", "2")).thenReturn("any");
//convert2BankAccountInfo()
CreateTransferRequest transferRequest = getCreateTransferRequest();
String result = transferService.createTransfer(transferRequest);
Assert.assertEquals(result, null);
}
@Test
public void testCreateTransferIfRouteMethod5() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
MockitoAnnotations.initMocks(this);
Transfer transfer = getTransfer();
transfer.setStatus(TransferStatus.CLEARED);
FundResult<BankAccountInfoDTO> mockResult = mock(FundResult.class);
Mockito.doReturn(mockResult).when(pciExternalService).queryBankAccountInfo(any(String.class), any(String.class));
Mockito.when(transferDomainService.create(transfer)).thenReturn(any());
Mockito.when(serviceChannelRoutingEngine.isBkt("test")).thenReturn(false);
Mockito.when(serviceChannelRoutingEngine.promptPayStatus(any(), anyString(), anyString(), any())).thenReturn(-1);
Mockito.when(serviceChannelRoutingEngine.fastStatus("1")).thenReturn(1);
Mockito.when(serviceChannelRoutingEngine.getDFTMoneyConfig()).thenReturn(Money.ofMinorUnit("THB", 0));
Mockito.when(serviceChannelRoutingEngine.getExchangeCode("1", "2")).thenReturn("any");
//convert2BankAccountInfo()
CreateTransferRequest transferRequest = getCreateTransferRequest();
String result = transferService.createTransfer(transferRequest);
Assert.assertEquals(result, null);
}
@Test
public void testCreateTransferIfRouteMethod6() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
MockitoAnnotations.initMocks(this);
Transfer transfer = getTransfer();
transfer.setStatus(TransferStatus.CLEARED);
FundResult<BankAccountInfoDTO> mockResult = mock(FundResult.class);
Mockito.doReturn(mockResult).when(pciExternalService).queryBankAccountInfo(any(String.class), any(String.class));
Mockito.when(transferDomainService.create(transfer)).thenReturn(any());
Mockito.when(serviceChannelRoutingEngine.isBkt("test")).thenReturn(false);
Mockito.when(serviceChannelRoutingEngine.promptPayStatus(any(), anyString(), anyString(), any())).thenReturn(-1);
Mockito.when(serviceChannelRoutingEngine.fastStatus(anyString())).thenReturn(1);
Mockito.when(serviceChannelRoutingEngine.getDFTMoneyConfig()).thenReturn(Money.ofMinorUnit("THB", 2));
Mockito.when(serviceChannelRoutingEngine.getExchangeCode("1", "2")).thenReturn("any");
//convert2BankAccountInfo()
CreateTransferRequest transferRequest = getCreateTransferRequest();
String result = transferService.createTransfer(transferRequest);
Assert.assertEquals(result, null);
}
@Test
public void testCreateTransferIfRouteMethod7() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
MockitoAnnotations.initMocks(this);
Transfer transfer = getTransfer();
transfer.setStatus(TransferStatus.CLEARED);
FundResult<BankAccountInfoDTO> mockResult = mock(FundResult.class);
Mockito.doReturn(mockResult).when(pciExternalService).queryBankAccountInfo(any(String.class), any(String.class));
Mockito.when(transferDomainService.create(transfer)).thenReturn(any());
Mockito.when(serviceChannelRoutingEngine.isBkt("test")).thenReturn(false);
Mockito.when(serviceChannelRoutingEngine.promptPayStatus(any(), anyString(), anyString(), any())).thenReturn(-1);
Mockito.when(serviceChannelRoutingEngine.fastStatus(anyString())).thenReturn(-1);
Mockito.when(serviceChannelRoutingEngine.getDFTMoneyConfig()).thenReturn(Money.ofMinorUnit("THB", 2));
Mockito.when(serviceChannelRoutingEngine.getExchangeCode("1", "2")).thenReturn("any");
//convert2BankAccountInfo()
CreateTransferRequest transferRequest = getCreateTransferRequest();
String result = transferService.createTransfer(transferRequest);
Assert.assertEquals(result, null);
}
如果routeMethod这个方法在逻辑不变且正常工作的前提下进行细化拆分,那么它的测试方法也可以简单很多,而全面测试并持续通过所有测试的系统,才是可测试的系统。在迭进设计的四条规则中,运行所有测试的优先级排在最前,说明程序保持可持续测试的重要性。
总结
书中介绍的规则均来自作者多年的实践经验,涵盖从命名到重构的多个编程方面,虽为一家之言,然诚有可资借鉴的价值。从《代码整洁之道》中可以学到:好代码和糟糕的代码之间的区别:如何编写好代码,如何将糟糕的代码转化为好代码:如何创建好名称、好函数、好对象和好类;如何格式化代码以实现其可读性的最大化:如何在不妨碍代码逻辑的前提下充分实现错误处理;如何进行单元测试和测试驱动开发。从书中收获良多,也意识到需要在今后的开发工作中不断实践不断学习。