一篇搞懂策略模式的实战应用

3,695 阅读9分钟

一、前言

  在上一篇我们学习了工厂模式的应用,这一篇我们会接着学习策略模式在实际项目当中的应用。

  首先,在讲策略模式之前,我们看一下日常生活中关于大型超市的会员策略:
  商场往往根据不同的客户制定不同的商品报价策略,比如针对普通会员用户打9折扣,针对VIP会员打8折,针对超级会员用户打5折...

  现在我们要做一个报价管理的模块,简要点就是要针对不同的会员客户,提供不同的折扣报价。

  我们一般的代码可能是这样子的:

package com.MyMineBug.demoRun.strategy;

import java.math.BigDecimal;

public class MemberManagement {

	public BigDecimal calculatePrice(String customType){
        if ("普通会员用户".equals(customType)) {
            System.out.println("抱歉!普通会员用户打9折!");
            return new BigDecimal("90");
        }else if ("VIP会员".equals(customType)) {
            System.out.println("恭喜你!VIP会员打8折!");
            return new BigDecimal("80");
        }else if("超级会员用户".equals(customType)){
            System.out.println("恭喜你!超级会员用户打5折!");
            return new BigDecimal("50");
        }
        //普通用户都是原价
        return new BigDecimal("100");
    }

}

经过测试,上面的代码工作的很好,可是上面的代码是有问题的。上面存在的问题:把不同客户的报价的算法都放在了同一个方法里面,使得该方法很是庞大(现在是只是一个演示,所以看起来还不是很臃肿)。

如果后面继续对其进行优化,可能是会将每种会员提取出来,作为一种单独的算法,但是这样会违反我们的开闭原则

开闭原则:

1.对于扩展是开放的(Open for extension)。这意味着模块的行为是可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。也就是说,我们可以改变模块的功能。

2.对于修改是关闭的(Closed for modification)。对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。

那有没有什么办法使得我们的报价管理即可扩展、可维护,又可以方便的响应变化呢?当然有解决方案啦,就是我们下面要讲的策略模式

二、初步了解策略模式

2.1 定义

  策略模式定义了一系列的算法,并将每一个算法封装起来,使每个算法可以相互替代,使算法本身和使用算法的客户端分割开来,相互独立。

2.2 结构

1.策略接口角色IStrategy:用来约束一系列具体的策略算法,策略上下文角色StrategyContext使用此策略接口来调用具体的策略所实现的算法。

2.具体策略实现角色ConcreteStrategy:具体的策略实现,即具体的算法实现。

3.策略上下文角色StrategyContext:策略上下文,负责和具体的策略实现交互,通常策略上下文对象会持有一个真正的策略实现对象,策略上下文还可以让具体的策略实现从其中获取相关数据,回调策略上下文对象的方法。

2.3 使用策略模式重写报价管理模块

  这里实现,我们是通过spring注解的方式,将我们策略的实现类注入到map里面,项目启动时,会自动将IStrategy的实现类注入到策略上下文StrategyContext,具体实现如下:

公共报价策略接口:

package com.MyMineBug.demoRun.strategy;

import java.math.BigDecimal;

public interface IStrategy  {

	/**
	 * 计算价格
	 * @return
	 */
	public BigDecimal calculatePrice();
}

普通会员用户报价策略实现:

package com.MyMineBug.demoRun.strategy;

import java.math.BigDecimal;

import org.springframework.stereotype.Component;
@Component("GeneralMember")
public class GeneralMember implements IStrategy{

	@Override
	public BigDecimal calculatePrice() {
		return new BigDecimal("90");
	}

}

VIP会员用户策略实现:

package com.MyMineBug.demoRun.strategy;

import java.math.BigDecimal;

import org.springframework.stereotype.Component;

@Component("VipMember")
public class VipMember implements IStrategy{

	@Override
	public BigDecimal calculatePrice() {
		return new BigDecimal("80");
	}

}

超级会员用户策略实现:

package com.MyMineBug.demoRun.strategy;

import java.math.BigDecimal;

import org.springframework.stereotype.Component;

@Component("SuperMember")
public class SuperMember implements IStrategy{

	@Override
	public BigDecimal calculatePrice() {
		return new BigDecimal("50");
	}

}

策略报价上下文:

package com.MyMineBug.demoRun.strategy;
/**
 * 策略管理器
 * @author 18360
 *
 */

import java.math.BigDecimal;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
public class StrategyContext {
	
	private final Map<String, IStrategy> strategyMap = new ConcurrentHashMap<String, IStrategy>();
	
	/**
	 * 注入所有实现了IStrategy接口的Bean
	 * @param strategyMap
	 */
	@Autowired
	public void StrategyInterface(Map<String, IStrategy> strategyMap) {
		this.strategyMap.clear();
		strategyMap.forEach((k, v)-> this.strategyMap.put(k, v));
	}
	
    /**
     * 计算价格
     * @param memberLevel   会员等级
     * @return              价格
     */
    public BigDecimal calculatePrice(String memberLevel) {
    	if(!StringUtils.isEmpty(memberLevel)){
    		return strategyMap.get(memberLevel).calculatePrice();
    	}
		return null;
    }
}

由于是spring boot项目,所以我们可以在controller层模拟外部不同的会员用户的报价,代码如下:

package com.MyMineBug.demoRun.controller;

import java.math.BigDecimal;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import com.MyMineBug.demoRun.strategy.StrategyContext;

