设计模式-策略模式及应用

365 阅读8分钟

在平时的编码过程中,我们往往会遇到一个接口的实现会有多种,不同的实现(不同的策略)使用在不同的场景中。举个简单的场景,项目中使用到的站内消息,站内消息的存储介质我们可以是关系型数据库(又分不同的厂商),也可以是非关系型数据库。比如新增插入一条消息这个方法,他的实现方式可能就会很多,如我们现实为了满足不同的客户,关系型数据库使用的有mysql、oracel有些客户则使用的是mongonDB非关系型数据库,客户根据自己的需求,我们动态去调整切换,而无需重新开发。

不单单是代码层面,比如每年的双十一,我们买一件商品;这几年商家搞得五花八门的优惠方案供你选择。优惠券的组合方案,什么跨店满减、定金抵现金、成为本店会员享受商品几折的优惠等等,往往我们会综合考虑,考某种方式最省钱,我们会倾向于某种方式。今天介绍的策略模式,能很好解决我们上面生活中,或者软件设计的过程中所遇到这些问题。

定义及结构特点

策略模式,英文全称是Strategy Design Pattern。在GoF的《设计模式》一书中,它是这样定义的: 定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。策略模式可以使算法的变化独立于使用它们的客戶端(这里的客戶端代指使用算法的代码)。对应我们前面所讲的例子就是我们的消息写入实现。 策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。

策略模式的结构与实现

策略模式是准备一组算法,并将这组算法封装到一系列的策略类里面,作为一个抽象策略类的子类。策略模式的重心不是如何实现算法,而是如何组织这些算法,从而让程序结构更加灵活,具有更好的维护性和扩展性,现在我们来分析其基本结构和实现方法。

  1. 模式的结构
  • 抽象策略(Strategy)类:定义了一个公共接口,各种不同的算法以不同的方式实现这个接口,环境角色使用这个接口调用不同的算法,一般使用接口或抽象类实现。
  • 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现。
  • 环境(Context)类:持有一个策略类的引用,最终给客户端调用。

2.策略模式结构图 在这里插入图片描述

代码案例
1.案例1
  • 抽象策略
public interface AbstractStrategy {
  // 可以是接口或者抽象类的抽象方法	
  void operation();
}
  • 具体策略1
public class ConcreteStrategy1 implements AbstractStrategy{
	@Override
	public void operation() {
       System.out.println("策略1具体实现");		
	}
}
  • 具体策略2
public class ConcreteStrategy2 implements AbstractStrategy{
	@Override
	public void operation() {
       System.out.println("策略2具体实现");		
	}
}
  • 策略环境
public class Context {

	private AbstractStrategy strategy;

	public AbstractStrategy getStrategy() {
		return strategy;
	}

	public void setStrategy(AbstractStrategy strategy) {
		this.strategy = strategy;
	}
	
	public void doStrategy(){
		strategy.operation();
	}
}
  • 客户端
public class Client {
	
	public static void main(String[] args) {
		
		ConcreteStrategy1 concreteStrategy1 = new ConcreteStrategy1();
		// 设置指定策略
		Context context = new Context();
		context.setStrategy(concreteStrategy1);
		// 调用
		context.doStrategy();
		System.out.println("-------重新设置--------");
		ConcreteStrategy2 concreteStrategy2 = new ConcreteStrategy2();
		context.setStrategy(concreteStrategy2);
		context.doStrategy();
	}
}

执行结果:

-------重新设置--------
策略2具体实现

通过在环境上下文中设置不同的策略实现,来切换策略的实现,对应前面的概念定义:定义一族算法类,将每个算法分别封装起来,让它们可以互相替换。体会一下

2.案例2

案例2,来举一个实际场景的案例,假如现在需要做一份虾。每个人的口味不同,可能去吃饭时候点的做法也不同。这里我们吧厨师的对虾的烹饪方式,作为策略的抽象。不同的做法为抽象策略的具体实现(不同算法)。

  • 抽象策略
//抽象策略
public interface AbstractShrimpCook {
	// 大虾烹饪方式
	void cook();
}
  • 具体实现A
public class BraiseShrimp implements AbstractShrimpCook{
	@Override
	public void cook() {
		System.out.println("红烧大虾...");
	}
}
  • 具体实现B
public class PoachShrimp implements AbstractShrimpCook {
	@Override
	public void cook() {
		System.out.println("水煮大虾...");
	}
}
  • 具体实现C
//清蒸
public class SteamedShrimp implements AbstractShrimpCook {
  public void cook() {
	   System.out.println("清蒸大虾...");
	}
}
  • 环境信息
public class Context {

	AbstractShrimpCook abstractShrimpCook;

	public AbstractShrimpCook getAbstractShrimpCook() {
		return abstractShrimpCook;
	}

	public void setAbstractShrimpCook(AbstractShrimpCook abstractShrimpCook) {
		this.abstractShrimpCook = abstractShrimpCook;
	}
	
	// 调用
	public void cook() {
		abstractShrimpCook.cook();
	}
}
  • 客户端
