前言
我在之前的文章孔乙己“茴”字四种写法引起我对策略模式实现的思考中留下了一个悬念,文章中的代码实现出现了较多的重复代码块,这样的问题对于一个对代码质量有较高要求的人是不可容忍的。为啥这么说呢?因为这样的不合格的代码,无论是你还是他人进行维护或者更新新的功能,都必将难以下手,终将成为令众人“敬仰”的祖传代码。
我们先大致回顾一下前文的内容,前文设定了一个抽奖的业务场景,奖品共有现金、优惠券、积分和谢谢参与四类,后续很大可能增加新的奖品类型如赠送抽奖次数,用户抽中则实时将奖励发送给用户。现在呢我们稍稍的改变下业务场景,我们还是回到之前的订单活动业务场景,活动分为多次住宿活动、连住订单活动、首次入住活动、会籍订单活动等等,订单满足活动条件会赠送积分、优惠券、会籍等奖品。由此可见,活动的类型和奖品的类型都是会逐渐的增多,所以针对此场景引出了策略设计模式,同时在编码的过程中发现很多重复的代码块和固定的的流程和逻辑,这个场景符合模板设计模式,于是引出了我们本文的另一主角:模板设计模式。
问题在哪里
首先我们先找出有哪些重复代码块:
- IRewardSendStrategy 接口的 isTypeMatch 方法,每个策略的最终实现内容都是一样的,这个是重复代码。
- 使用实现 InitializingBean 接口的方式组合策略类时,afterPropertiesSet 方法的实现,每个策略类的实现代码也都是重复代码。
- 判断订单是否符合活动的条件,符合条件则发送奖励,不符合则结束处理。这些逻辑是固定的,具体的判断过程和具体的奖励发放过程是不固定的,我们可以控制判断、发奖励的流程,让具体的判断过程和具体的奖励发放过程让子类去实现。
重复代码块的出现,明显是不符合面向对象OOP的开发原则的,必将对软件的健康带来影响,那接下来我们来看看如何用模板设计模式来解决。
模板设计模式
我们知道可以用模板设计模式来解决重复代码的问题,提高代码利用率的同时,也可以让代码更加的健壮。那什么是模板设计模式?我先介绍一下模板设计模式。
在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行,这种类型的设计模式属于行为型模式。模板模式中涉及到在父类实现算法骨架,具体步骤在子类实现,所以必须要有抽象类(Java8中的接口的 default 方法貌似也可以实现)。
介绍
- 意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
- 主要解决:一些方法通用,却在每一个子类都重新写了这一方法。
- 何时使用:有一些通用的方法。
- 如何解决:将这些通用算法抽象出来。
- 关键代码:在抽象类实现,其他步骤延迟到子类实现。
应用实例:
- JDK中 ReentrantLock中公平锁和非公平锁的实现
- Spring 中对 Hibernate 的支持,将一些已经定好的方法封装起来,比如开启事务、获取 Session、关闭 Session 等。
优点:
- 封装不变部分,扩展可变部分。
- 提取公共代码,便于维护。
- 行为由父类控制,子类实现。
缺点:
- 可能会增加代码的阅读难度。
- 每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
使用场景:
- 有多个子类共有的方法,且逻辑相同。
- 重要的、复杂的方法,可以考虑作为模板方法。
注意事项:
为防止子类重写,一般模板方法都加上 final 关键词。
具体应该怎样做
接下来我就用代码来展现具体的做法,参照之前的代码,进行一些改动,具体代码我都会直接贴在文章中,我建议大家学习时,还是要动手去敲一敲,不要上来就要源码。要知道纸上得来终觉浅,自己还是要亲自去实践一把,才能得到不一样的经验。接下来我就用代码来展现具体的做法,参照之前的代码,进行一些改动,具体代码我都会直接贴在文章中,我建议大家学习时,还是要动手去敲一敲,不要上来就要源码。要知道纸上得来终觉浅,自己还是要亲自去实践一把,才能得到不一样的经验。不要再说了,赶快开始吧!!!
改造好的代码的UML图和机构图如下:
- 定义奖励、活动类型枚举
@Getter
public enum RewardTypeEnum {
/**
* 现金奖励
*/
CASH("1","现金"),
/**
* 积分
*/
POINT("2","积分"),
/**
* 优惠券
*/
COUPON("3","优惠券"),
/**
* 谢谢参与
*/
THANK_YOU("4","谢谢参与"),
;
private String code;
private String desc;
RewardTypeEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
}
@Getter
public enum ActiveTypeEnum {
/**
* 酒店订单
*/
HOTEL_ORDER("1","酒店订单"),
/**
* 会籍订单
*/
LEVEL_ORDER("2","会籍订单"),
;
private String code;
private String desc;
ActiveTypeEnum(String code, String desc) {
this.code = code;
this.desc = desc;
}
}
复制代码
- 定义活动、奖励发放策略接口
/**
* 奖励发送的策略接口
*
* @author Sam
* @date 2021/5/16
* @since 1.0.0
*/
public interface IRewardSendStrategy {
/**
* 发放奖励的类型,通过这个方法来标示不同的策略
* @return
*/
String type();
/**
* 是否匹配
* @param type 奖励类型
* @return
*/
boolean isTypeMatch(String type);
/**
* 发送奖励
* @param memberId 会员id
*/
void sendReward(Long memberId);
}
/**
* 订单活动逻辑判断的策略接口
*
* @author Sam
* @date 2020/11/26
* @since 1.7.3
*/
public interface IActiveHandleStrategy {
/**
* 返回活动的类型
* ActiveCategoryEnum 枚举
*
* @return
*/
String getCategory();
/**
* 返回活动的详细类型
* ActiveCategoryDetailEnum 枚举
*
* @return
*/
String getCategoryDetail();
/**
* 是否匹配
*
* @param category 活动类型
* @return
*/
boolean isTypeMatch(String category);
/**
* 订单检查
*
* @param temporaryOrderDto 临时订单
* @param activeDto 活动dto
* @return Result
*/
boolean checkOrder(ActiveOrderDto temporaryOrderDto, ActiveDto activeDto);
}
复制代码
- 关键的一步就是识别出公共不变的方法、逻辑,将公共不变方法、固定的逻辑抽取到抽象父类中。
参照 标题:问题在哪里 提出的问题 ,最终优化的代码如下:
/**
* 奖励发送的策略抽象类,将 isTypeMatch 方法和实现 InitializingBean 提升到抽象类中,达到代码复用的目的
*
* @author Sam
* @date 2020/11/26
* @since 1.7.3
*/
public abstract class AbstractRewardSendStrategy implements InitializingBean, RewardSendStrategy {
@Override
public final boolean isTypeMatch(String type) {
return Objects.equals(type, this.type());
}
@Override
public final void afterPropertiesSet() throws Exception {
RewardSendStrategyFactory.registerStrategy(this.type(), this);
}
}
/**
* 活动抽象类,抽取公共方法,
* 把订单是否符合奖励的判断之后发送奖励的公共逻辑在此处实现,
* 订单具体条件的判断延迟由子类去实现.
* 策略和模板模式组合使用
*
* @author Sam
* @date 2020/11/26
* @since 1.7.3
*/
@Slf4j
@Component
public abstract class AbstractActiveHandleStrategy implements IActiveHandleStrategy {
/**
* 其他抽象方法
*/
public abstract void otherMethod();
public final void otherMethod1() {
System.out.println("其他公用方法");
}
@Override
public final boolean isTypeMatch(String categoryDetail) {
return Objects.equals(categoryDetail, this.getCategoryDetail());
}
/**
* 外部真正要调用的方法
*
* @param temporaryOrderDto 订单
*/
public final boolean handle(ActiveOrderDto temporaryOrderDto, ActiveDto activeDto) {
// 调用接口中需要子类实现的方法
boolean result = checkOrder(temporaryOrderDto, activeDto);
if (!result) {
log.error("订单 {} 不符合活动 {} 的奖励发放条件", temporaryOrderDto.getOrderNo(), activeDto.getId());
return false;
}
return sendReward(temporaryOrderDto, temporaryOrderDto.getMemberId(), activeDto);
}
/**
* 统一的发送奖励的方法
*
* @param temporaryOrderDto 订单
* @param memberId 用户ID
* @param activeDto 活动
*/
protected final boolean sendReward(ActiveOrderDto temporaryOrderDto, long memberId, ActiveDto activeDto) {
AbstractIRewardSendStrategy impl = RewardSendStrategyFactory.getImpl(activeDto.getRewardType());
impl.sendReward(memberId, activeDto);
return true;
}
}
复制代码
抽象类中是可以没有抽象方法的,但一个类中如果有抽象方法,那这个类就必须定义成抽象类。
- 为了更好的提供给第三方调用,创建策略工厂整合策略。
@Slf4j
@Component
public class RewardSendStrategyFactory {
/**
* 保存策略集合
*/
private final static Map<String, AbstractRewardSendStrategy> STRATEGY_MAP = new ConcurrentHashMap<>(16);
/**
* 添加策略实例
*
* @param type
* @param strategy
*/
public static void registerStrategy(String type, AbstractRewardSendStrategy strategy) {
STRATEGY_MAP.put(type, strategy);
}
/**
* 获取策略实例
*
* @param type
* @return
*/
public static AbstractRewardSendStrategy getImpl(String type) {
return STRATEGY_MAP.get(type);
}
}
/**
* 负责所有活动处理的入口,根据 getImpl(String categoryDetail)类型来判断调用具体的活动策略
*
* @author Sam
* @date 2020/7/13
* @since 1.6.8
*/
@Slf4j
@Component
public class ActiveHandleFactory {
@Autowired
private List<AbstractActiveHandleStrategy> activeHandleList;
/**
* 对外的统一入口
*
* @param categoryDetail 类型
* @return
*/
public AbstractActiveHandleStrategy getImpl(String categoryDetail) {
return activeHandleList.stream().filter(strategy -> strategy.isTypeMatch(categoryDetail))
.findAny()
.orElseThrow(() -> new UnsupportedOperationException("没有找到策略实现"));
}
}
复制代码
- 具体的策略实现
因为策略实现代码比较简单,我这个地方就给出一个优惠券发放和会籍订单活动的策略实现,其他的大家照猫画虎就行了。
@Slf4j
@Service("couponRewardSendStrategyV1")
public class CouponRewardSendStrategy extends AbstractRewardSendStrategy {
@Override
public String type() {
return RewardTypeEnum.COUPON.getCode();
}
@Override
public void sendReward(Long memberId) {
log.info("给[{}]发送优惠券奖品", memberId);
}
}
/**
* 会籍订单的处理
*
* @author Sam
* @date 2020/11/26
* @since 1.7.3
*/
@Slf4j
@Service
public class LevelOrderActiveHandleStrategy extends AbstractActiveHandleStrategy {
@Override
public void otherMethod() {
log.info("会籍订单的实现");
}
@Override
public String getCategory() {
return ActiveTypeEnum.LEVEL_ORDER.getCode();
}
@Override
public String getCategoryDetail() {
return ActiveTypeEnum.LEVEL_ORDER.getCode();
}
@Override
public boolean checkOrder(ActiveOrderDto temporaryOrderDto, ActiveDto activeDto) {
log.info("判断订单 {} 的属性是否符合活动 {} 的条件", temporaryOrderDto, activeDto);
Random random = new Random();
int i = random.nextInt(4);
if (i >= 2) {
return false;
}
return true;
}
}
复制代码
- 写个单元测试,看看具体的效果
@ContextConfiguration(locations = {"classpath:spring/spring-dao.xml", "classpath:spring/spring-service.xml"})
@RunWith(value = SpringJUnit4ClassRunner.class)
public class RewardSendStrategyFactoryTest {
@Autowired
ActiveHandleFactory activeHandleFactory;
@Test
public void test() {
ActiveDto activeDto = new ActiveDto();
activeDto.setId(101L);
activeDto.setCategory(ActiveTypeEnum.HOTEL_ORDER.getCode());
activeDto.setCategoryDetail(ActiveTypeEnum.HOTEL_ORDER.getCode());
activeDto.setRewardType(RewardTypeEnum.COUPON.getCode());
activeDto.setReward("No213215632" + RewardTypeEnum.COUPON.getDesc());
ActiveOrderDto activeOrderDto = new ActiveOrderDto();
activeOrderDto.setMemberId(11L);
activeOrderDto.setOrderType("1");
activeOrderDto.setOrderNo("202105111");
AbstractActiveHandleStrategy impl = activeHandleFactory.getImpl(activeDto.getCategoryDetail());
impl.handle(activeOrderDto, activeDto);
}
}
复制代码
单元测试的输出结果如下:
总结
这样我们的代码就优化完了,AbstractActiveHandleStrategy.handle() 方法中逻辑是固定不变的,这样就确定好了代码的逻辑骨架,后续有新的订单活动或者新的奖励类型,这个地方的代码都不需要改动,只需要增加对应接口的子类就行,符合开闭原则。大家看我代码写了很多,其实关键地方就在那个handle方法上。这样看起来模板设计模式是不是很简单,而且模板和策略两个模式也能很好的结合,最后的效果也不错,其实这两个只要你写稍微复杂一点的代码,都是有他们俩的使用场景的。另外由上文我们就可以看出,设计模式之间其实并不是割裂的,复杂的业务代码实现时,可能会符合多种设计模式。作为程序员我们就要对业务进行抽象,用更多更好并且合理的模式去实现。另外多说一句,大家在学习的时候一定要多多思考,多多动手,千万不要养成眼高手低的习惯,要知道纸上得来终觉浅,绝知此事要躬行的道理。设计模式系列的文章我会持续的更新,下一篇的主题有可能是管道模式,请大家敬请期待吧!哈哈!
文章首发于个人博客 Sam的个人博客,禁止未经授权转载,违者依法追究相关法律责任!