@RestController
@RequestMapping
public class StrategyController {

	@Autowired
	private StrategyContext strategyContext;

	@RequestMapping("/calculatePrice")
	public BigDecimal calculatePrice(@RequestParam("memberLevel") String memberLevel) {
		return strategyContext.calculatePrice(memberLevel);
	}
	
	@RequestMapping("/whello")
	public String hello() {
		return "hello run"; 
	}

}

如果启动spring boot项目报错,需要在启动入口加上扫描注解:@ComponentScan(basePackages = {"com.MyMineBug.demoRun"})

启动服务后,在界面输入http://localhost:8080/calculatePrice?memberLevel=GeneralMember

输出:

三、深入理解策略模式

3.1 策略模式的作用

  就是把具体的算法实现从业务逻辑中剥离出来,成为一系列独立算法类,使得它们可以相互替换。

3.2 策略模式的着重点

  不需要去管如何来实现算法,而是管如何组织和调用这些算法,从而让我们的程序结构更加的灵活、可扩展。

  我们前面的第一个报价管理的示例,发现每个策略算法实现对应的都是在MemberManagement中calculatePrice方法中的if else语句里面,我们知道if else if语句里面的代码在执行的可能性方面可以说是平等的,你要么执行if,要么执行else,要么执行else if。
  策略模式就是把各个平等的具体实现进行抽象、封装成为独立的算法类,然后通过上下文和具体的算法类来进行交互。各个策略算法都是平等的,地位是一样的,正是由于各个算法的平等性,所以它们才是可以相互替换的。虽然我们可以动态的切换各个策略,但是同一时刻只能使用一个策略。

四、策略模式在JDK中的运用

  在多线程编程中,我们经常使用线程池来管理线程,以减缓线程频繁的创建和销毁带来的资源的浪费,在创建线程池的时候,经常使用一个工厂类来创建线程池Executors,实际上Executors的内部使用的是类ThreadPoolExecutor。它有一个最终的构造函数如下:

corePoolSize:线程池中的核心线程数量,即使这些线程没有任务干,也不会将其销毁。

maximumPoolSize:线程池中的最多能够创建的线程数量。

keepAliveTime:当线程池中的线程数量大于corePoolSize时,多余的线程等待新任务的最长时间。

unit:keepAliveTime的时间单位。

workQueue:在线程池中的线程还没有还得及执行任务之前,保存任务的队列(当线程池中的线程都有任务在执行的时候,仍然有任务不断的提交过来,这些任务保存在workQueue队列中)。

threadFactory:创建线程池中线程的工厂。

handler:当线程池中没有多余的线程来执行任务,并且保存任务的多列也满了(指的是有界队列),对仍在提交给线程池的任务的处理策略。

RejectedExecutionHandler 是一个策略接口,用在当线程池中没有多余的线程来执行任务,并且保存任务的多列也满了(指的是有界队列),对仍在提交给线程池的任务的处理策略。

公共策略接口:

这个策略接口有四个实现类:

AbortPolicy:该策略是直接将提交的任务抛弃掉,并抛出RejectedExecutionException异常。

DiscardPolicy:该策略也是将任务抛弃掉(对于提交的任务不管不问,什么也不做),不过并不抛出异常。

DiscardOldestPolicy:该策略是当执行器未关闭时,从任务队列workQueue中取出第一个任务,并抛弃这第一个任务,进而有空间存储刚刚提交的任务。使用该策略要特别小心,因为它会直接抛弃之前的任务。

CallerRunsPolicy:该策略并没有抛弃任何的任务,由于线程池中已经没有了多余的线程来分配该任务,该策略是在当前线程(调用者线程)中直接执行该任务。

类ThreadPoolExecutor中持有一个RejectedExecutionHandler接口的引用,以便在构造函数中可以由外部客户端自己制定具体的策略并注入。下面看一下其类图:

五、总结

策略模式的优点:

  1.策略模式的功能就是通过抽象、封装来定义一系列的算法,使得这些算法可以相互替换,所以为这些算法定义一个公共的接口,以约束这些算法的功能实现。如果这些算法具有公共的功能,可以将接口变为抽象类,将公共功能放到抽象父类里面。

  2.策略模式的一系列算法是可以相互替换的、是平等的,写在一起就是if-else组织结构,如果算法实现里又有条件语句,就构成了多重条件语句,可以用策略模式,避免这样的多重条件语句。

  3.扩展性更好:在策略模式中扩展策略实现非常的容易,只要新增一个策略实现类,然后在使用策略实现的地方,使用这个新的策略实现就好了。

策略模式的缺点:

  1.客户端必须了解所有的策略,清楚它们的不同: 如果由客户端来决定使用何种算法,那客户端必须知道所有的策略,清楚各个策略的功能和不同,这样才能做出正确的选择,但是这暴露了策略的具体实现。

  2.增加了对象的数量: 由于策略模式将每个具体的算法都单独封装为一个策略类,如果可选的策略有很多的话,那对象的数量也会很多。

  3.只适合偏平的算法结构:由于策略模式的各个策略实现是平等的关系(可相互替换),实际上就构成了一个扁平的算法结构。即一个策略接口下面有多个平等的策略实现(多个策略实现是兄弟关系),并且运行时只能有一个算法被使用。这就限制了算法的使用层级,且不能被嵌套。

  策略模式的本质:分离算法,选择实现。

  如果觉得还不错,请点个赞!!!

  Share Technology And Love Life