public class Client {
	public static void main(String[] args) throws Exception {
		// 水煮
		String cookType = "POACH";
		
		AbstractShrimpCook abstractShrimpCook;
		if ("POACH".equals(cookType)) {
			abstractShrimpCook = new PoachShrimp();
		}
		else if ("BRAISE".equals(cookType)) {
			abstractShrimpCook = new BraiseShrimp();
		}
		else if ("STEAMED".equals(cookType)) {
			abstractShrimpCook = new SteamedShrimp();
		} else {
			throw new Exception("未找到指定类型策略");
		}
		
		Context context = new Context();
		context.setAbstractShrimpCook(abstractShrimpCook);
		
		context.cook();
	}
}

这里我们模拟程序的调用方,根据传递的指定类型,来获取具体的策略即抽象策略的具体实现算法。

这里有个严重的问题,作为使用方来说,我们可以看出并不是无需关注策略的实现方式。具体使用的策略是根据执行的策略类型决定的。如果增加新的实现,使用方也需要增加新的ifelse判断逻辑。这也违背了策略模式的精髓。无需重点关注策略的实现,而是如何组织这些算法,从而让程序结构更加灵活,具有更好的维护性和扩展性。

下面将案例2改造一下;将context改造为一下方式,并增加一个烹饪类型枚举对象

  • 枚举对象
public enum CookType {
	POACH,
	BRAISE,
	STEAMED;
}

  • context
public class Kitchen {
	AbstractShrimpCook abstractShrimpCook;
	//策略缓存
    static final Map<CookType,AbstractShrimpCook> cookMap = new HashMap<CookType, AbstractShrimpCook>();
	
	static {
		cookMap.put(CookType.POACH, new PoachShrimp());
		cookMap.put(CookType.BRAISE, new BraiseShrimp());
		cookMap.put(CookType.STEAMED, new SteamedShrimp());
	}

	public AbstractShrimpCook getAbstractShrimpCook() {
		return abstractShrimpCook;
	}

	public void setAbstractShrimpCook(AbstractShrimpCook abstractShrimpCook) {
		this.abstractShrimpCook = abstractShrimpCook;
	}

	//调用具体策略方法
	public void cookMethod() {
		abstractShrimpCook.cook();
	}
	
	// 通过类型匹配缓存中的策略
	public void cookMethod2(CookType cookStrategy){
		AbstractShrimpCook cShrimpCook = cookMap.get(cookStrategy);
		if (Objects.nonNull(cShrimpCook)) {
			cShrimpCook.cook();
		}else {
			throw new IllegalArgumentException("this "+ cookStrategy +"cookStrategy not found.");
		}
	}
}
  • 客户端2
public class Client {
	
	public static void main(String[] args) {
		//初始化厨房(context)
		Kitchen kitchen = new Kitchen();
		//初始化烹饪策略(方式)
	    PoachShrimp poachShrimp = new PoachShrimp();
	    kitchen.setAbstractShrimpCook(poachShrimp);
	    //制作
	    poachShrimp.cook();
	    System.out.println("------方式二------");
	    kitchen.cookMethod2(CookType.BRAISE);  
	}
}

水煮大虾... ------方式二------ 红烧大虾...

案例结构图: 在这里插入图片描述

这种方法与方法一区别是,context(Kitchen)维护了所有算法的实现,客户端无需关注具体的实现,使用时传递指定的类型即可。及时后续有拓展的需求,只需要怎么抽象策略的实现,增加到context中,使用方也无需改动代码逻辑,且做到了算法的实现与使用做到了隔离,满足开闭原则。策略增加客户端无需增加新的判断逻辑。

策略模式的优缺点及应用场景

1.优点:

  • 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句,如 if...else 语句、switch...case 语句。
  • 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
  • 策略模式可以提供相同行为的不同实现,客户可以根据不同需求,动态切换实现。
  • 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
  • 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。

2.缺点:

  • 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
  • 策略模式造成很多的策略类,增加维护难度。
策略模式的应用场景
  • 系统中对一种算法(方法)有多种实现,且需要动态的切换算法的场景;
  • 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时;
  • 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。

策略模式在源码中的使用

  • JDK源码的使用 在JDK中,Comparator接口提供比较方法compare()抽象,用于定义排序规则。实现类可以自定义的传递其比较算法,用于排序。
 public static void main(String[] args) {
        //定义一个整型数组
        Integer[] array = {3, 5, 7, 2, 8, 1, 9};

        // 定义升序匿名实现算法
        Comparator<Integer> comparator = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                if (o1 < o2) {
                    return -1;
                } else {
                    return 0;
                }
            }
        };
        //使用 arrays 工具类排序
        Arrays.sort(array,comparator);
       System.out.println(Arrays.toString(array));
    }
}

执行结果 [1, 2, 3, 5, 7, 8, 9] 看下JDK中,Arrays.sort() 方法的源码

public static <T> void sort(T[] a, Comparator<? super T> c) {
    if (c == null) {
        sort(a);
    } else {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a, c);
        else
            TimSort.sort(a, 0, a.length, c, null, 0, 0);
    }
}

默认如果不传排序算法,可以使用默认的排序规则(升序),也可以自定义指定排序的规则。

这里是我们自定义实现的升序算法,在JDK的集合中,构造方法往往可以传递我们的排序算法。比如TreeMap

public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable {
    // 传递 Comparator 排序算法,map中存储的值,按照排序算法排序
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
}

我们知道treeMap是有序的,通过构造函数的排序算法可以自定义排序策略。还有例如TreeSet集合,源码如下

public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
{
public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }
    }