设计模式之策略模式

源码

java程序的设计原则

6大原则:

单一职责:一个类和方法只做一件事。
开闭原则:对修改关闭,对扩展开发。
里氏替换原则:子类可扩展新方法,但不可修改父类已有方法(父类已提供了具体实现的方法)。
依赖倒置:依赖于抽象,而非具体实现,即面向接口编程(如方法参数,类属性使用接口声明,这样可接收任何子类)。
接口隔离:使用多个隔离的接口定义抽象,降低耦合。
最少知道/迪米特原则:降低类之间的依赖,聚合,组合等。

1:策略设计模式

策略设计模式一般使用的场景是,多种可互相替代的同类行为,在具体的运行过程中根据不同的情况,选择其中一种行为来执行,比如支付,有微信支付,支付宝支付,银行卡支付,那么到底使用哪种支付方式,这是由用户来决定的,再比如购物优惠,用户可以选择使用优惠券,可以选择满减优惠,以及其他优惠方式,到底是使用优惠券,还是满减,或者其他优惠方式,还是由用户来决定,类似的场景我们都可以考虑使用策略设计模式,可能对于类似的场景我们最常用的还是ifelse,ifelse的缺点是缺少扩展性,从6大原则来说不符合开闭原则,下面我们通过一个实际的场景来看下策略设计模式如何使用。

策略模式的UML图如下:

在这里插入图片描述

1.1:场景

在这里插入图片描述

在我们购物时经常会有如图中的优惠活动:

直减:比如在双十一等时间,商家会选择这种方式进行促销,如原价999的商品,直接降价到899。
满减:一般以券的方式发放给用户,当用户消费金额达到一定数目时,直接使用券来抵扣一定额度的现在,如图中“满2000减1000”,总价2999,用券后需要2899。
折扣:商家直接打折出售商品,如原价1000元的商品,直接八折出售,实际价格为800元。
N元购:另外一种营销手段,比如1元购1999元的手机,但是并非百分百可购得,而是有一定概率,类似于买彩票。

了解了以上的几种优惠活动,

下面我们先来看下通过常规的if-else如何实现,具体参考1.2:if-else实现

1.2:if-else实现

示例代码如下,用于计算用户实际需要支付的金额(仅仅示例,并没有提供真正实现)

class FakeCls {
    // type: 优惠方式
    // 1:直减 2:满减:3:折扣:4:N元购
    double needPayAmount(int type, String otherParams) {
        if (type == 1) {
            // 直减相关逻辑
        } else if (type == 2) {
            // 满减相关逻辑
        } else if (type == 3) {
            // 折扣相关逻辑
        } else if (type == 4) {
            // N元购相关逻辑
        }
    }   
}

以上的代码,很明显不符合6大涉及原则中的单一职责开闭原则(注意并不是默认其他原则都符合),这样写代码扩展性很弱,修改代码的成本高,对现有功能的影响大(说不定你一行代码的修改,老功能都不好用了,你说是让测试测还是不测,不测吧,很明显被影响了,测吧,又会增加人家的工作量),接下来我们看下如何使用策略设计模式来优化代码。

1.3:策略设计模式实现

首先根据依赖倒置原则,我们需要一个接口,如下:

public interface PayAmountStrategy {
    double payAmount(Object param);
}

再根据单一职责原则,我们分别为每种优惠策略提供一个具体实现类。

  • 直减
// 直减
public class DecreasePayAmountStrategy implements PayAmountStrategy {
    @Override
    public double payAmount(Object param) {
        System.out.println("使用直减方式支付");
        return 0;
    }
}
  • 满减
// 满减
public class CouponPayAmountStrategy implements PayAmountStrategy {
    @Override
    public double payAmount(Object param) {
        System.out.println("使用满减支付");
        return 0;
    }
}
  • 折扣
// 折扣
public class DiscountPayAmountStrategy implements PayAmountStrategy {
    @Override
    public double payAmount(Object param) {
        System.out.println("使用折扣方式支付");
        return 0;
    }
}
  • N元购
// N元购
public class NYuanPayAmountStrategy implements PayAmountStrategy {

    @Override
    public double payAmount(Object param) {
        System.out.println("使用N元购支付");
        return 0;
    }
}

接下来定义Context类:

public class PayAmountContext {
    // 依赖倒置原则,面向接口编程
    // 客户端需要设置自己想要使用的具体策略类,因此需要客户端对策略类有具体的了解,这点也是策略设计模式的不足之处
    private PayAmountStrategy payAmountStrategy;

    public PayAmountContext(/*PayAmount payAmount*/) {
        /*this.payAmount = payAmount;*/
    }

    public void setPayAmount(PayAmountStrategy payAmountStrategy) {
        this.payAmountStrategy = payAmountStrategy;
    }

    public double payAmount(Object param) {
        return this.payAmountStrategy.payAmount(param);
    }
}

此时将我们的类映射到策略设计模式UML图如下:

在这里插入图片描述

  • 测试
public class StrategyClient {
    public static void main(String[] args) {
        PayAmountContext payAmountContext = new PayAmountContext();
        // 使用直减策略
        payAmountContext.setPayAmount(new DecreasePayAmountStrategy());
        payAmountContext.payAmount(null);
        // 使用N元购策略
        payAmountContext.setPayAmount(new NYuanPayAmountStrategy());
        payAmountContext.payAmount(null);
        // 使用满减策略
        payAmountContext.setPayAmount(new CouponPayAmountStrategy());
        payAmountContext.payAmount(null);
        // 使用折扣策略
        payAmountContext.setPayAmount(new DiscountPayAmountStrategy());
        payAmountContext.payAmount(null);
    }
}

