策略模式从零到一,奥利给!

528 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

上一篇文章介绍了什么是面向对象编程,以及简单的工厂模式,第二篇文章呢说的是23种设计模式中的策略模式,对,就是本专栏第一个设计模式文章!首先设计模式这四个字我想应该很容易理解,说白点就是做一件事的不同方法,最终效果是一样的,但是做的过程是不同的。

tips:在本人知乎专栏中有一篇Thread类源码解析的文章,Thread类实际上用到的就是策略模式。这两篇文章都看,相信你会掌握策略模式的!

用户故事:我想要一个商场收银软件,根据客户所购买商品的单价和数量计算出客户需要支付的费用

故事分析及设计

我们大概需要做这样一些事:

  1. 知道用户买了哪些商品,以及商品单价与购买数量(单个商品的费用为单价乘以数量)
  2. 通过运算得到最终需要支付的金额

功能实现

好吧,这次找不到合适人选,还是小林来实现这个功能。

小林:哎呦,你这不是打我脸吗?这功能不是很简单吗,思路清晰的不行,我秒写,看好喽!

五分钟,小林写下来这样的一段代码:

public class StoreCashier {
    public static void main(String[] args) {

        List<Shop> shopList = new ArrayList<>();
        shopList.add(new Shop(1, 1.0, "豆沙包"));
        shopList.add(new Shop(1, 2.5, "大肉包"));
        shopList.add(new Shop(1, 2.0, "绿豆粥"));
        shopList.add(new Shop(1, 2.5, "早餐豆浆"));

        System.out.println("购买商品的总价格是:" + getCount(shopList));


    }

    public static double getCount(List<Shop> list) {
        // 定义一个变量用于统计总价格
        double allCount = 0.0d;
        for (Shop shop : list) {
            allCount += shop.getNum() * shop.getPrice();
        }
        return allCount;
    }
}

有点东西啊小林,五分钟就解决了,你以后一定是个人才。小林如果要增加一个打八折活动可以实现吗?

小林稍加思索:很简单啊,我在总金额后面乘0.8不就行了?

那如果是有些商品打八折,有些商品打七折呢?你怎么改?

小林陷入了沉思,突然灵光一现:我有思路了,我可以添加一个参数商品类型,然后使用switch语句分情况进行,打八折的商品乘以0.8,打七折的商品乘以0.7。我把代码写出来了,你看看吧。

public class StoreCashier {
    public static void main(String[] args) {

        List<Shop> shopList = new ArrayList<>();
        shopList.add(new Shop(1, 1.0, "豆沙包"));
        shopList.add(new Shop(1, 2.5, "大肉包"));
        shopList.add(new Shop(1, 2.0, "绿豆粥"));
        shopList.add(new Shop(1, 2.5, "早餐豆浆"));

        System.out.println("正常收费购买商品的总价格是:" + getCount(shopList, "正常收费"));
        System.out.println("打八折购买商品的总价格是:" + getCount(shopList, "打八折"));
        System.out.println("打七折购买商品的总价格是:" + getCount(shopList, "打七折"));
        System.out.println("打五折购买商品的总价格是:" + getCount(shopList, "打五折"));
    }

    public static double getCount(List<Shop> list, String discountType) {
        // 定义一个变量用于统计总价格
        double allCount = 0.0d;
        switch (discountType) {
            case "正常收费":
                for (Shop shop : list) {
                    allCount += shop.getNum() * shop.getPrice();
                }
                break;
            case "打八折":
                for (Shop shop : list) {
                    allCount += shop.getNum() * shop.getPrice() * 0.8;
                }
                break;
            case "打七折":
                for (Shop shop : list) {
                    allCount += shop.getNum() * shop.getPrice() * 0.7;
                }
                break;
            case "打五折":
                for (Shop shop : list) {
                    allCount += shop.getNum() * shop.getPrice() * 0.5;
                }
                break;
        }
        return allCount;
    }
}

思路清晰,不错。但是存在一些问题,比如四个分支的for循序是不是一模一样的?这不是主要的,现在各平台满减活动是数不胜数,如果我想加一个满5减1的活动,应该怎么实现呢?如果我从一折达到九折,什么折都打,你要写多个case?

小林思考了许久:我想起来上篇文章的简单工厂模式,我可以抽象一个商品类出来,让普通商品,打折商品,满减商品分别去继承商品类,然后重写计算金额方法。这样代码就会好一点了。大概这这样:

