前言 我在之前的文章孔乙己“茴”字四种写法引起我对策略模式实现的思考 中留下了一个悬念,文章中的代码实现出现了较多的重复代码块,这样的问题对于一个对代码质量有较高要求的人是不可容忍的。为啥这么说呢?因为这样的不合格的代码,无论是你还是他人进行维护或者更新新的功能,都必将难以下手,终将成为令众人“敬仰”的祖传代码。
我们先大致回顾一下前文的内容,前文设定了一个抽奖的业务场景,奖品共有现金、优惠券、积分和谢谢参与四类,后续很大可能增加新的奖品类型如赠送抽奖次数,用户抽中则实时将奖励发送给用户。现在呢我们稍稍的改变下业务场景,我们还是回到之前的订单活动业务场景,活动分为多次住宿活动、连住订单活动、首次入住活动、会籍订单活动等等,订单满足活动条件会赠送积分、优惠券、会籍等奖品。由此可见,活动的类型和奖品的类型都是会逐渐的增多,所以针对此场景引出了策略设计模式,同时在编码的过程中发现很多重复的代码块和固定的的流程和逻辑,这个场景符合模板设计模式,于是引出了我们本文的另一主角:模板设计模式。
问题在哪里 首先我们先找出有哪些重复代码块:
IRewardSendStrategy 接口的 isTypeMatch 方法,每个策略的最终实现内容都是一样的,这个是重复代码。
使用实现 InitializingBean 接口的方式组合策略类时,afterPropertiesSet 方法的实现,每个策略类的实现代码也都是重复代码。
判断订单是否符合活动的条件,符合条件则发送奖励,不符合则结束处理。这些逻辑是固定的,具体的判断过程和具体的奖励发放过程是不固定的,我们可以控制判断、发奖励的流程,让具体的判断过程和具体的奖励发放过程让子类去实现。
重复代码块的出现,明显是不符合面向对象OOP的开发原则的,必将对软件的健康带来影响,那接下来我们来看看如何用模板设计模式来解决。
模板设计模式 我们知道可以用模板设计模式来解决重复代码的问题,提高代码利用率的同时,也可以让代码更加的健壮。那什么是模板设计模式?我先介绍一下模板设计模式。
在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行,这种类型的设计模式属于行为型模式。模板模式中涉及到在父类实现算法骨架,具体步骤在子类实现,所以必须要有抽象类(Java8中的接口的 default 方法貌似也可以实现)。
介绍
意图:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
主要解决:一些方法通用,却在每一个子类都重新写了这一方法。
何时使用:有一些通用的方法。
如何解决:将这些通用算法抽象出来。
关键代码:在抽象类实现,其他步骤延迟到子类实现。
应用实例:
JDK中 ReentrantLock中公平锁和非公平锁的实现
Spring 中对 Hibernate 的支持,将一些已经定好的方法封装起来,比如开启事务、获取 Session、关闭 Session 等。
优点:
封装不变部分,扩展可变部分。
提取公共代码,便于维护。
行为由父类控制,子类实现。
缺点:
可能会增加代码的阅读难度。
每一个不同的实现都需要一个子类来实现,导致类的个数增加,使得系统更加庞大。
使用场景:
有多个子类共有的方法,且逻辑相同。
重要的、复杂的方法,可以考虑作为模板方法。
注意事项:
为防止子类重写,一般模板方法都加上 final 关键词。
具体应该怎样做 接下来我就用代码来展现具体的做法,参照之前的代码,进行一些改动,具体代码我都会直接贴在文章中,我建议大家学习时,还是要动手去敲一敲,不要上来就要源码。要知道纸上得来终觉浅,自己还是要亲自去实践一把,才能得到不一样的经验。不要再说了,赶快开始吧!!!
改造好的代码的UML图和机构图如下:
定义奖励、活动类型枚举
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 @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; } }
定义活动、奖励发放策略接口
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 public interface IRewardSendStrategy { String type () ; boolean isTypeMatch (String type) ; void sendReward (Long memberId) ; } public interface IActiveHandleStrategy { String getCategory () ; String getCategoryDetail () ; boolean isTypeMatch (String category) ; boolean checkOrder (ActiveOrderDto temporaryOrderDto, ActiveDto activeDto) ; }
关键的一步就是识别出公共不变的方法、逻辑 ,将公共不变方法、固定的逻辑抽取到抽象父类中。
参照 标题:问题在哪里 提出的问题 ,最终优化的代码如下:
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 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 ); } } @Slf 4j@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()); } 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); } protected final boolean sendReward (ActiveOrderDto temporaryOrderDto, long memberId, ActiveDto activeDto) { AbstractIRewardSendStrategy impl = RewardSendStrategyFactory.getImpl(activeDto.getRewardType()); impl.sendReward(memberId, activeDto); return true ; } }
抽象类中是可以没有抽象方法的,但一个类中如果有抽象方法,那这个类就必须定义成抽象类。
为了更好的提供给第三方调用,创建策略工厂整合策略。
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 @Slf 4j@Component public class RewardSendStrategyFactory { private final static Map<String, AbstractRewardSendStrategy> STRATEGY_MAP = new ConcurrentHashMap<>(16 ); public static void registerStrategy (String type, AbstractRewardSendStrategy strategy) { STRATEGY_MAP.put(type, strategy); } public static AbstractRewardSendStrategy getImpl (String type) { return STRATEGY_MAP.get(type); } } @Slf 4j@Component public class ActiveHandleFactory { @Autowired private List<AbstractActiveHandleStrategy> activeHandleList; public AbstractActiveHandleStrategy getImpl (String categoryDetail) { return activeHandleList.stream().filter(strategy -> strategy.isTypeMatch(categoryDetail)) .findAny() .orElseThrow(() -> new UnsupportedOperationException("没有找到策略实现" )); } }
具体的策略实现
因为策略实现代码比较简单,我这个地方就给出一个优惠券发放和会籍订单活动的策略实现,其他的大家照猫画虎就行了。
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 @Slf 4j@Service ("couponRewardSendStrategyV1" )public class CouponRewardSendStrategy extends AbstractRewardSendStrategy { @Override public String type () { return RewardTypeEnum.COUPON.getCode(); } @Override public void sendReward (Long memberId) { log.info("给[{}]发送优惠券奖品" , memberId); } } @Slf 4j@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 ; } }
写个单元测试,看看具体的效果
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 @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方法上。这样看起来模板设计模式是不是很简单,而且模板和策略两个模式也能很好的结合,最后的效果也不错,其实这两个只要你写稍微复杂一点的代码,都是有他们俩的使用场景的。另外由上文我们就可以看出,设计模式之间其实并不是割裂的,复杂的业务代码实现时,可能会符合多种设计模式。作为程序员我们就要对业务进行抽象,用更多更好并且合理的模式去实现。另外多说一句,大家在学习的时候一定要多多思考,多多动手,千万不要养成眼高手低的习惯,要知道纸上得来终觉浅,绝知此事要躬行的道理。设计模式系列的文章我会持续的更新,下一篇的主题有可能是管道模式,请大家敬请期待吧!哈哈!