大话设计模式读书笔记(策略模式)

人物:小菜,大鸟

事件:做一个商场收银软件,营业员根据客户所购买的商品的单价和数量,向客户收费


策略模式:

1.大鸟让小菜做一个小软件,能输入单价和数量,计算总价,小菜第一次很粗糙地完成了

2.大鸟指出了小菜软件的不可扩展性,不灵活等缺点,然后小菜回想着上次学习的简单工厂模式,接着进行了小菜的第二次实现

3.大鸟又指出频繁的策略变动可能会导致重复的代码上线,让小菜继续改进,于是在大鸟的引导下,开始先了解起了策略模式

4.在了解了策模式后,小菜将策略模式融入软件,又发现又得在客户端进行判断(这个缺点具体参考简单工厂模式一章)

5.小菜最后将简单工厂模式和策略模式相结合,完成了设计与实现

6.小结了策略模式

小菜初次尝试 

用两个文本框输入单价和数量,一个确定按钮算出每种商品费用,列表框记录商品清单,一个标签记录总计,最后加上一个重置按钮,关键代码如下:

@Slf4j
public class CashierSystem {
    BigDecimal totalPrice = BigDecimal.ZERO;

    private BigDecimal getTotalPrice(BigDecimal price, BigDecimal num) {
        totalPrice = totalPrice.add(price.multiply(num)).setScale(2, RoundingMode.HALF_UP);
        return totalPrice;
    }

    public static void main(String[] args) {
        CashierSystem result = new CashierSystem();

        BigDecimal applePrice = new BigDecimal("2");
        BigDecimal appleNumber = new BigDecimal("5");
        result.getTotalPrice(applePrice, appleNumber).toString();

        BigDecimal peachPrice = new BigDecimal("6.6");
        BigDecimal peachNumber = new BigDecimal("6");
        String resultPrice = result.getTotalPrice(peachPrice, peachNumber).toString();

        log.info("总价为:{}", resultPrice);
    }
}

大鸟:那现在商场要求对商品搞活动,所有商品打八折

小菜:那在最后的totalPrice乘以0.8不就行了?

大鸟:那这样不是活动完了,还要把代码再改一遍?

小菜:那我增加一个下拉框,可以选择是打8折还是原价

小菜尝试将可能打折的内容全部列出:

public enum DiscountTypeEnum {

    NO_DISCOUNT(BigDecimal.ONE, "原价"),

    DISCOUNT_EIGHTY(new BigDecimal("0.8"), "打八折"),

    DISCOUNT_HALF(new BigDecimal("0.5"), "打半折");

    private BigDecimal code;
    private String message;

    DiscountTypeEnum(BigDecimal code, String message) {
        this.code = code;
        this.message = message;
    }

    public BigDecimal getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

然后前端页面只要选了相应的折扣,就能直接在totalPrice上做打折处理:

@Slf4j
public class CashierSystem {
    BigDecimal totalPrice = BigDecimal.ZERO;

    private BigDecimal getTotalPrice(BigDecimal price, BigDecimal num) {
        totalPrice = totalPrice.add(price.multiply(num)).setScale(2, RoundingMode.HALF_UP);
        return totalPrice;
    }

    public static void main(String[] args) {
        CashierSystem result = new CashierSystem();
        BigDecimal peachPrice = new BigDecimal("6.6");
        BigDecimal peachNumber = new BigDecimal("6");
        String resultPrice = result.getTotalPrice(peachPrice, peachNumber)
                .multiply(DiscountTypeEnum.DISCOUNT_EIGHTY.getCode())
                .toString();
        log.info("总价为:{}", resultPrice);
    }
}

大鸟:但是你列出的打折可能性有限,如果又出现满300返100,满700返300的活动,那就不只是做乘法了,肯定还会改变原来代码逻辑的,那又该怎么办?

小菜:那用简单工厂模式,根据需求,子类有几个写几个,如:打八折,打半折,满300返100等,都写上

大鸟:用设计模式的时候先想想,怎么用合理,难道后面打三折还要再加一个子类?要知道哪些是同一类型的,哪些是不同的

小菜用简单工厂模式的尝试:

先划分子类的类型,现在可以区分为三种,一种是正常售卖,一种是打折类型,直接初始化参数即可,最后一种是满减,用两个参数做传参即可:

现金收费抽象类:

public abstract class AbstractCashierSystem {
    public abstract BigDecimal acceptCash(BigDecimal money);
}

原价收费子类:

public class CashNormal extends AbstractCashierSystem {
    @Override
    public BigDecimal acceptCash(BigDecimal money) {
        return money;
    }
}

打折收费子类:

@Data
public class CashRebate extends AbstractCashierSystem {
    private String discountType;

