一、应用场景
之所以会将策略和模板模式放在一起,是因为这两种模式用的最多最广泛,而且基本都是联合使用的。在开始之前,先复习一下模式的定义:
- 模板模式(Template Pattern)
模板模式是在一个抽象类中定义执行的方法,每个方法中都有一个对应的业务流程模板,它的子类需要按照需要来重写模板流程中的方法,子类只需要对这些基本方法进行实现即可,子类并不需要对模板方法进行实现,这种设计模式也属于行为型模式。
- 策略模式(Strategy Pattern)
策略模式是指一个策略接口被多个策略实现类所实现,通常会创建一个工厂类获取接口实现 bean,具体使用哪一种根据用户选择的类型来和 Map 里的 key 做匹配,策略模式是一种行为性模式。
不管是策略模式还是模板模式,都是运用了 java 多态这一特点,父类引用指向之类对象,通常情况下,模板模式使用的是抽象类,而策略模式使用的是接口而已。其子类都需要子类重写其父类方法或者实现接口方法,两者都满足开闭原则,使得系统在不影响其它功能的前提下更容易拓展。但是两者又有一些差异,模板模式是一种耦合的模式,策略模式是一种松散的模式。模板模式中,通常只有一个业务方法的入口,策略模式中的接口通常会有多个。
单纯的策略模式和模板模式在实践中应用很少,一般都是两种模式结合起来使用,如下图所示,这里即使用了模板模式的高内聚的业务流程,也是用了策略模式的松散性,相同的内容放在抽象类中进行处理,特有的内容放在具体的实现类里面进行操作。
二、应用实践
在介绍了其应用场景后,在这里将结合实际的业务场景来介绍两种设计模式的合并使用。这里采用的是下单支付的场景,用户下单支付成功后,需要邮件和短信通知用户,并且给用户发积分并发送 MQ。
1、PayTypeEnum 枚举类
@Getter
public enum PayTypeEnum {
ALIPAY(1, "支付宝支付"),
WEIXIN(2, "微信支付"),
UNIPAY(3,"银联支付"),
;
PayTypeEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
private Integer code;
private String msg;
public static PayTypeEnum getPayType(Integer code) {
return Arrays.stream(PayTypeEnum.values()).filter(e -> e.getCode().equals(code)).findFirst().orElse(null);
}
}
2、PayDto 请求体
@Data
public class PayDto {
/**
* 订单号
*/
private String orderNo;
/**
* 支付方式 1 支付宝 2 微信 3 银联
*/
private Integer payType;
}
3、BaseBusiness 类
首先,我们需要定义一个接口类,如下图所示,定义了业务流程方法,发送邮件以及发送短信等方法。
public interface BaseBusiness {
/**
* 查询支付方式
*/
PayTypeEnum getCode();
/**
* 处理业务流程
*/
Result<String> handleOrderFlow(PayDto pay);
/**
* 发送邮件
*/
boolean sendEmail(PayDto pay);
/**
* 发送手机短信
*/
boolean sendPhone(PayDto pay);
}
4、AbstractAppBusinessTemplate 类
实现接口的模板抽象类,定义了业务的流程顺序,以及抽象的支付方法。同时也实现了发送短信和邮件的方法,还有一个发送消息的方法。
@Slf4j
public abstract class AbstractAppBusinessTemplate implements BaseBusiness {
/**
* 模板方法:处理业务流程
*/
@Override
public final Result<String> handleOrderFlow(PayDto pay) {
// step1 支付
boolean result = doPay(pay);
if (!result) {
return Result.failed("支付失败!");
}
// step2 发送短信和邮件通知到客户
sendEmail(pay);
sendPhone(pay);
// step3 发送用户积分
grantUserScore(pay);
// step4 发送消息
sendMsgMQ(pay);
return Result.success("处理成功!");
}
// 普通方法
public void sendMsgMQ(PayDto pay){
log.info("send mq {}", JSONObject.toJSONString(pay));
}
public void grantUserScore(PayDto pay){}
// 订单支付
protected abstract boolean doPay(PayDto pay);
@Override
public boolean sendEmail(PayDto pay) {
log.info("send email for order {}", pay.getOrderNo());
return true;
}
@Override
public boolean sendPhone(PayDto pay) {
log.info("send phone for order {}", pay.getOrderNo());
return true;
}
}
5、AliPayAppBusinessStrategy 类
阿里业务类型实现类,这里实现了支付的方法,以及发送短信和邮件的方法,这里不同的业务可能配置不同的短信发送服务,通用的短信发送在抽象模板进行处理,特有的可以在具体的实现类里面实现。
@Slf4j
@Service
public class AlipayAppBusinessStrategy extends AbstractAppBusinessTemplate {
@Override
protected boolean doPay(PayDto pay) {
try {
TimeUnit.MILLISECONDS.sleep(RandomUtil.randomInt(200, 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("支付宝支付业务流程");
return true;
}
@Override
public boolean sendEmail(PayDto pay) {
System.out.println("AlipayAppBusinessStrategyImpl, email,开始运行");
super.sendEmail(pay);
System.out.println("AlipayAppBusinessStrategyImpl, email,运行完成");
return true;
}
@Override
public boolean sendPhone(PayDto pay) {
return super.sendPhone(pay);
}
}
6、UnionPayAppBusinessStrategy 类
@Slf4j
@Service
public class UnionPayAppBusinessStrategy extends AbstractAppBusinessTemplate {
@Override
protected boolean doPay(PayDto pay) {
try {
TimeUnit.MILLISECONDS.sleep(RandomUtil.randomInt(200, 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("银联支付业务流程");
return true;
}
@Override
public boolean sendEmail(PayDto pay) {
return super.sendEmail(pay);
}
@Override
public boolean sendPhone(PayDto pay) {
return super.sendPhone(pay);
}
}
7、WeixinAppBusinessStrategy 类
@Slf4j
@Service
public class WeixinPayAppBusinessStrategy extends AbstractAppBusinessTemplate {
@Override
protected boolean doPay(PayDto pay) {
try {
TimeUnit.MILLISECONDS.sleep(RandomUtil.randomInt(200, 1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
log.info("微信支付业务流程");
return true;
}
@Override
public boolean sendEmail(PayDto pay) {
return super.sendEmail(pay);
}
@Override
public boolean sendPhone(PayDto pay) {
return super.sendPhone(pay);
}
}
8、PayAppBusinessFactory 工厂类
PayAppBusinessFactory 工厂类获取接口实现 bean,并存储到 ConcurrentHashMap,通过枚举获取对应的实现 bean
@Component
@Slf4j
public class PayAppBusinessFactory implements ApplicationContextAware {
public static final ConcurrentHashMap<PayTypeEnum, BaseBusinessService> BASE_BUSINESS_BEAN_MAP = new ConcurrentHashMap<>();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
log.info("PayAppBusinessFactory 启动开始");
Map<String, BaseBusinessService> map = applicationContext.getBeansOfType(BaseBusinessService.class);
map.forEach((key, value) -> BASE_BUSINESS_BEAN_MAP.put(value.getCode(), value));
log.info("PayAppBusinessFactory 启动完成");
}
public static <T extends BaseBusinessService> T getTrafficMode(PayTypeEnum code) {
return (T) BASE_BUSINESS_BEAN_MAP.get(code);
}
}
9、controller
@PostMapping(value = "handle2")
public Result<String> handle2(@RequestBody Pay) {
BaseBusinessService baseBusinessService = PayAppBusinessFactory.getTrafficMode(PayTypeEnum.getPayType(pay.getPayType()));
Result<String> result = baseBusinessService.handleOrderFlow(pay);
log.info("result is {}", JSONObject.toJSONString(result));
return result;
}
}
10、测试接口 localhost:8080/testUtils/handle2
入参:
{
"orderNo": "test1",
"payType": 1
}
返回结果:
{
"code": 200,
"data": "处理成功!",
"message": "操作成功"
}
日志打印:
PayAppBusinessFactory 启动开始
PayAppBusinessFactory 启动完成
支付宝支付业务流程
AlipayAppBusinessStrategyImpl, email,开始运行
send email for order test1
AlipayAppBusinessStrategyImpl, email,运行完成
send phone for order test1
send mq {"orderNo":"test1","payType":1}
result is {"code":200,"data":"处理成功!","message":"操作成功"}
三、过程中的一些思考与总结
1、Java 类实现某个接口后,可以不用实现接口中的所有方法
接口中的方法都是抽象的,实际修饰符是 public abstract ,我们平时都省略了。所以抽象类不需要全部实现接口中的方法(可以根据自己需要去实现某些接口),但是抽象类的子类(抽象类除外)一定需要实现父类没有实现的接口及父类中所有的抽象方法,也可以全部实现父类实现的接口及父类中所有的抽象方法(如果子类想要调用父类的方法,则用 super 关键字进行调用父类的方法)。
这里面我测试了一下,现象是:当 AbstractAppBusinessTemplate 实现了 handleOrderFlow() 方法,它的子类可以不需要实现该 handleOrderFlow() 方法,当然也可以去实现,子类如果实现 handleOrderFlow() 后,可以用 super 关键字去调用父类该 handleOrderFlow() 方法。如果不想被子类实现该接口,则在模板类中该 handleOrderFlow() 方法添加 final 关键字。
2、sendEmail、sendPhone 方法执行顺序
调用接口,以阿里支付为例,先执行 AlipayAppBusinessStrategy 里的 sendEmail 方法,然后 super 调用父类(AbstractAppBusinessTemplate )的 sendEmail 方法,然后再执行 AlipayAppBusinessStrategy 里的 sendEmail 方法里 super 下面的代码。
意图:因为发邮件,每个支付通道的格式以及内容不一样,但是发送是一样的,各自的邮件内容及格式可以写在 AlipayAppBusinessStrategy 里,然后 AbstractAppBusinessTemplate 里的 sendEmail 方法写发送功能代码,因为这部分是一样的。
3、策略类中实现 baseBusiness 接口可写可不写(implements BaseBusiness)
三个策略类中实现 baseBusiness 接口可写可不写,因为继承了父类,父类实现了 baseBusiness 接口,那三个策略类就要全部实现该 baseBusiness 接口中所有的方法。
4、abstract 抽象类特征
- 子类在继承抽象类后,必须实现抽象类中的抽象方法
- 抽象类中可以有抽象的方法,也可以有普通方法
- 如果类中含有抽象的方法,当前类必须为抽象类
5、interface 接口特征
在 java 中,可以使用关键字定义一个接口,一个接口由变量的定义和方法定义两部分组成。
[public] interface 接口名 {
[public] [static] [final] 变量;
[public] [abstract] 方法;
}
实现接口:要让一个类遵循某组特定的接口需要使用implements 关键字
[public] class 类名 implements Interface1,Interface2,...{
//实现所有接口中声明的方法
}
- 实现接口的类,称为实现类
- 实现类必须实现接口中所有的方法
- 如果不实现接口的方法,那么该实现类也要为一个抽象类
四、参考文档
springboot-策略和模板模式的思考与实践