使用装饰模式实现多种扣费方式

利用装饰模式实现多种扣费方式,利于扩展。

背景

项目中有个扣费功能。原本是直接从用户本身扣费,最近新增的需求要求从别的地方扣费。那么就要对原本的扣费流程进行修改,最基本的方式就是直接 if else,但是本次使用装饰模式来实现该功能。

优点在于后期可以扩展更多扣费方式,而且可以简单地调整扣费方式的优先级,同时有利于简化调用方的代码。

实现

装饰模式首先要求有一个接口,这个接口给调用方使用。调用方只需要持有接口的一个实例对象即可。

接口

那么接口定义如下(隐去部分业务相关代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 负责签署时扣除次数,使用装饰模式
*/
public interface ConsumeHandler {

/**
* 扣除次数
*
* @param user
* @param count 需要扣除的次数
* @return 返回扣除后剩余的次数;
*/
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;
}

/**
* 子类需要重写的方法,用于实际扣除次数;子类不需要考虑次数是否扣除完毕的问题
*
* @param user
* @param count 当前处理器需要扣除的次数
* @return 返回当前处理器扣除的次数
*/
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
    /**
* 注入次数消费处理器
*
* @return
*/
@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,且失去了一层代理,可能部分切面功能无法使用。

注意事项

如果采用多种途径共同扣费逻辑,需要特别处理事务回滚相关。因为每一个处理器都不能知道最终结果是否会扣费成功,因此自身都会进行数据持久化操作。只有调用方知道是否扣费成功,此时方能进行回滚操作


使用装饰模式实现多种扣费方式
http://blog.inkroom.cn/2020/07/01/23AVPCZ.html
作者
inkbox
发布于
2020年7月1日
许可协议