    @Override
    public BigDecimal acceptCash(BigDecimal money) {
        //从客户端传来的discountType,这里举例:"NO_DISCOUNT"
        String discountType = "NO_DISCOUNT";
        return money.multiply(DiscountTypeEnum.valueOf(discountType).getCode());
    }

    CashRebate(String discountType) {
        this.discountType = discountType;
    }
}

返利收费子类:

@Data
public class CashReturn extends AbstractCashierSystem {
    private BigDecimal moneyCondition;
    private BigDecimal moneyReturn;

    @Override
    public BigDecimal acceptCash(BigDecimal money) {
        BigDecimal result = money;
        if (money.compareTo(moneyCondition) > 0) {
            result = money.subtract(
                    money.divide(moneyCondition, 4, RoundingMode.HALF_UP).multiply(moneyReturn)
            );
        }
        return result;
    }

    public CashReturn(BigDecimal moneyCondition, BigDecimal moneyReturn) {
        this.moneyCondition = moneyCondition;
        this.moneyReturn = moneyReturn;
    }

}

收费工厂类:

public class CashFactory {
    public static AbstractCashierSystem createCashAccept(String type) {
        AbstractCashierSystem cs = null;
        switch (type) {
            case "正常收费":
                cs = new CashNormal();
                break;
            case "满300返100":
                CashReturn cr1 = new CashReturn(new BigDecimal("300"), new BigDecimal("100"));
                cs = cr1;
                break;
            case "打8折":
                CashRebate cr2 = new CashRebate(DiscountTypeEnum.DISCOUNT_EIGHTY.getMessage());
                cs = cr2;
                break;
        }
        return cs;
    }
}

客户端:

@Slf4j
public class CashOperation {
    public static void main(String[] args) {
        BigDecimal totalPrice;
        //比如客户端选择的是满返,满300返100
        AbstractCashierSystem cs = CashFactory.createCashAccept("满300返100");
        totalPrice = cs.acceptCash(new BigDecimal("400"));
        log.info("合计总价为:{}", totalPrice);
    }
}

综上小结:面向对象的编程,并不是类越多越好,类的划分是为了封装,但分类的基础是抽象,具有相同属性和功能的对象的抽象集合才是类。

小菜:这样设计好后

  1.如果是再加满500返200,则客户端加一个选项即可

  2.如果再出新的促销方式,如满100积分10分,则再出一个子类分支即可,不会影响之前代码

大鸟:不错,简单工厂模式看似已经解决了这个问题,但是只是解决了对象的创建,从实际情况出发,工厂本身就包含了多种收费模式,可能经常性地更改打折额度和返回额度,那么每次都要重新改代码重新部署,那还有没有更好的方法来解决呢?

试用策略模式

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

什么是算法:从上面的例子来看,打折和返利都是一种算法

为什么用策略模式:用工厂生成算法对象,这没有错,但算法本身是一种策略,最重要的是算法是随时可以相互替换的,这就是变化点,而封装变化点是面向对象的一种很重要的方式

策略模式简要实现:

抽象算法类(定义支持的所有算法的公共接口):

public abstract class Strategy {
    public abstract void AlgorithmInterface();
}

具体算法A:

public class ConcreteStrategyA extends Strategy {
    @Override
    public void AlgorithmInterface() {
    }
}

具体算法B:

public class ConcreteStrategyB extends Strategy {
    @Override
    public void AlgorithmInterface() {
    }
}

Context:用于对Strategy对象的引用

public class Context {
    Strategy strategy;

    /**
     * 初始化时传入的策略
     * @param strategy
     */
    public Context(Strategy strategy) {
        this.strategy = strategy;
    }

    /**
     * 根据具体策略对象,调用其算法的方法
     */
    public void ContextInterface() {
        strategy.AlgorithmInterface();
    }
}

客户端代码:

public class StrategyCashOperation {
    public static void main(String[] args) {
        Context context;

        context = new Context(new ConcreteStrategyA());
        context.ContextInterface();

        context = new Context(new ConcreteStrategyB());
        context.ContextInterface();
    }
}

小菜尝试将策略模式融入收银系统

其实正常收费,返利,满减都是一种具体策略,AbstractCashierSystem是抽象策略,现在只要加入Context类,再改下客户端即可:

添加CashContext类:

public class CashContext {
    private AbstractCashierSystem cs;