// 商品基类
public class ShopCount {

    public ShopCount(){

    }

    // 计算商品价格
    public double getCount(double money){
        return money;
    }

}
// 正常商品直接返回money
public class ZhengchangShopCount extends ShopCount {
    @Override
    public double getCount(double money){
        return money;
    }
}


// 打折商品
public class DazheShopCount extends ShopCount{
    // 存储打多少折
    private double moneyRebate = 0d;

    // 创建该商品需要一个参数,告诉我打多少折
    public DazheShopCount(double moneyRebate){
        this.moneyRebate = moneyRebate;
    }

    @Override
    public double getCount(double money){
        return money * moneyRebate;
    }
}


// 满减商品:需要两个参数,一个是返利条件,一个是返利值
public class ManjianShopCount extends ShopCount {
    // 返利条件
    private double moneyCondition = 0d;
    // 返利值
    private double moneyReturn = 0;

    public ManjianShopCount(double moneyCondition, double moneyReturn) {
        this.moneyCondition = moneyCondition;
        this.moneyReturn = moneyReturn;
    }

    @Override
    public double getCount(double money){
         if (money>moneyCondition){
             return money - moneyReturn;
         }else {
             return money;
         }
    }
}
// 商品工厂
public class ShopCountFactory {

    public static ShopCount createShopCount(String shopType){
        ShopCount shopCount = null;
        switch (shopType){
            case "正常商品":
                shopCount = new ZhengchangShopCount();
                break;
            case "打八折":
                shopCount = new DazheShopCount(0.8);
                break;
            case "满5减1":
                shopCount = new ManjianShopCount(5.0,1.0);
                break;
        }
        return shopCount;
    }
}
public class MainTest {
    public static void main(String[] args) {
        // 小林的日常早餐
        List<Shop> shopList = new ArrayList<>();
        shopList.add(new Shop(1, 2.5, "大肉包"));
        shopList.add(new Shop(1, 2.0, "绿豆粥"));
        shopList.add(new Shop(1, 1.5, "鸡蛋"));

        // 统一小林应该付多少钱
        double allCount = 0d;
        for (Shop shop : shopList) {
            allCount += shop.getNum() * shop.getPrice();
        }
        System.out.println("正常情况小林要支付:" + ShopCountFactory.createShopCount("正常商品").getCount(allCount));
        System.out.println("打八折小林要支付:" + ShopCountFactory.createShopCount("打八折").getCount(allCount));
        System.out.println("满5减1小林要支付:" + ShopCountFactory.createShopCount("满5减1").getCount(allCount));
    }
}

不错,不错,小林你已经掌握了简单工厂模式了,恭喜你。既然你已经熟悉了简单工厂,那我们就来个其他的问题,虽然现在我们现在可以随意的增加打折的折扣力度,以及满减的规则。但是一年这么多个节日,超市中活动层出不穷,所以商品的类型(打折,原价,满减)变化是非常快的。我们在变得不是商品本身,因为它的名字之类的信息是不存在变化的,变化的计算价格的算法。 鉴于算法多变,我们不可能频繁修改工厂类,因为一旦修改就需要重新部署,机器多的话是很大的一个工作量。所以需要封装这个变化点

小林想破脑袋也没有想到上面这段话是什么意思.

我们直接告诉他吧,首先看下策略模式比较正式的定义:

策略模式:它定义了算法家族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化,不会影响到使用算法的客户。

商场何时打折,何时满减,亦或是其他一些促销活动。变化的是计算总价的算法,我们用工厂去生成这些算法对象是可行的,但是这种算法变化是非常快的,我们称为变化点在面向对象中,封装变化点是一种很重要的思维方式。

小林:我懂了,上个版本我写的ShopCount类实际上可以改为抽象策略接口,而原来的三个子类就可以实现抽象策略接口,这个抽象策略接口中就一个方法,那就是我们计算价格的策略!在这个基础上我加一个 ShopContext类去维护抽象策略接口的对象,通过多态来实现策略的变化。看看我写的代码吧!

// 抽象策略接口
public interface ShopCountSategy {
    // 计算商品价格
    double getCount(double money);
}
// 普通商品
public class ZhengchangShopCount implements ShopCountSategy {
    @Override
    public double getCount(double money){
        return money;
    }
}

