实际案例进行代码设计演进:无状态的类

在软件设计中,当选择把一个类设计为有状态后,往往意味着不安全、重量级,需要更多的资源来维护,而无状态在很多场景下是一个非常好的选择。

举个例子来探讨下。

我们业务逻辑中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{

    @Override
    public 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(注册器),如果出现HandlerProcessor,我认为过于模糊,还是有待商榷的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值