利用装饰模式实现多种扣费方式,利于扩展。
背景
项目中有个扣费功能。原本是直接从用户本身扣费,最近新增的需求要求从别的地方扣费。那么就要对原本的扣费流程进行修改,最基本的方式就是直接 if else,但是本次使用装饰模式来实现该功能。
优点在于后期可以扩展更多扣费方式,而且可以简单地调整扣费方式的优先级,同时有利于简化调用方的代码。
实现
装饰模式首先要求有一个接口,这个接口给调用方使用。调用方只需要持有接口的一个实例对象即可。
接口
那么接口定义如下(隐去部分业务相关代码):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
public interface ConsumeHandler {
int consume(User user, int count);
}
|
返回值
项目中扣除的是次数,若有需要扣除其他都是同理。
每一个处理器返回的值代表从当前途径扣除的次数;这样做的好处在于,可以允许同时使用多种方式支付,只要所有资金来源加起来能够超过需要扣除的次数,那么一定能扣除成功,不至于强求一次支付成功。
参数
count代表需要当前处理器扣除的次数。注意是当前,不是总计次数。
举例
假设有处理器A和处理器B,目前要扣除12次,扣除顺序为A->B。
基于装饰模式,应该由B来调用A
那么B接收次数为12次,调用A,告诉A需要扣除12次
A执行逻辑,扣除5次,返回7
B拿到7,判断剩余次数不为0,需要自身扣除7次。
B扣除完成,返回给调用方0
调用方接收到扣除结果0,扣费成功。
抽象类
装饰模式有一些通用逻辑,例如都需要接收一个上级参数。本次中还有一个判断当前是否应该继续扣费的逻辑,因此额外写一个抽象类实现这些逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| public abstract class AbstractConsumeHandler implements ConsumeHandler {
protected ConsumeHandler parent;
public AbstractConsumeHandler(ConsumeHandler parent) { this.parent = parent; }
@Override public int consume(User user, int count) { if (count == 0) return count; int c = count; if (this.parent != null) { c = this.parent.consume(contract, signatory, user, count); if (c == 0) { return 0; } } int myCount = c;
c = consumeCount(contract, signatory, user, myCount);
return myCount - c; }
public abstract int consumeCount( User user, int count);
}
|
实际处理器,只需要继承抽象类,并且直接实现扣费逻辑即可。
注入
项目基于SpringBoot开发,需要一种友好的方式注入实例。
本来想直接通过@Component之类的注解注入处理器实例,但是这样一来不好注入上级实例,不利于调整优先级;二来调用方不方便选择注入实例。
所以还是只能通过配置类手动注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
@Bean public ConsumeHandler consumeHandler(AgentService agentService, UserService userService) {
AgentCountConsumeHandler agentCountConsumeHandler = new AgentCountConsumeHandler(null);
agentCountConsumeHandler.setService(agentService);
UserCountConsumeHandler userCountConsumeHandler = new UserCountConsumeHandler(agentCountConsumeHandler); userCountConsumeHandler.setUserService(userService);
return userCountConsumeHandler; }
|
这种注入方式较为死板,处理器本身依赖的外部组件只能手动set,且失去了一层代理,可能部分切面功能无法使用。
注意事项
如果采用多种途径共同扣费逻辑,需要特别处理事务回滚相关。因为每一个处理器都不能知道最终结果是否会扣费成功,因此自身都会进行数据持久化操作。只有调用方知道是否扣费成功,此时方能进行回滚操作