    public CashContext(AbstractCashierSystem cs) {
        this.cs = cs;
    }

    public BigDecimal GetResult(BigDecimal money) {
        return cs.acceptCash(money);
    }
}

客户端代码调整:

@Slf4j
public class CashOperation {
    public static void main(String[] args) {
        BigDecimal totalPrice;
        CashContext cs = null;
        BigDecimal money;
        //客户端传入策略对象type
        String type = "正常收费";
        switch (type) {
            case "正常收费":
                cs = new CashContext(new CashNormal());
                break;
            case "满300返100":
                cs = new CashContext(new CashReturn(new BigDecimal("300"), new BigDecimal("100")));
                break;
            case "打8折":
                cs = new CashContext(new CashRebate(DiscountTypeEnum.DISCOUNT_EIGHTY.getMessage()));
                break;
        }
        //客户端传入金额500
        money = new BigDecimal("500");
        totalPrice = cs.GetResult(money);
        log.info("总额:{}", totalPrice);
    }
}

大鸟:策略模式用进去了,但是在客户端判断策略模式,不是又走了之前的老路么,怎么讲判断转移?试试策略模式和工厂模式的结合

改造后的CashContext:

public class CashContext {
    private AbstractCashierSystem cs;

    public CashContext(String type) {
        switch (type) {
            case "正常收费":
                CashNormal cs0 = new CashNormal();
                cs = cs0;
                break;
            case "满300返100":
                CashReturn cs1 = new CashReturn(new BigDecimal("300"), new BigDecimal("100"));
                cs = cs1;
                break;
            case "打8折":
                CashRebate cs2 = new CashRebate(DiscountTypeEnum.DISCOUNT_EIGHTY.getMessage());
                cs = cs2;
                break;
        }
    }

    public BigDecimal GetResult(BigDecimal money) {
        return cs.acceptCash(money);
    }
}

客户端改造:

@Slf4j
public class CashOperation {
    public static void main(String[] args) {
        //客户端传入策略对象type
        String type = "正常收费";
        CashContext cs = new CashContext(type);

        //客户端传入金额500
        BigDecimal money = new BigDecimal("500");

        //通过对Context的GetResult方法的调用,可以得到收取费用的结果,让具体算法与客户进行了格隔离
        BigDecimal totalPrice = cs.GetResult(money);
        log.info("总额:{}", totalPrice);
    }
}

思考:原来简单工厂模式并非只有建立一个工厂类的做法,也可以这样做,那么简单工厂模式和上面两种模式的结果到底有什么不同呢?

简单工厂模式:

AbstractCashierSystem cs = CashFactory.createCashAccept("满300返100");

两者结合:

CashContext cs = new CashContext(type);

答:可以看出,简单工厂模式要识别AbstractCashierSystem和CashFactory两个类,而两种模式结合后,只用识别CashContext一个类就行了

      这样的好处 --> 耦合度更低,我们在客户端实例化的是CashContect的对象,调用的是CashContext的getResult方法,这使得具体的收费算法与客户端彻底分离

策略模式解析

1.什么是策略模式:

  (1)策略模式是一种定义一系列算法的方法

  (2)从概念来看,所有算法完成工作相同只是实现不同,策略模式可以以相同的方式调用所有算法,减少了算法类与使用算法类之间的耦合

2.有什么好处:

  (1)解耦,如上面所说

  (2)策略模式的Strategy类层次为Context定义了一系列可重用的算法或行为,继承有助于析取出这些算法的公共功能,比如这里的Strategy类是:AbstractCashierSystem,然后在Context里定义了getResult()的方法,这样所有继承了AbstractCashierSystem的子类,都可以用Context里的getResutl()方法

  (3)简化了单元测试,每个算法都有自己的类,可以通过自己的接口单独测试

 3.小结:

  策略模式封装了变化。它可以用来封装任何类型的规则,所以只要在分析过程中遇到需要不同时间应用不同的业务规则,就可以考虑使用策略模式处理这种变化的可能性。

原文地址:https://www.cnblogs.com/wencheng9012/p/13365500.html