运行:

使用直减方式支付
使用N元购支付
使用满减支付
使用折扣方式支付

在实际工作中,不一定非得按照这种方式来做,也可以根据实际的情况进行变化和调整,但是是不变的。另外我们看到具体策略类还需要交给客户端来实例化,如果具体实例也能够做到对客户端透明就更好了,接下来我们一起看下如何实现。

1.4:基于注解和约定标记优化

在实际开发过程中,对于不同的支付方式,我们肯定都是会和客户端来约定标记来标示当前用户选择的是哪种支付方式的,那么就可以在标记上来做文章,做法具体是,首先定义一个注解,然后将该注解定义在具体的实现类上来和前端的标记对应起来,这样就能知道哪种支付方式方式对应的具体实现类是哪个了,因此我们先来定义这个注解,如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Key {
    String payType();
}

假定约定的标记和对应的支付方式如下:

满减 -> couponPay
直减 -> decreasePay
折扣 -> discountPay
N元购 -> nYuanPay

然后将注解是用在具体实现类上,修改如下:

// 满减
@Key(payType = "couponPay")
public class CouponPayAmountStrategy implements PayAmountStrategy {
    @Override
    public double payAmount(Object param) {
        System.out.println("使用满减支付V2");
        return 0;
    }
}
// 直减
@Key(payType = "decreasePay")
public class DecreasePayAmountStrategy implements PayAmountStrategy {
    @Override
    public double payAmount(Object param) {
        System.out.println("使用直减方式支付V2");
        return 0;
    }
}
// 折扣
@Key(payType = "discountPay")
public class DiscountPayAmountStrategy implements PayAmountStrategy {
    @Override
    public double payAmount(Object param) {
        System.out.println("使用折扣方式支付V2");
        return 0;
    }
}
// N元购
@Key(payType = "nYuanPay")
public class NYuanPayAmountStrategy implements PayAmountStrategy {

    @Override
    public double payAmount(Object param) {
        System.out.println("使用N元购支付V2");
        return 0;
    }
}

接下来使用SPI 来定义实现类:
src/main/resources/META-INF/services/dongshi.daddy.strategy.v2.PayAmountStrategy:

dongshi.daddy.strategy.v2.CouponPayAmountStrategy
dongshi.daddy.strategy.v2.DecreasePayAmountStrategy
dongshi.daddy.strategy.v2.DiscountPayAmountStrategy
dongshi.daddy.strategy.v2.NYuanPayAmountStrategy

定义简单工厂类:

public class PayTypeFactory {
    //    private static Map<Integer, PayAmountStrategy> productMap = new HashMap<>();
    private static Map<String, PayAmountStrategy> productMap = new HashMap<>();

    static {
        ServiceLoader<PayAmountStrategy> load = ServiceLoader.load(PayAmountStrategy.class);
        Iterator<PayAmountStrategy> iterator = load.iterator();
        while (iterator.hasNext()) {
            PayAmountStrategy next = iterator.next();
            Class<? extends PayAmountStrategy> aClass = next.getClass();
            if (!aClass.isAnnotationPresent(dongshi.daddy.strategy.v2.Key.class))
                throw new IllegalStateException("class: " + aClass + " expect @dongshi.daddy.strategy.v2.Key, but not found!");
//            String payType = aClass.getAnnotation(Key.class).payType();
            productMap.put(aClass.getAnnotation(Key.class).payType(), next);
        }
    }

    public static PayAmountStrategy makeProduct(String payType) {
        return productMap.get(payType);
    }
}

客户端测试类:

public class StrategyClient {
    public static void main(String[] args) {
        PayAmountContext payAmountContext = new PayAmountContext();
        /*// 使用直减策略
        payAmountContext.setPayAmount(new DecreasePayAmountStrategy());
        payAmountContext.payAmount(null);
        // 使用N元购策略
        payAmountContext.setPayAmount(new NYuanPayAmountStrategy());
        payAmountContext.payAmount(null);
        // 使用满减策略
        payAmountContext.setPayAmount(new CouponPayAmountStrategy());
        payAmountContext.payAmount(null);
        // 使用折扣策略
        payAmountContext.setPayAmount(new DiscountPayAmountStrategy());
        payAmountContext.payAmount(null);*/
        // 使用直减策略
        payAmountContext.setPayAmount(PayTypeFactory.makeProduct("decreasePay"));
        payAmountContext.payAmount(null);
        // 使用N元购策略
        payAmountContext.setPayAmount(PayTypeFactory.makeProduct("nYuanPay"));
        payAmountContext.payAmount(null);
        // 使用满减策略
        payAmountContext.setPayAmount(PayTypeFactory.makeProduct("couponPay"));
        payAmountContext.payAmount(null);
        // 使用折扣策略
        payAmountContext.setPayAmount(PayTypeFactory.makeProduct("discountPay"));
        payAmountContext.payAmount(null);

    }
}

运行:

使用直减方式支付V2
使用N元购支付V2
使用满减支付V2
使用折扣方式支付V2

这样子,当我们系统增加了一种新的支付方式时,只需要提供具体的实现类,然后使用注解设置其标记,并将实现类定义到SPI文件中,在客户端就可以通过PayTypeFactory.makeProduct("新支付方式标记")来使用新支付方式了,符合开闭原则。

参考文章列表

策略模式(策略设计模式)详解


版权声明:本文为wang0907原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。