// 满减商品
public class ManjianShopCount implements ShopCountSategy {
    // 返利条件
    private double moneyCondition = 0d;
    // 返利值
    private double moneyReturn = 0;

    public ManjianShopCount(double moneyCondition, double moneyReturn) {
        this.moneyCondition = moneyCondition;
        this.moneyReturn = moneyReturn;
    }

    @Override
    public double getCount(double money){
         if (money>moneyCondition){
             return money - moneyReturn;
         }else {
             return money;
         }
    }
}

// 打折商品
public class DazheShopCount implements ShopCountSategy {
    private double moneyRebate = 0d;

    public DazheShopCount(double moneyRebate){
        this.moneyRebate = moneyRebate;
    }

    @Override
    public double getCount(double money){
        return money * moneyRebate;
    }
}
public class ShopContext  {

    private ShopCountSategy shopCountSategy;

    public ShopContext(ShopCountSategy shopCountSategy){
        this.shopCountSategy = shopCountSategy;
    }

    public double startCount(double money) {
        return shopCountSategy.getCount(money);
    }
}
public class MainTest {
    public static void main(String[] args) {
        // 小林的日常早餐
        List<Shop> shopList = new ArrayList<>();
        shopList.add(new Shop(1, 2.5, "大肉包"));
        shopList.add(new Shop(1, 2.0, "绿豆粥"));
        shopList.add(new Shop(1, 1.5, "鸡蛋"));

        // 统一小林应该付多少钱
        double allCount = 0d;
        for (Shop shop : shopList) {
            allCount += shop.getNum() * shop.getPrice();
        }

        // 控制台判断商品类型(正常商品,打八折商品,满5减1商品)
        String shopType = "打八折";
        ShopContext shopContext = null;

        switch (shopType) {
            case "正常商品":
                shopContext = new ShopContext(new ZhengchangShopCount());
                break;
            case "打八折":
                shopContext = new ShopContext(new DazheShopCount(0.8));
                break;
            case "满5减1":
                shopContext = new ShopContext(new ManjianShopCount(5.0, 1.0));
                break;
        }
        System.out.println("小林应该支付:" + shopContext.startCount(allCount));
    }
}

我们看看上面的代码是否可以在进一步呢?现在代码有的MainTest中有一个switch语句,switch的语句是我自己造的,也就是说现在是在客户端去判断用哪个算法。我们得想办法将这部分判断移出客户端。

小林:上篇文章讲的简单工厂,这篇文章是策略模式,哦,我突然有个大胆的想法,工厂不一定就是一个单独的类,我们可以将工厂模式与策略模式进行一个结合。这样还减少了耦合!哈哈,我改进一下:

public class ShopContext  {

    private ShopCountSategy shopCountSategy;

    public ShopContext(String type){
        switch (type){
            case "正常商品":
                shopCountSategy = new ZhengchangShopCount();
                break;
            case "打八折":
                shopCountSategy = new DazheShopCount(0.8);
                break;
            case "满5减1":
                shopCountSategy = new ManjianShopCount(5.0, 1.0);
                break;
        }
    }

    public double startCount(double money) {
        return shopCountSategy.getCount(money);
    }
}
public class MainTest {
    public static void main(String[] args) {
        // 小林的日常早餐
        List<Shop> shopList = new ArrayList<>();
        shopList.add(new Shop(1, 2.5, "大肉包"));
        shopList.add(new Shop(1, 2.0, "绿豆粥"));
        shopList.add(new Shop(1, 1.5, "鸡蛋"));

        // 统一小林应该付多少钱
        double allCount = 0d;
        for (Shop shop : shopList) {
            allCount += shop.getNum() * shop.getPrice();
        }

        // 控制台判断商品类型(正常商品,打八折商品,满5减1商品)
        ShopContext zhengchangContext = new ShopContext("正常商品");
        System.out.println("小林应该支付:" + zhengchangContext.startCount(allCount));
        ShopContext dazheContext = new ShopContext("打八折");
        System.out.println("小林应该支付:" + dazheContext.startCount(allCount));
        ShopContext manjianContext = new ShopContext("满5减1");
        System.out.println("小林应该支付:" + manjianContext.startCount(allCount));
    }
}

小林:现在你看看,我的代码是不是就比较有设计感了呢?

小林,这我可得给你竖大拇指啊,孺子可教也!

自信源于努力,我是小林,我们下期见!