一般用于业务开发,不容易有大量使用设计模式的场景。这里总结一下业务开发中比较常用的设计模式。语言当然是Java,基于Spring框架。1策略模式(StrategyPattern)一个类的行为或方法可以在运行时根据不同的条件执行。举个简单的例子:上班可以骑共享单车,也可以选择公交,也可以选择地铁。这里的交通工具是针对上班行为的策略(解决方案)。策略模式一般有三个角色:Context:策略的上下文执行环境Strategy:策略的抽象ConcreteStrategy:策略的具体实现出现的场景其实有很多种。比如之前商城遇到的登录(手机号、微信、QQ等)和优惠券(满折券、现金券、折扣券等)。这里主要说说最近遇到的两个。一是事先知道采取何种策略,二是需要动态计算来确定采取何种策略。1.1静态(参数)策略在构建增长系统时,用户需要根据不同的来源遵循不同的处理逻辑。然而,当数据出现时,可以识别这个来源。SyncContext/***同步上下文**/@Data@BuilderpublicclassSyncContext{//任务IDprivateLongtaskId;//任务类型一:自然注册;2:团购用户;3:LandingpagereservationprivateIntegertaskType;//所有剩余资金的相关信息(忽略细节)privateObjectreqVO;//存储执行策略名称(伪装执行结果)privateStringrespVO;}SyncStrategy/***同步策略**/publicinterfaceSyncStrategy{/***具体策略*@paramctxContext*/voidprocess(SyncContextctx);}OtSyncStrategy/***自然注册**/@Slf4j@ServicepublicclassOtSyncStrategyimplementsSyncStrategy,BeanNameAware{privateStringbeanName;@Overridepublicvoidprocess(SyncContextctx){log.info([自然登记]{}",ctx);ctx.setRespVO(beanName);}@OverridepublicvoidsetBeanName(Strings){beanName=s;}}AbSyncStrategy/***团购用户**/@Slf4j@ServicepublicclassAbSyncStrategyimplementsSyncStrategy,BeanNameAware{privateStringbeanName;@Overridepublicvoidprocess(SyncContextctx){log.info("[团购用户]{}",ctx);ctx.setRespVO(beanName);}@OverridepublicvoidsetBeanName(Strings){beanName=s;}}DefaultSyncStrategy/***登录页面注册(默认)**/@Slf4j@ServicepublicclassDefaultSyncStrategyimplementsSyncStrategy,BeanNameAware{privateStringbeanName;@Overridepublicvoidprocess(SyncContextctx){log.info([登陆页面注册]{}",ctx);ctx.setRespVO(beanName);}@OverridepublicvoidsetBeanName(Strings){beanName=s;}}至此,策略模式的三个角色就组装好了,不过貌似还是有些问题。SyncContext中有一个taskType,但是应该怎么和具体的策略匹配呢?我们可以使用Spring框架的依赖注入管理策略。SyncStrategy/***同步策略**/publicinterfaceSyncStrategy{StringOT_STRATEGY="otStrategy";StringAB_STRATEGY="abStrategy";StringDEFAULT_STRATEGY="defaultStrategy";/***具体策略*@paramctxContext*/voidprocess(SyncContextctx);}同时修改具体策略,指定@Service别名。只需修改三个具体的策略类即可。OtSyncStrategy/***自然注册**/@Slf4j@Service(SyncStrategy.OT_STRATEGY)publicclassOtSyncStrategyimplementsSyncStrategy,BeanNameAware{privateStringbeanName;@Overridepublicvoidprocess(SyncContextctx){log.info([自然登记]{}",ctx);ctx.setRespVO(beanName);}@OverridepublicvoidsetBeanName(Strings){beanName=s;}}这个时候我们好像需要一个集成调用的类,不然就得把所有的策略都暴露了。一个简单的工厂就可以了。SyncStrategyFactory/***同步策略工厂类接口**/publicinterfaceSyncStrategyFactory{MapSTRATEGY_MAP=Map.of(1,SyncStrategy.OT_STRATEGY,2,SyncStrategy.AB_STRATEGY,3,SyncStrategy.DEFAULT_STRATEGY);/***根据任务类型获取具体策略**@paramtaskType任务类型*@return具体策略*/SyncStrategygetStrategy(IntegertaskType);/***执行策略//XXX:其实这一块在这里职责单一,同时也不符合Factory的初衷。**@paramctx策略上下文*/voidexec(SyncContextctx);}SyncStrategyFactoryImpl/***策略工厂实现**/@Slf4j@Service@RequiredArgsConstructorpublicclassSyncStrategyFactoryImplimplementsSyncStrategyFactory{//这个可以通过SpringBean别名private注入finalMap策略映射;@OverridepublicSyncStrategygetStrategy(IntegertaskType){if(!STRATEGY_MAP.containsKey(taskType)||!strategyMap.containsKey(STRATEGY_MAP.get(taskType))){返回空;}返回strategyMap.get(STRATEGY_MAP.get(taskType));}@Overridepublicvoidexec(SyncContextctx){Optional.of(getStrategy(ctx.getTaskType())).ifPresent(strategy->{log.info([StrategyExecution]查找策略{},ctx=>{}",strategy.getClass().getSimpleName(),ctx);strategy.process(ctx);log.info("[策略执行]执行完成ctx=>{}",ctx);});}}至此,在Spring环境下注入SyncStrategyF就很方便了演员打电话最后补上单测/***策略单测**/@Slf4j@SpringBootTestclassSyncStrategyFactoryTest{@AutowiredSyncStrategyFactorystrategyFactory;@TestvoidtestOtStrategy(){finalSyncContextctx=SyncContext.builder().taskType(1).build();strategyFactory.exec(ctx);Assertions.assertEquals("otStrategy",ctx.getRespVO());}@TestvoidtestAbStrategy(){finalSyncContextctx=SyncContext.builder().taskType(2).build();strategyFactory.exec(ctx);Assertions.assertEquals("abStrategy",ctx.getRespVO());}@TestvoidtestDefaultStrategy(){finalSyncContextctx=SyncContext.builder().taskType(3).build();strategyFactory.exec(ctx);Assertions.assertEquals("defaultStrategy",ctx.getRespVO());}@TestvoidtestOtherStrategy(){finalSyncContextctx=SyncContext.builder().taskType(-1).build();strategyFactory.exec(ctx);Assertions.assertNull(ctx.getRespVO());}}1.2动态(参数)策略其实在上面的策略模式中,taskType也可以作为元数据过程放在具体的策略中。选择特定策略时,遍历所有策略实施类,当任务类型与当前参数匹配时,将终止遍历,并将通过当前策略类处理。在上面提到的登陆页面注册中,在和CRM同步数据的时候,有很多数据需要校验。因为不同地区的着陆页参数不一样,有一些历史着陆页。其实这个方法可以加在策略类中,比如booleanmatch(StrategyContextctx)。具体见代码LayoutContext/***LayoutContext**/@Data@BuilderpublicclassLayoutContext{//登陆页面版本(LandingPageVersion)//国家和地区privateStringcountry;//通道号privateStringchannel;//最终处理结果得到布局IDprivateStringlayoutId;}LayoutStrategy/***布局处理策略**/publicinterfaceLayoutStrategy{/***检查是否匹配策略**@paramctxstrategycontext*@returnbool*/booleanmatch(LayoutContextctx);/***具体策略处理**@paramctx策略上下文*/voidprocess(LayoutContextctx);}具体布局处理策略/***子布局**/@Slf4j@Order(10)@ServicepublicclassLayoutChildStrategy实现LayoutStrategy{//儿童专用频道号(最高优先级)privatestaticfinalStringCHILD_CHANNEL="FE-XX-XX-XX";@Overridepublicbooleanmatch(LayoutContextctx){返回对象。nonNull(ctx)&&CHILD_CHANNEL.equals(ctx.getChannel());}@Overridepublicvoidprocess(LayoutContextctx){log.info("[childrenLayout]Startprocessing");ctx.setLayoutId("111");}}/***根据LPV判断策略*/@Slf4j@Order(20)@ServicepublicclassLayoutLpvStrategyimplementsLayoutStrategy{//需要经过LPVprocessing逻辑通道号privatestaticfinalSetLPV_CHANNELS=Set.of("LP-XX-XX-01","LP-XX-XX-02","XZ-XX-XX-01","XZ-XX-XX-02");@Overridepublicbooleanmatch(LayoutContextctx){returnObjects.nonNull(ctx)&&Objects.nonNull(ctx.getChannel())&&LPV_CHANNELS.contains(ctx.getChannel());}@Overridepublicvoidprocess(LayoutContextctx){log.info("[LPVlayout]startprocessing");ctx.setLayoutId("222");}}/***默认处理策略*/@Slf4j@Order(999)@ServicepublicclassLayoutDefaultStrategyimplementsLayoutStrategy{@Overridepublicbooleanmatch(LayoutContextctx){//自底向上策略returntrue;}@Overridepublicvoidprocess(LayoutContextctx){log.info("[默认布局]开始处理");ctx.setLayoutId("999");}}最后是工厂类:/***布局处理工厂**/publicinterfaceLayoutProcessFactory{/***获取具体策略**@paramctxcontext*@returnStrategy*/OptionalgetStrategy(LayoutContextctx);/***策略调用**@paramctxcontext*/voidexec(LayoutContextctx);}/***布局处理工厂实现*/@Slf4j@Service@RequiredArgsConstructorpublicclassLayoutProcessFactoryImplimplementsLayoutProcessFactory{//Spring会注入privatefinalListstrategyList按照@Order注解排序;@OverridepublicOptionalgetStrategyxtext(Layout){returnstrategyList.stream().filter(s->s.match(ctx)).findFirst();}}@Overridepublicvoidexec(LayoutContextctx){log.info("[Layoutprocessing]trytoprocessctx=>{}",ctx);getStrategy(ctx).ifPresent(s->{s.process(ctx);log.info([布局处理]处理完成ctx=>{}",ctx);});}}最后单测:@SpringBootTestclassLayoutProcessFactoryTest{@AutowiredprivateLayoutProcessFactoryprocessFactory;@TestvoidtestChild()throwsIllegalAccessException{//通过反射获取ChannelfinalFieldchildChannel=ReflectionUtils.findField(LayoutChildStrategy.class,"CHILD_CHANNEL");assertNotNull(childChannel);childChannel.setAccessible(true);//XXX:可以跟setAccessible禁止使用StringchildChannelStr=(String)childChannel.get(LayoutChildStrategy.class);//初始化上下文LayoutContextctx=LayoutContext.builder().channel(childChannelStr).build();//processFactory.exec(ctx);assertEquals("111",ctx.getLayoutId());}@TestvoidtestLpv(){LayoutContextctx=LayoutContext.builder().channel("LP-XX-XX-02").build();流程工厂。执行(ctx);assertEquals("222",ctx.getLayoutId());}@TestvoidtestDefault(){finalLayoutContextctx=LayoutContext.builder().build();processFactory.exec(ctx);assertEquals("999",ctx.getLayoutId());}}2思考能给我们带来什么策略模式?对业务逻辑进行了一定程度的封装,将不可变和可变逻辑分离,使得后续的业务变更只需要修改相应的策略或者增加新的策略即可。但想得更深一点。以前,易失性和非易失性逻辑的修改成本可能相差不大,但使用设计模式后,易失性代码修改的成本降低了,但非易失性代码修改的成本增加了。所以使用前请三思。策略模式会消除if-else吗?好像没有,直接把选项移回去(或者交给调用者)。该策略允许将原本混合在一个文件甚至一个函数中的代码分散到几个文件中。如果每段逻辑只是简单的几行代码,使用策略就得不偿失了。它不像if-else或switch那样容易理解和清楚。策略模式和其他模式有什么区别?有点像模板模式。而模板模式主要是在父类(上层)中安排一些动作和方法。而具体的动作和方法的实现是由不同的子类完成的。重点是编排。这有点像桥接模式。但是桥接在多个维度上都有变化,策略可以认为是一维的桥接。3后续打算在一篇文章中一起讲一下常用的设计模式。贴代码好像有点长,单独说一下。封面图片来源:https://refactoring.guru/desi...echo'5Y6f5Yib5paH56ugOiDmjpjph5Eo5L2g5oCO5LmI5Zad5aW26Iy25ZWKWzkyMzI0NTQ5NzU1NTA4MF0pL+aAneWQpihscGUyMzQp'|base64-d