策略模式
在策略模式(Strategy Pattern)中,一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。在策略模式中,我们创建表示各种策略的对象和一个行为随着策略对象改变而改变的 context 对象。策略对象改变 context 对象的执行算法。
介绍
意图:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
主要解决:在有多种算法相似的情况下,使用 if...else 所带来的复杂和难以维护。
何时使用:一个系统有许多许多类,而区分它们的只是他们直接的行为。
如何解决:将这些算法封装成一个一个的类,任意地替换。
关键代码:实现同一个接口。
应用实例: 1、诸葛亮的锦囊妙计,每一个锦囊就是一个策略。 2、旅行的出游方式,选择骑自行车、坐汽车,每一种旅行方式都是一个策略。 3、JAVA AWT 中的 LayoutManager。
优点: 1、算法可以自由切换。 2、避免使用多重条件判断。 3、扩展性良好。
缺点: 1、策略类会增多。 2、所有策略类都需要对外暴露。
使用场景: 1、如果在一个系统里面有许多类,它们之间的区别仅在于它们的行为,那么使用策略模式可以动态地让一个对象在许多行为中选择一种行为。 2、一个系统需要动态地在几种算法中选择一种。 3、如果一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重的条件选择语句来实现。
注意事项:如果一个系统的策略多于四个,就需要考虑使用混合模式,解决策略类膨胀的问题。
用策略模式实现选择支付方式的业务场景
大家都知道,优惠活动,优惠策略会有很多种可能如:领取优惠券抵扣、返现促销、拼团优惠。下面我们用代码来模拟,首先我们创建一个促销策略的抽象PromotionStrategy:
public interface PromotionStrategy {
void doPromotion();
}
然后分别创建优惠券抵扣策略 CouponStrategy 类、返现促销策略 CashbackStrategy类、拼团优惠策略GroupbuyStrategy类和无优惠策略EmptyStrategy类,CouponStrategy类:
public class CouponStrategy implements PromotionStrategy {
public void doPromotion() {
System.out.println("领取优惠券,课程的价格直接减优惠券面值抵扣");
}
}
CashbackStrategy类:
public class CashbackStrategy implements PromotionStrategy {
public void doPromotion() {
System.out.println("返现促销,返回的金额转到支付宝账号");
}
}
GroupbuyStrategy类:
public class GroupbuyStrategy implements PromotionStrategy{
public void doPromotion() {
System.out.println("拼团,满20人成团,全团享受团购价");
}
}
EmptyStrategy类:
public class EmptyStrategy implements PromotionStrategy {
public void doPromotion() {
System.out.println("无促销活动");
}
}
然后创建促销活动方案PromotionActivity类:
public class PromotionActivity {
private PromotionStrategy promotionStrategy;
public PromotionActivity(PromotionStrategy promotionStrategy) {
this.promotionStrategy = promotionStrategy;
}
public void execute(){
promotionStrategy.doPromotion();
}
}
编写客户端测试类:
public class PromotionActivityTest {
public static void main(String[] args) {
PromotionActivity activity618 = new PromotionActivity(new CouponStrategy());
PromotionActivity activity1111 = new PromotionActivity(new CashbackStrategy());
activity618.execute();
activity1111.execute();
}
}
此时,小伙伴们会发现,如果把上面这段测试代码放到实际的业务场景其实并不实用。因为我们做活动时候往往是要根据不同的需求对促销策略进行动态选择的,并不会一次性执行多种优惠。所以,我们的代码通常会这样写:
public static void main(String[] args) {
PromotionActivity promotionActivity = null;
String promotionKey = "COUPON";
if(StringUtils.equals(promotionKey,"COUPON")){
promotionActivity = new PromotionActivity(new CouponStrategy());
}else if(StringUtils.equals(promotionKey,"CASHBACK")){
promotionActivity = new PromotionActivity(new CashbackStrategy());
}//......
promotionActivity.execute();
}
这样改造之后,满足了业务需求,客户可根据自己的需求选择不同的优惠策略了。但是,经过一段时间的业务积累,我们的促销活动会越来越多。于是,我们的程序猿小哥哥就忙不赢了,每次上活动之前都要通宵改代码,而且要做重复测试,判断逻辑可能也变得越来越复杂。这时候,我们是不需要思考代码是不是应该重构了?回顾我们之前学过的设计模式应该如何来优化这段代码呢?其实,我们可以结合单例模式和工厂模式。创建PromotionStrategyFactory 类:
public class PromotionStrategyFactory {
private static Map<String,PromotionStrategy> PROMOTION_STRATEGY_MAP = new HashMap<String, PromotionStrategy>();
static {
PROMOTION_STRATEGY_MAP.put(PromotionKey.COUPON,new CouponStrategy());
PROMOTION_STRATEGY_MAP.put(PromotionKey.CASHBACK,new CashbackStrategy());
PROMOTION_STRATEGY_MAP.put(PromotionKey.GROUPBUY,new GroupbuyStrategy());
}
private static final PromotionStrategy NON_PROMOTION = new EmptyStrategy();
private PromotionStrategyFactory(){}
public static PromotionStrategy getPromotionStrategy(String promotionKey){
PromotionStrategy promotionStrategy = PROMOTION_STRATEGY_MAP.get(promotionKey);
return promotionStrategy == null ? NON_PROMOTION : promotionStrategy;
}
private interface PromotionKey{
String COUPON = "COUPON";
String CASHBACK = "CASHBACK";
String GROUPBUY = "GROUPBUY";
}
}
这时候我们客户端代码就应该这样写了:
public static void main(String[] args) {
String promotionKey = "GROUPBUY";
PromotionActivity promotionActivity = new PromotionActivity(PromotionStrategyFactory.getPromotionStrategy(promotionKey));
promotionActivity.execute();
}
图片.png代码优化之后,是不是我们程序猿小哥哥的维护工作就轻松了?每次上新活动,不影响原来的代码逻辑。为了加深对策略模式的理解,我们再来举一个案例。相信小伙伴们都用过支付宝、微信支付、银联支付以及京东白条。一个常见的应用场景就是大家在下单支付时会提示选择支付方式,如果用户未选,系统也会默认好推荐的支付方式进行结算。来看一下类图,下面我们用策略模式来模拟此业务场景:
创建Payment抽象类,定义支付规范和支付逻辑,代码如下:
public abstract class Payment {
//支付类型
public abstract String getName();
//查询余额
protected abstract double queryBalance(String uid);
//扣款支付
public MsgResult pay(String uid, double amount) {
if(queryBalance(uid) < amount){
return new MsgResult(500,"支付失败","余额不足");
}
return new MsgResult(200,"支付成功","支付金额:" + amount);
}
}
分别创建具体的支付方式,支付宝AliPay类:
public class AliPay extends Payment {
public String getName() {
return "支付宝";
}
protected double queryBalance(String uid) {
return 900;
}
}
京东白条JDPay类:
public class JDPay extends Payment {
public String getName() {
return "京东白条";
}
protected double queryBalance(String uid) {
return 500;
}
}
微信支付WechatPay类:
public class WechatPay extends Payment {
public String getName() {
return "微信支付";
}
protected double queryBalance(String uid) {
return 256;
}
}
银联支付UnionPay类:
public class UnionPay extends Payment {
public String getName() {
return "银联支付";
}
protected double queryBalance(String uid) {
return 120;
}
}
创建支付状态的包装类MsgResult:
public class MsgResult {
private int code;
private Object data;
private String msg;
public MsgResult(int code, String msg, Object data) {
this.code = code;
this.data = data;
this.msg = msg;
}
public String toString(){
return ("支付状态:[" + code + "]," + msg + ",交易详情:" + data);
}
}
创建支付策略管理类:
public class PayStrategy {
public static final String ALI_PAY = "AliPay";
public static final String JD_PAY = "JdPay";
public static final String UNION_PAY = "UnionPay";
public static final String WECHAT_PAY = "WechatPay";
public static final String DEFAULT_PAY = ALI_PAY;
private static Map<String,Payment> payStrategy = new HashMap<String,Payment>();
static {
payStrategy.put(ALI_PAY,new AliPay());
payStrategy.put(WECHAT_PAY,new WechatPay());
payStrategy.put(UNION_PAY,new UnionPay());
payStrategy.put(JD_PAY,new JDPay());
}
public static Payment get(String payKey){
if(!payStrategy.containsKey(payKey)){
return payStrategy.get(DEFAULT_PAY);
}
return payStrategy.get(payKey);
}
}
创建订单Order类:
public class Order {
private String uid;
private String orderId;
private double amount;
public Order(String uid,String orderId,double amount){
this.uid = uid;
this.orderId = orderId;
this.amount = amount;
}
//完美地解决了switch的过程,不需要在代码逻辑中写switch了
//更不需要写if else if
public MsgResult pay(){
return pay(PayStrategy.DEFAULT_PAY);
}
public MsgResult pay(String payKey){
Payment payment = PayStrategy.get(payKey);
System.out.println("欢迎使用" + payment.getName());
System.out.println("本次交易金额为:" + amount + ",开始扣款...");
return payment.pay(uid,amount);
}
}
测试代码:
public class PayStrategyTest {
public static void main(String[] args) {
//省略把商品添加到购物车,再从购物车下单
//直接从点单开始
Order order = new Order("1","20180311001000009",324.45);
//开始支付,选择微信支付、支付宝、银联卡、京东白条、财付通
//每个渠道它支付的具体算法是不一样的
//基本算法固定的
//这个值是在支付的时候才决定用哪个值
System.out.println(order.pay(PayStrategy.ALI_PAY));
}
}
测试结果:
欢迎使用支付宝
本次交易金额为:324.45,开始扣款...
支付状态:[200],支付成功,交易详情:支付金额:324.45
Process finished with exit code 0
策略模式在JDK源码中的体现
首先来看一个比较常用的比较器 Comparator 接口,我们看到的一个大家常用的compare()方法,就是一个策略抽象实现:
@FunctionalInterface
public interface Comparator<T> {
...
int compare(T o1, T o2);
...
}
Comparator抽象下面有非常多的实现类,我们经常会把Comparator 作为参数传入作为排序策略,例如Arrays类的parallelSort 方法等:
public class Arrays {
...
public static <T> void parallelSort(T[] a, Comparator<? super T> cmp) {
if (cmp == null)
cmp = NaturalOrder.INSTANCE;
int n = a.length, p, g;
if (n <= MIN_ARRAY_SORT_GRAN ||
(p = ForkJoinPool.getCommonPoolParallelism()) == 1)
TimSort.sort(a, 0, n, cmp, null, 0, 0);
else
new ArraysParallelSortHelpers.FJObject.Sorter<T>
(null, a,
(T[])Array.newInstance(a.getClass().getComponentType(), n),
0, n, 0, ((g = n / (p << 2)) <= MIN_ARRAY_SORT_GRAN) ?
MIN_ARRAY_SORT_GRAN : g, cmp).invoke();
}
...
}
还有TreeMap的构造方法:
public class TreeMap<K,V> extends AbstractMap<K,V> implements NavigableMap<K,V>, Cloneable, java.io.Serializable{
...
public TreeMap(Comparator<? super K> comparator) {
this.comparator = comparator;
}
...
}
策略模式在Spring源码中的应用
Resource类:
public interface Resource extends InputStreamSource {
boolean exists();
default boolean isReadable() {
return true;
}
default boolean isOpen() {
return false;
}
default boolean isFile() {
return false;
}
URL getURL() throws IOException;
URI getURI() throws IOException;
File getFile() throws IOException;
default ReadableByteChannel readableChannel() throws IOException {
return Channels.newChannel(this.getInputStream());
}
long contentLength() throws IOException;
long lastModified() throws IOException;
Resource createRelative(String var1) throws IOException;
@Nullable
String getFilename();
String getDescription();
}
图片.png我们虽然没有直接使用Resource类,但是我们经常使用它的子类,例如:
还有一个非常典型的场景,Spring的初始化也采用了策略模式,不同的类型的类采用不同的初始化策略。首先有一个InstantiationStrategy接口,我们来看一下源码:
public interface InstantiationStrategy {
Object instantiate(RootBeanDefinition var1, @Nullable String var2, BeanFactory var3) throws BeansException;
Object instantiate(RootBeanDefinition var1, @Nullable String var2, BeanFactory var3, Constructor<?> var4, @Nullable Object... var5) throws BeansException;
Object instantiate(RootBeanDefinition var1, @Nullable String var2, BeanFactory var3, @Nullable Object var4, Method var5, @Nullable Object... var6) throws BeansException;
}
图片.png顶层的策略抽象非常简单,但是它下面有两种策略SimpleInstantiationStrategy和CglibSubclassingInstantiationStrategy,我们看一下类图:
打开类图我们还发现CglibSubclassingInstantiationStrategy策略类还继承了SimpleInstantiationStrategy类,说明在实际应用中多种策略之间还可以继承使用。小伙们可以作为一个参考,在实际业务场景中,可以根据需要来设计。
策略模式的优缺点
优点:
1、策略模式符合开闭原则。
2、避免使用多重条件转移语句,如if...else...语句、switch语句
3、使用策略模式可以提高算法的保密性和安全性。
缺点:
1、客户端必须知道所有的策略,并且自行决定使用哪一个策略类。
2、代码中会产生非常多策略类,增加维护难度。
委派模式与策略模式综合应用
在上面的代码中我们列举了非常几个业务场景,相信小伙伴对委派模式和策略模式有了非常深刻的理解了。现在,我们再来回顾一下,DispatcherServlet 的委派逻辑,代码如下:
private void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception{
String uri = request.getRequestURI();
String mid = request.getParameter("mid");
if("getMemberById".equals(uri)){
new MemberController().getMemberById(mid);
}else if("getOrderById".equals(uri)){
new OrderController().getOrderById(mid);
}else if("logout".equals(uri)){
new SystemController().logout();
}else {
response.getWriter().write("404 Not Found!!");
}
}
这样的代码扩展性不太优雅, 也不现实, 因为我们实际项目中一定不止这几个Controller,往往是成千上万个Controller,显然,我们不能写成千上万个if...else... 。那么我们如何来改造呢?小伙伴们一定首先就想到了策略模式,来看一下我是怎么优化的:
public class DispatcherServlet extends HttpServlet{
private List<Handler> handlerMapping = new ArrayList<Handler>();
public void init() throws ServletException {
try {
Class<?> memberControllerClass = MemberController.class;
handlerMapping.add(new Handler()
.setController(memberControllerClass.newInstance())
.setMethod(memberControllerClass.getMethod("getMemberById", new Class[]{String.class}))
.setUrl("/web/getMemberById.json"));
}catch(Exception e){
}
}
private void doDispatch(HttpServletRequest request, HttpServletResponse response){
//1、获取用户请求的url
// 如果按照J2EE的标准、每个url对对应一个Serlvet,url由浏览器输入
String uri = request.getRequestURI();
//2、Servlet拿到url以后,要做权衡(要做判断,要做选择)
// 根据用户请求的URL,去找到这个url对应的某一个java类的方法
//3、通过拿到的URL去handlerMapping(我们把它认为是策略常量)
Handler handle = null;
for (Handler h: handlerMapping) {
if(uri.equals(h.getUrl())){
handle = h;
break;
}
}
//4、将具体的任务分发给Method(通过反射去调用其对应的方法)
Object object = null;
try {
object = handle.getMethod().invoke(handle.getController(),request.getParameter("mid"));
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
//5、获取到Method执行的结果,通过Response返回出去
// response.getWriter().write();
}
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
doDispatch(req,resp);
} catch (Exception e) {
e.printStackTrace();
}
}
class Handler{
private Object controller;
private Method method;
private String url;
public Object getController() {
return controller;
}
public Handler setController(Object controller) {
this.controller = controller;
return this;
}
public Method getMethod() {
return method;
}
public Handler setMethod(Method method) {
this.method = method;
return this;
}
public String getUrl() {
return url;
}
public Handler setUrl(String url) {
this.url = url;
return this;
}
}
}
上面的代码我结合了策略模式、工厂模式、单例模式。当然,我的优化方案不一定是最完美的,仅代表个人观点。感兴趣的小伙伴可以继续思考,如何让这段代码变得更优雅。
一些信息
路漫漫其修远兮,吾将上下而求索
码云:https://gitee.com/javacoo
QQ群:164863067
作者/微信:javacoo
邮箱:xihuady@126.com
网友评论