写好业务代码的经典案例
我对写好业务代码的一些个人思考
我对于一些生硬的名词介绍如何写好业务代码觉得非常难懂,想要写好业务代码我觉得需要结合一些实际案例来理解到底应该如何写好业务代码
我在掘金、csdn…浏览过一些文章,也总结了一些个人思考
1.可读性问题
同方法下代码语义层级不一致导致的可读性变差
-
反例
下面的代码示例中,方法名是保存还款数据的,主要有3个步骤:
第一个步骤是插入业务防重表
第二个步骤是批量更新还款明细
第三个步骤是插入异步任务
通过代码我们可以明显的发现第一步和第二三步是不在同一个语义层级上的,第一步把处理的详细细节暴露在了这一层,导致这一层的可读性下降了
java复制代码public void saveRepaymentData(){
//S1 :插入防重表
String uuid =BizUniqueUtils.getUUID(BusinessType.REPAYMENT.getValue(), reqVo.getRepaymentNo()+"_"+ reqVo.getPin());
BizUnique bizUnique =newBizUnique();
bizUnique.setUuid(uuid);
bizUnique.setPin(reqVo.getPin());
bizUnique.setBusinessId(reqVo.getRepaymentNo());//业务ID 还款单号
bizUnique.setBusinessType(BusinessType.REPAYMENT.getValue());
bizUnique.setCreateDate(newDate());
bizUniqueDao.insert(bizUnique);
//S2: 批量更新还款单明细
repaymentDetailService.batchUpdateRepayDetail(repayDetails);
//S3: 写分期单实收任务
saveSyncTaskData(repaymentMsgs,TaskTypeForJT.REPAYMENT_SUCCESS.getValue(), repaymentVo);
...}
-
正例
正确的做法应该是将细节的处理单独封装在一个方法里面,保证这个方法里面的三个步骤处在同一个语义层级里面
java复制代码public void saveRepaymentData(){
//S1 :插入防重表
insertBizUnique(reqVo.getPin(),reqVo.getRepaymentNo());
//S2: 批量更新还款单明细
repaymentDetailService.batchUpdateRepayDetail(repayDetails);
//S3: 写分期单实收任务
saveSyncTaskData(repaymentMsgs,TaskTypeForJT.REPAYMENT_SUCCESS.getValue(), repaymentVo);
...
}
private insertBizUnique(String pin,String repaymentNo){
String uuid =BizUniqueUtils.getUUID(BusinessType.REPAYMENT.getValue(), repaymentNo +"_"+ pin);
BizUnique bizUnique =newBizUnique();
bizUnique.setUuid(uuid);
bizUnique.setPin(pin);
bizUnique.setBusinessId(repaymentNo);//业务ID 还款单号
bizUnique.setBusinessType(BusinessType.REPAYMENT.getValue());
bizUnique.setCreateDate(newDate());
bizUniqueDao.insert(bizUnique);
}
2.单一职责问题
单一职责适用于业务领域的划分、应用职责的划分、类功能的划分以及方法级别的功能划分等场景。
职责不单一导致功能不够内聚,可读性变差,不能复用等问题
-
反例
下面的对外接口类的名称是RepaymentResource,主要功能应该是处理还款相关的功能。其中的第1个方法 planRepayment 是处理还款冲销的逻辑,第2个
方法 updateRepayAccountAsset 是接收到还款成功异步消息后恢复账户额度的功能,第2个方法虽然是还款成功触发的,但是很明显它的功能和还款没有直
接关系,因为这个方法主要处理的是账户相关业务。
java复制代码public interface RepaymentResource{
/**
* 还款冲销业务逻辑
*/
PlanRepaymentResVo planRepayment(RepaymentReqVo planRepaymentReqVo);
/**
* 还款成功后恢复额度
*/
BaseResponseVo updateRepayAccountAsset(RepaymentSuccessMsg repaymentSuccessMsg);
...
}
-
正例
将第二个方法放到和账户相关的接口类中
java复制代码public interface RepaymentResource{
/**
* 还款冲销业务逻辑
*/
PlanRepaymentResVo planRepayment(RepaymentReqVo planRepaymentReqVo);
...
}
public interface AccountResource{
/**
* 还款成功后恢复额度
*/
BaseResponseVo updateRepayAccountAsset(RepaymentSuccessMsg repaymentSuccessMsg);
...
}
3.重复代码的问题
重复代码问题可能出现在不同系统间的重复代码,或者同一个系统内不同模块间的重复代码,或者同一个类中的不同方法之间的重复代码。重复代码的例子很多,
这里就不单独举例了。重复代码会导致维护成本高,代码逻辑不一致,漏改代码等稳定性问题。
通常来说出现了重复代码时,需要对比一下差异,看是否能将差异的代码进行抽象,通过抽象将重复的代码逻辑进行统一。
4.命名不规范
命名问题看似是最简单的问题,实际上也是在代码中出现最多的问题,命名不规范会导致代码可读性变差。
常见的问题有:命名太过笼统不能准确说明类或者方法的功能;命名和实际功能不匹配,举例如下
-
反例
这段代码中的checkPassword方法,字面上的含义是校验用户的密码,不会有任何的副作用。
但是这段代码在密码不合法的情况下做了Session的初始化动作(Session.initialize()😉。
导致调用方可能在不知情的情况下无意间对Session进行了初始化。
java复制代码public class UserValidator{
private Cryptographer cryptographer;
public boolean checkPassword(String userName,String password){
User user =UserGateway.findByName(userName);
if(user !=User.NULL){
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if("Valid Password".equals(phrase)){
Session.initialize(); // 和方法名不一致的行为
returntrue;
}
}
returnfalse;
}
}
-
正例
为了保证方法名和实际的功能一致,可以将 Session初始化的功能(Session.initialize(); ) 从函数中移除。
或则将函数的名称改为 checkPasswordAndInitializeSession(不过这违反了单一职责的原则)。
因为这里即做了密码校验,又做了session的初始化,但是单一原则并不是不可打破的
java复制代码public class UserValidator{
private Cryptographer cryptographer;
public boolean checkPasswordAndInitializeSession(String userName,String password){
User user =UserGateway.findByName(userName);
if(user !=User.NULL){
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if("Valid Password".equals(phrase)){
Session.initialize(); // 和方法名不一致的行为
returntrue;
}
}
returnfalse;
}
}
5.缺乏模型抽象,无边界控制
- 什么是缺乏模型抽象?
所有的对象都是只负责存放数据的Java对象。这导致的最大问题就是任何代码都可以无所顾忌的去修改对象的任何属性,没有任何边界控制
这个对象在调用过程中还伴随着各种beanCopy动作,最终导致对象的属性变更轨迹完全无法跟踪。其他人在阅读代码的时候会耗费大量的时间来分析。
-
反例
下面的代码中的S1步骤对入参的request对象进行了更新,然后在S2中可能又对request对象进行了变更,然后在S3中又进行了beanCopy的动作。
经过这一系列的操作,很难去排查和定位request的amount属性到底变成什么值了。这样的不光可读性差,而且非常难维护,而且很容易出现问题。
java复制代码@Data
public class Request{
privateString pin;
privateBigdecimal amount;
}
public class Service1{
privateService2 service2;
public boolean doBizProcess(Request req){
req.setAmount(100); //S1 : 随意设置request的属性值
service2.doBizProcess2(req);//S2 :这个方法里可能会把request值设置为 200
BeanUtils.copyProperties(request1, req);//S3: 这里又从另外一个对象copy过来属性
returnfalse;
}
}
-
正例
按照业务进行建模,一般来说请求对象应该被建模为一个值对象(Value Object)。
值对象的属性是不会变更的,这个可以通过去掉值类的setter方法来实现。
如果调用Service2的方法也需要一个Request对象,这时需要重新创建一个新的值对象。
另外,在业务逻辑处理的代码中要禁止beancopy的使用。
java复制代码@Getter
@Builder
public class RequestVo{
privateString pin;
privateBigdecimal amount;
}
public class Service1{
privateService2 service2;
public boolean doBizProcess(Request req){
RequestVo r2 =Request.builder().pin(req.getPin()).amount(100).build(); //S1 : 重新构建一个值对象
service2.doBizProcess2(r2 );//S2 :由于没有setters,这个方法里已经不能随意变更r2的属性
BeanUtils.copyProperties(request1, req);//S3: 禁止beancopy操作
returnfalse;
}
}