扩展性改造--策略模式
引言
相信很多人多看过“策略模式”的定义、类图关系、以及使用介绍,本文的标题是“扩展性改造--策略模式”,但不会一开始就对“策略模式”的定义、类图进行讲解。
本文通过一个笔者最近开发中遇到的真实案例进行讲解,来说明“策略模式”到底用来解决什么问题,或者说什么情况下该使用“策略模式”,让大家更清楚的理解“策略模式”。
最近在做的项目是一个多产品融合项目,以前这几个产品是独立开发、部署、维护,但这些产品都有很多相似之处,比如pc店铺、m店铺、pc活动、m活动(pc指的是电脑版,m指的是移动版)。但所有相似(或者相同)的功能要在多个系统中单独维护,这些相似的功能有变动时,每个系统都需要做对应的修改,复用性很差。这个多产品融合项目,其中一个项目目标是为了提高“复用性”。
扩展性问题
把多个产品融合到一个系统确实解决了我们大部分“相似组件”的复用性问题,最近几天对同事们的代码进行codeview,发现这种融合代码设计又引入的新的扩展性问题。先来看下现在的代码结构:
A、首先定义一个接口类(BaseService),该类包含定各个业务接口方法定义(对应pc店铺、m店铺、pc活动、m活动)
B、然后定义一个抽象类(BaseAbsServcie),该类实现了部分各个业务都相同的业务方法,并对各个业务实现不同的部分方法定义为abstract方法。
C、最后按业务定义4个不同的实现类:PcShopServiceImpl、MShopServiceImpl、PcActServiceImpl、MActServiceImpl。
类图关系如下:
乍一看没啥问题,思路很清晰,4个业务公共的操作提到BaseAbsServcie中实现,变动的业务放到4个业务实现类里实现,实现了“复用性”和“扩展性”的完美结合。真的“完美”了吗?来看几个问题:
1、如果有一个方法,只能部分复用:比如pc店铺和m店铺是相同的实现,pc活动和m活动实现是相同,但店铺跟活动实现有差别。那这个方法是放到各个实现类里、还是BaseAbsServcie里呢?如果放到实现类里,就无法复用;如果是放到BaseAbsServcie里,只能部分就得定义成两个方法,对应的在BaseService里也得定义成两个方法,但实际上只应该定义一个方法。
2、BaseAbsServcie中定义的是公共方法,如果有一天这个公共方法店铺的需要调整,而活动业务的不变(或者pc业务的需要调整,而m业务的不变),怎么处理?在BaseAbsServcie中拆成两个方法,还是该到各个业务实现方法中去重写,不管采用哪种方式,对于后期维护都是灾难性的。
3、假设有一天业务扩展了,新增两个微信、手Q业务,又该怎么办?
打住。。。我已经不知道将来该如何维护这套代码了。
那应该如何来改进呢?其实这是一个非常普遍的问题,在java编程的世界里,已经有很多人遇到过类似的问题,并已经有很好的解决方案形成一种编程模式,大家只要采用这种模式进行处理即可。
OO设计原则
为了解决上述问题,我们先来看下两条“OO设计原则”:1、针对接口编程、不针对实现编程;2、多用组合,少用继承。
上述问题的根本原因,就是违背了这两条设计原则。上述方案的本质是上面向实现编程,对每个业务创建一个实现类;在复用性上采用的是继承实现,而非组合。
什么是“面向接口编程”,什么是“组合”?我们先来看下新的设计方案:
1、分析该服务里包含的公共“行为”:页面渲染、缓存处理。定义两个行为接口类: RenderBehavior、CacheBehavior。
2、创建BaseService服务接口类,并创建一个抽象的实现类BaseAbsServcie,通过“组合”的方式,把RenderBehavior、CacheBehavior定义为BaseAbsServcie的成员变量。
3、定义“页面渲染行为”RenderBehavior的实现,根据业务分为pc页面渲染和m页面渲染,分别创建接口实现类:PcRenderImpl(pc活动和pc店铺都属于pc渲染)、MRenderImpl(m活动和m店铺都属于m渲染),
4、定义“缓存处理行为”CacheBehavior的实现,根据业务分为redis缓存和硬盘存储,分别创建接口实现类:CacheRedisImpl、 CacheDiskImpl (无缓存实现CacheNoImpl)。
5、重新定义抽象服务类BaseAbsServcie的4个业务子类:MActServiceImpl、MShopServiceImpl、PcActServiceImpl、PcShopServiceImpl
由于代码内容较多,这里没有把具体的代码内容贴出来,具体代码内容详见github:https://github.com/gantianxing/strategy.git
最终的类图如下:
可以看到,该方案通过“组合”的方式把抽取的“渲染行为”和“缓存处理行为”整合到BaseAbsServcie,而不是通过继承。并把面向业务的的子类实现,改为面向“渲染行为”和“缓存处理行为”接口的实现,这就是前面提到的“面向接口编程”。
假设现在要增加一个“微信手q”业务页面渲染,并且不能cdn缓存。这时我们只需要新增一个“WqRenderImpl”,缓存处理复用CacheNoImpl,再创建一个BaseAbsService的子类WqServiceImpl即可完成业务扩展,并且不会对已有代码造成任何影响。
最后编写测试方法:
public class Test { public static void main(String[] args) { MActServiceImpl mact = new MActServiceImpl(new MRenderImpl(),new CacheRedisImpl()); //mact.setCacheBehavior(new CacheNoImpl());//动态调整缓存行为 String pageId="sdfsdfsfd"; mact.render(pageId); } }
运行github中的代码,打印消息如下:
设置m活动页缓存key 设置m活动页cdn缓存URL m页面渲染 第一步:采用redis缓存m_act_page_sdfsdfsfd 第二步:清除cdn缓存sale.jd.com/m/act/sdfsdfsfd.html
策略模式
其实上述优化过程就是使用的“策略模式”,其核心就是:抽取类中的所有“行为”(可以有不同算法实现),并为可变的“行为”定义接口,为每个接口创建不同的算法实现(称之为“面向接口编程”);并把这些“行为接口”作为“成员变量”引入到策略类(称之为“组合”);在策略类的子类中,根据业务需要选择指定的算法实现进行初始化。
以上述代码为例:
“行为接口”类有两个:RenderBehavior、CacheBehavior;
“行为算法实现”类有5个,对于上面两个行为接口:
PcRenderImpl、MRenderImpl、CacheRedisImpl、CacheNoImpl、CacheDiskImpl;
“策略类”为:BaseAbsServcie,行为接口作为其成员变量;
“策略类的子类,对应4个具体的业务:
PcShopServiceImpl、PcActServiceImpl、MShopServiceImpl、MActServiceImpl。
策略模式可以在不影响已有“行为”算法的情况下,实现对 “行为”新算法的无限扩展,以及算法的动态切换,并且不会影响客户端代码。从而是代码框架具备良好的扩展性和维护性(修改其中一个算法,不会影响到其他算法)。
策略模式遵循上述提到的设计原则:1、针对接口编程、不针对实现编程;2、多用组合,少用继承
在spring中使用策略模式
上述测试代码是在main方法中运行,但我们现实中大多数情况下都是使用的spring框架。最后来看下在spring中如何使用策略模式。
首先创建一个工厂类,具体代码内容如下:
public class MySpringFactory { private Map<String, BaseAbsServcie> servcieMap = new HashMap(); public Map<String, BaseAbsServcie> getServcieMap() { return servcieMap; } public void setServcieMap(Map<String, BaseAbsServcie> servcieMap) { this.servcieMap = servcieMap; } public void doRender(String strType,String pageId) { this.servcieMap.get(strType).render(pageId); } }
然后定义spring bean xml配置文件(java配置方式可以自行实现):
<?xml version="1.0" encoding="GBK"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd" default-autowire="byName"> <bean id="mySpringFactory" class = "com.sky.strategy.MySpringFactory"> <property name="servcieMap"> <map> <entry key="pcAct" value-ref="pcAct"/> <entry key="mAct" value-ref="mAct"/> <entry key="pcShop" value-ref="pcShop"/> <entry key="mShop" value-ref="mShop"/> </map> </property> </bean> <bean id="pcAct" class="com.sky.strategy.service.impl.PcActServiceImpl"> <constructor-arg name="renderBehavior" ref="pcRender"/> <constructor-arg name="cacheBehavior" ref="cacheRedis"/> </bean> <bean id="mAct" class="com.sky.strategy.service.impl.MActServiceImpl"> <constructor-arg name="renderBehavior" ref="mRender"/> <constructor-arg name="cacheBehavior" ref="cacheRedis"/> </bean> <bean id="pcShop" class="com.sky.strategy.service.impl.PcShopServiceImpl"> <constructor-arg name="renderBehavior" ref="pcRender"/> <constructor-arg name="cacheBehavior" ref="cacheDisk"/> </bean> <bean id="mShop" class="com.sky.strategy.service.impl.MShopServiceImpl"> <constructor-arg name="renderBehavior" ref="mRender"/> <constructor-arg name="cacheBehavior" ref="cacheDisk"/> </bean> <bean id="pcRender" class="com.sky.strategy.behavior.impl.PcRenderImpl"/> <bean id="mRender" class="com.sky.strategy.behavior.impl.MRenderImpl"/> <bean id="cacheDisk" class="com.sky.strategy.behavior.impl.CacheDiskImpl"/> <bean id="cacheNo" class="com.sky.strategy.behavior.impl.CacheNoImpl"/> <bean id="cacheRedis" class="com.sky.strategy.behavior.impl.CacheRedisImpl"/> </beans>
在需要使用的地方直接引用MySpringFactory对应的spring bean即可:
private MySpringFactory mySpringFactory; public void commonRender(String type,String pageId){ mySpringFactory.doRender(type,pageId); }
代码详见github: https://github.com/gantianxing/strategy.git
上一篇: 策略模式一商场促销商品