目录
- 面向过程
- 封装计算进`Task`
- 封装计算进`Calculator`
- 代码演进中做了什么
- 学到了什么
在软件设计中,当选择把一个类设计为有状态后,往往意味着不安全、重量级,需要更多的资源来维护,而无状态在很多场景下是一个非常好的选择。
举个例子来探讨下。
我们业务逻辑中B端派发任务,C端接单完成任务,其中有个任务实体
Task
,需要根据业务金额taskMoney
及服务费率serviceFeeRate
收取B端服务费serviceFee
,计算逻辑如下taskMoney * serviceFeeRate = serviceFee
那么该如何进行服务费的CalCulate()
?
这里我们专注考虑下两个设计点
- 实体
Task
是否包含计算的逻辑 - 计算的逻辑怎么设计
ok,尝试演进下代码的设计。
面向过程
可能写出如下的流水代码
,肯定是不可取的。
- 代码耦合,无法拓展,当计算逻辑变化时,可能影响到其他业务逻辑
- 没法对计算逻辑进行单元测试
/*** 派发任务* @param request*/public void distributeTask(Request request) {check(request);。。。//任务实体Task task = generateTask(request);//计算服务费BigDecimal serviceFee = task.getTaskMoney().multiply(task.getServiceFeeRate()).setScale(2, RoundingMode.HALF_UP);task.setServiceFee(serviceFee);。。。。//保存任务save(task);。。。。}
封装计算进Task
第一步尝试将计算服务费
逻辑封装进Task
类中,是否有问题
@Data
public class Task {/*** 任务名称*/private String taskName;/*** 任务Id*/private String taskId;/*** 任务金额*/private BigDecimal taskMoney;/*** 服务费率*/private BigDecimal serviceFeeRate;/*** 服务费*/private BigDecimal serviceFee;/*** 计算服务费*/public void calculateServiceFee() {BigDecimal serviceFee = this.taskMoney.multiply(this.getServiceFeeRate()).setScale(2, RoundingMode.HALF_UP);this.setServiceFee(serviceFee);}
}
这样我们客户端service的调用就会是这样的
//任务实体Task task = generateTask(request);//当然这一步是可以放在generateTask()方法中。//task.calculateServiceFee();
看起来service
的调用是清晰了,但这是有代价的,我们要做的是降低代价,做到平衡。
代价就是Task
类内多了一个职责
,即计算逻辑的维护;
理论上Task
类应当只会在一种情况下会变更
:属性的变更,比如说增加一个属性TaskDescription
。
违反了solid
中的第一原则:单一职责。
封装计算进Calculator
既然service
与实体Task
承担不来他们不该应有的压力:Calculate
,那我们来个压力转移。
定义一个计算器Calculator
,专注于计算服务费。
Task
可以定义为实体类/业务类,一定是有状态的
Calculator
定义为操作类/辅助类,无状态stateless
注意这两者是完全不同的
设计Calculator
有以下几种方案:
1.持有计算所需属性(有状态)
public class Calculator {private BigDecimal taskMoney;private BigDecimal serviceFeeRate;/*** 计算服务费* @return 计算结果*/public BigDecimal calculateServiceFee() {return this.taskMoney.multiply(serviceFeeRate).setScale(2, RoundingMode.HALF_UP);}
}
缺点很明显
1.当
Task
属性变化时,此辅助类也跟着变化
2.线程不安全,多线程当多个
Task
需要计算时,会存在计算的值相互覆盖的场景
2.持有整个计算对象(有状态)
public class Calculator {private Task task;/*** 计算服务费* @return 计算结果*/public BigDecimal calculateServiceFee() {return task.getServiceFee().multiply(task.getServiceFeeRate()).setScale(2, RoundingMode.HALF_UP);}
}
这种方案控制了Task变化时影响的Calculator 本身属性的变化,但是仍然存在线程安全的问题
3.不持有对象无状态设计
public class Calculator {/*** 计算服务费* @return 计算结果*/public BigDecimal calculateServiceFee(Task task) {return task.getServiceFee().multiply(task.getServiceFeeRate()).setScale(2, RoundingMode.HALF_UP);}
}
这里的无状态设计我们保证了线程安全,又保证了单一职责,算是比较完善的情况。
4.面向接口的可拓展的无状态设计
对于上述的设计,我们可以通过继承来将具体的行为封装到子类中去。
4.1定义一个接口
public interface ICalculator<T> {BigDecimal calcualate(T t);
}
4.2 具体的计算服务类
比如说回到我们最初的需求,需要一个计算服务费的类,使用ServiceFeeCalculator
实现ICalculator
。
如果需要新增其他计算类,比如说计算薪水的类,再增加一个具体的SalaryCalculator
即可。
完全做到了和业务逻辑的分离,使ICalculator
是一个可拓展的无状态类.
/*** @author hailang.zhang* @since 2023-11-21*/
public class ServiceFeeCalculator implements ICalculator<Task{@Overridepublic BigDecimal calcualate(Task task) {return task.getServiceFee().multiply(task.getServiceFeeRate()).setScale(2, RoundingMode.HALF_UP);;}
}
4.3 简单工厂模式
甚至更近一步,根据开闭原则
,我们将获取ICalculator的逻辑委托给工厂,
让service
进一步做到计算逻辑的无感,甚至无需自己对计算方法进行选择。
public class CalculatorFactory {public static ICalculator calculate(String type) {if (type.equals("sercviceFee")) {return new ServiceFeeCalculator();} else if (type.equals("salalry")) {return new SalaryCalculator();} else if ()......省略}
}
当然上述的工厂模式可以进一步的优化,有兴趣的可以看我之前的一篇文章
使用工厂、模板、策略模式重构代码
最终在service使用的时候效果
public void distributeTask(Request request) {。。。//任务实体Task task = generateTask(request);//计算服务费(此处可以将简单工厂模式进一步的优化)**BigDecimal serviceFee = CalculatorFactory.calculate("serviceFee").calcualate(task);**。。。。//保存任务save(task);。。。。}
代码演进中做了什么
- 从功能层面区分有状态和无状态的类
Task/Calculator
- 设计无状态的类
Calculator
- 进一步抽象出接口
ICalculator
,面向接口 - 进一步解耦
CalculatorFactory
,封装算法
学到了什么
无状态的类yyds,但要明确的是有状态的业务类是不可避免的,比如Task
,但我们可以将部分辅助功能拆分出来,比如**Calculator
。尽量去保证业务类的单一性,而且无状态的设计会非常轻量级,无需去维护线程安全。
对于Calculator
(计算器)辅助类的命名尽量精确,可以体现其本身的功能,可以出现类似Importor
(导入器)、Register
(注册器),如果出现Handler
,Processor
,我认为过于模糊,还是有待商榷的。