续:
但是,其实我们的设计还是有些不完美的,因为它无法支持策略的重叠,这是什么意思呢?
就是说我们同一时间只能采用一种策略,假设我们商店现在有这么一个需求,假设到端午节了,我们商店要采取满1000返200,满2000返400的方式,并且原有的打折还要继续,这就相当于将返现金的活动与打折重叠计算了。
比如我是个金牌会员,假设我买了2000的东西,那么计算方式应该是先减去400为1600,再打五折,为800。最后这个会员只需要付800。
这就相当于将两个策略重叠使用了,我们现在的设计无法支持这种方式。那怎么办?需求也是在提醒我们在设计一个系统时要考虑全面,我们虽然不应该考虑一些本不存在或者发生概率很小的需求,但像商店或者商场这种灵活的促销方式,却是我们刚开始就应该考虑到的。
现在我们的需求变了,即我们任意的策略都可以随意组合,并且我们要求工厂帮我们自动判断,并将策略叠加返回给我们。那么针对上面的设计我们还需要改善,如果要改善一个设计,我们就需要考虑现有的设计不能支持什么需求。我们考虑上述设计不能支持什么。
1,我们只能根据客户消费的总金额去处理,而不能根据客户当次消费的金额去处理。
2,我们的设计只能支持单一策略,不能支持策略叠加。
为了满足这两个要求,我们需要添加一个类型的注解,去针对单次消费产生计费策略,另外,我们需要让策略工厂能够产生叠加的策略接口,那么冲着这个目标,我们首先定义如下三个注解,采用嵌套注解。
package com.calprice;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//我们定义一个嵌套注解
@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidRegion {
int max() default Integer.MAX_VALUE;
int min() default Integer.MIN_VALUE;
//既然可以任意组合,我们就需要给策略定义下顺序,就比如刚才说的2000那个例子,按先返后打折的顺序是800,反过来就是600了。
//所以我们必须支持这一特性,默认0,为最优先
int order() default 0;
}
定义上面这个嵌套注解是为了避免代码的重复,因为这三个属性我们在总额消费的策略注解和单次消费的策略注解中都要包括。下面给出另外两个注解。
package com.calprice;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//这是我们的总额有效区间注解,可以给策略添加有效区间的设置
@Target(ElementType.TYPE)//表示只能给类添加该注解
@Retention(RetentionPolicy.RUNTIME)//这个必须要将注解保留在运行时
public @interface TotalValidRegion {
//我们引用有效区间注解
ValidRegion value() default @ValidRegion;
}
这个总额注解与之前的注解基本一样,直接换成了嵌套注解。还有一个一次性消费的注解。如下:
package com.calprice;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//这是我们针对单次消费的有效区间注解,可以给策略添加有效区间的设置
@Target(ElementType.TYPE)//表示只能给类添加该注解
@Retention(RetentionPolicy.RUNTIME)//这个必须要将注解保留在运行时
public @interface OnceValidRegion{
//我们引用有效区间注解
ValidRegion value() default @ValidRegion;
}
以上三个注解我们就可以支持刚才的第一个要求了,我们可以针对一次消费进行策略判断,接下来我们需要修改策略工厂,去支持单次消费判断,并且还要支持策略重叠。如下:
package com.calprice;
import java.io.File;
import java.io.FileFilter;
import java.lang.annotation.Annotation;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedMap;
import java.util.TreeMap;
//我们使用一个标准的简单工厂来改进一下策略模式
public class CalPriceFactory {
private static final String CAL_PRICE_PACKAGE = "com.calprice";//这里是一个常量,表示我们扫描策略的包,这是LZ的包名
private ClassLoader classLoader = getClass().getClassLoader();//我们加载策略时的类加载器,我们任何类运行时信息必须来自该类加载器
private List<Class<? extends CalPrice>> calPriceList;//策略列表
//根据客户的总金额产生相应的策略
public CalPrice createCalPrice(Customer customer){
//变化点:为了支持优先级排序,我们采用可排序的MAP支持,这个Map是为了储存我们当前策略的运行时类信息
SortedMap<Integer, Class<? extends CalPrice>> clazzMap = new TreeMap<Integer, Class<? extends CalPrice>>();
//在策略列表查找策略
for (Class<? extends CalPrice> clazz : calPriceList) {
Annotation validRegion = handleAnnotation(clazz);//获取该策略的注解
//变化点:根据注解类型进行不同的判断
if (validRegion instanceof TotalValidRegion) {
TotalValidRegion totalValidRegion = (TotalValidRegion) validRegion;
//判断总金额是否在注解的区间
if (customer.getTotalAmount() > totalValidRegion.value().min() && customer.getTotalAmount() < totalValidRegion.value().max()) {
clazzMap.put(totalValidRegion.value().order(), clazz);//将采用的策略放入MAP
}
}
else if (validRegion instanceof OnceValidRegion) {
OnceValidRegion onceValidRegion = (OnceValidRegion) validRegion;
//判断单次金额是否在注解的区间,注意这次判断的是客户当次消费的金额
if (customer.getAmount() > onceValidRegion.value().min() && customer.getAmount() < onceValidRegion.value().max()) {
clazzMap.put(onceValidRegion.value().order(), clazz);//将采用的策略放入MAP
}
}
}
try {
//我们采用动态代理处理策略重叠的问题,相信看过LZ的代理模式的同学应该都对代理模式的原理很熟悉了,那么下面出现的代理类LZ将不再解释,留给各位自己琢磨。
return CalPriceProxy.getProxy(clazzMap);
} catch (Exception e) {
throw new RuntimeException("策略获得失败");
}
}
//处理注解,我们传入一个策略类,返回它的注解
private Annotation handleAnnotation(Class<? extends CalPrice> clazz){
Annotation[] annotations = clazz.getDeclaredAnnotations();
if (annotations == null || annotations.length == 0) {
return null;
}
for (int i = 0; i < annotations.length; i++) {
//变化点:这里稍微改动了下,如果是TotalValidRegion,OnceValidRegion这两种注解则返回
if (annotations[i] instanceof TotalValidRegion || annotations[i] instanceof OnceValidRegion) {
return annotations[i];
}
}
return null;
}
/* 以下不需要改变 */
//单例,并且我们需要在工厂初始化的时候
private CalPriceFactory(){
init();
}
//在工厂初始化时要初始化策略列表
private void init(){
calPriceList = new ArrayList<Class<? extends CalPrice>>();
File[] resources = getResources();//获取到包下所有的class文件
Class<CalPrice> calPriceClazz = null;
try {
calPriceClazz = (Class<CalPrice>) classLoader.loadClass(CalPrice.class.getName());//使用相同的加载器加载策略接口
} catch (ClassNotFoundException e1) {
throw new RuntimeException("未找到策略接口");
}
for (int i = 0; i < resources.length; i++) {
try {
//载入包下的类
Class<?> clazz = classLoader.loadClass(CAL_PRICE_PACKAGE + "."+resources[i].getName().replace(".class", ""));
//判断是否是CalPrice的实现类并且不是CalPrice它本身,满足的话加入到策略列表
if (CalPrice.class.isAssignableFrom(clazz) && clazz != calPriceClazz) {
calPriceList.add((Class<? extends CalPrice>) clazz);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
//获取扫描的包下面所有的class文件
private File[] getResources(){
try {
File file = new File(classLoader.getResource(CAL_PRICE_PACKAGE.replace(".", "/")).toURI());
return file.listFiles(new FileFilter() {
public boolean accept(File pathname) {
if (pathname.getName().endsWith(".class")) {//我们只扫描class文件
return true;
}
return false;
}
});
} catch (URISyntaxException e) {
throw new RuntimeException("未找到策略资源");
}
}
public static CalPriceFactory getInstance(){
return CalPriceFactoryInstance.instance;
}
private static class CalPriceFactoryInstance{
private static CalPriceFactory instance = new CalPriceFactory();
}
}
上面改动的地方并不多,主要是添加了一个单次消费的判断,另外就是没有直接返回策略实例,而是将满足条件的策略类信息传递给代理,产生一个代理,从而满足我们第二个要求,即策略可以重叠,代理类如下:
package com.calprice;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.SortedMap;
public class CalPriceProxy implements InvocationHandler{
private SortedMap<Integer, Class<? extends CalPrice>> clazzMap;
private CalPriceProxy(SortedMap<Integer, Class<? extends CalPrice>> clazzMap) {
super();
this.clazzMap = clazzMap;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Double result = 0D;
if (method.getName().equals("calPrice")) {
for (Class<? extends CalPrice> clazz : clazzMap.values()) {
if (result == 0) {
result = (Double) method.invoke(clazz.newInstance(), args);
}else {
result = (Double) method.invoke(clazz.newInstance(), result);
}
}
return result;
}
return null;
}
public static CalPrice getProxy(SortedMap<Integer, Class<? extends CalPrice>> clazzMap){
return (CalPrice) Proxy.newProxyInstance(CalPriceProxy.class.getClassLoader(), new Class<?>[]{CalPrice.class}, new CalPriceProxy(clazzMap));
}
}
现在可以支持策略重叠了,指定好一系列策略,如下:
package com.calprice;
//我们使用嵌套注解,并且制定我们打折的各个策略顺序是99,这算是很靠后的
//因为我们最后打折算出来钱是最多的,这个一算就很清楚,LZ不再解释数学问题
@TotalValidRegion(@ValidRegion(max=1000,order=99))
class Common implements CalPrice{
public Double calPrice(Double originalPrice) {
return originalPrice;
}
}
@TotalValidRegion(@ValidRegion(min=1000,max=2000,order=99))
class Vip implements CalPrice{
public Double calPrice(Double originalPrice) {
return originalPrice * 0.8;
}
}
@TotalValidRegion(@ValidRegion(min=2000,max=3000,order=99))
class SuperVip implements CalPrice{
public Double calPrice(Double originalPrice) {
return originalPrice * 0.7;
}
}
@TotalValidRegion(@ValidRegion(min=3000,order=99))
class GoldVip implements CalPrice{
public Double calPrice(Double originalPrice) {
return originalPrice * 0.5;
}
}
@OnceValidRegion(@ValidRegion(min=1000,max=2000,order=40))
class OneTDTwoH implements CalPrice{
public Double calPrice(Double originalPrice) {
return originalPrice - 200;
}
}
@OnceValidRegion(@ValidRegion(min=2000,order=40))
class TwotDFourH implements CalPrice{
public Double calPrice(Double originalPrice) {
return originalPrice - 400;
}
}
这里面相比之前,又添了两种策略,即满1000返200和满2000返400,并且优先级高于打折,也就是说会先计算现金返回,再打折。可以使用如下客户端测试一下。
package com.calprice;
//客户端调用
public class Client {
public static void main(String[] args) {
Customer customer = new Customer();
customer.buy(500D);
System.out.println("客户需要付钱:" + customer.calLastAmount());
customer.buy(1200D);
System.out.println("客户需要付钱:" + customer.calLastAmount());
customer.buy(1200D);
System.out.println("客户需要付钱:" + customer.calLastAmount());
customer.buy(1200D);
System.out.println("客户需要付钱:" + customer.calLastAmount());
customer.buy(2600D);
System.out.println("客户需要付钱:" + customer.calLastAmount());
}
}
这下策略模式就更加灵活了,不仅可以支持策略的随意增加,而且还可以重叠。当然,没有最好的设计只有更适合的设计,只要可以满足大部分需求,容纳大部分变化就算是很好的设计了。实在容纳和满足不了,我们还可以重构,而且重构往往会比预先设计更加凑效。
本次讲解策略模式算是由浅及深的方式,刚开始给出的是作为一个JAVA新人的时候对策略模式的理解,后面是一个代入了业务场景的例子,以及后面的逐渐使用简单工厂,注解,反射,代理等方式改善我们的策略工厂的过程,算是进行一个设计思想的锻炼吧。
策略模式本身并不太复杂,实现也比较简单,但是我们却花费了大量的篇幅去完善它,这是因为完善策略模式往往比使用更加复杂。当然策略模式也有缺点,就是不停的在各个算法间切换,造成很多逻辑判断。
最后总结一下策略模式的使用场景,就是有一系列的可相互替换的算法的时候,我们就可以使用策略模式将这些算法做成接口的实现,并让我们依赖于算法的类依赖于抽象的算法接口,这样可以彻底消除类与具体算法之间的耦合。
比如现在客户类,它就知道有个CalPrice接口可以计算最终价格,其它的它什么都不知道了,符合之前总纲中提到的最小知道原则。
共同学习,写下你的评论
评论加载中...
作者其他优质文章