【代码洁癖症】第2回-策略模式

【代码洁癖症】第2回-策略模式

序言

在一个宁静的午后,我有幸拜读了程杰大鸟的《大话设计模式》

觉得这是一本不可多得的好书

奈何里面都是C++代码写的示例,对于学Java的同学不是很友好

于是想将书中的核心提炼出来并结合Java示例与大家分享

并且加入一些我曾在生产环境下的应用来“学以致用”

这是第一次开始写CSDN专栏,内容会持续更新,感兴趣的小伙伴可以来个三连

本人水平有限,难免会有不足之处,希望大佬们不吝赐教!

面试第二轮

前情回顾

张三顺利通过了第一轮面试,也步入了设计模式的大门

通过几次修改自己代码结构

让自己的代码变得更容易扩展和维护

今天,他西装革履又走进了面试官的办公室

面试官: “张三你好,我们又见面了”

张三: “废话少说,我们开始面试吧,这次是什么题目?”

面试官: “我们公司今天来了个大客户——沃尔玛,要我们公司给他们开发一个收银程序,这样吧,你写个简单的收银程序给我看看”

张三: “So easy!别急面试官,我这就开始写”

张三第一版商场收银程序

public class Market01 {
    private static double totalPrice = 0.0;

    public static void main(String[] args) {
        while (true){
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入单价(元)");
            String singlePrice = scanner.nextLine();
            System.out.println("请输入数量");
            String count = scanner.nextLine();

            totalPrice += Double.valueOf(singlePrice)*Double.valueOf(count);
            System.out.println("总价为:"+totalPrice+"元");
        }
    }
}

张三:“面试官你看,我的程序还能不断累加总价”

在这里插入图片描述

面试官笑了一下

面试官: “现在商场搞活动,所有商品打8折,你怎么办?”

张三: “这还不简单,每次计算总价之前都乘上一个0.8就OK了,看我的”

修改后的商场收银程序

public class Market01 {
    private static double totalPrice = 0.0;

    public static void main(String[] args) {
        while (true){
            Scanner scanner = new Scanner(System.in);
            System.out.println("请输入单价(元)");
            String singlePrice = scanner.nextLine();
            System.out.println("请输入数量");
            String count = scanner.nextLine();
            /*累加之前乘以0.8(打八折)*/
            totalPrice += (Double.valueOf(singlePrice)*Double.valueOf(count))*0.8;
            System.out.println("总价为:"+totalPrice+"元");
        }
    }
}

效果图

在这里插入图片描述

面试官: “后面的‘000004’和‘000005’是什么鬼?”

张三: “这个我知道,这是Java中的精度丢失问题”

面试官: “你简单说说”

张三: “精度丢失是因为十进制的 double 类型的数据在进行计算的时候,计算机会先将其转换为二进制数据,然后再进行相关的运算然,而在十进制转二进制的过程中,有些十进制数是无法使用一个有限的二进制数来表达的,换言之就是转换的时候出现了精度的丢失问题,所以导致最后在运算的过程中,自然就出现了我们看到的一幕”

面试官: “回答得很好,那我们怎么去解决这个问题呢?”

张三: “BigDecimal ”

面试官: “回答得很好,我们这个问题就不深究了,来说说你写的程序: 万一商场活动结束了怎么办?难道还要改一遍代码?还有如果打七折、六折、五折怎么办?”

张三: “这个难不倒我”

增加了打折选项的商城收银程序

public class Market01 {
    private static double totalPrice = 0.0;
    private static double discount = 1.0;//默认不打折

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请选择折扣");
        System.out.println("0.不打折");
        System.out.println("1.八折");
        System.out.println("2.七折");
        System.out.println("3.六折");
        System.out.println("4.五折");
        String choice = scanner.nextLine();
        switch (choice){
            case "0":
                 // do nothing
                break;
            case "1":
                discount = 0.8;
                break;
            case "2":
                discount = 0.7;
                break;
            case "3":
                discount = 0.6;
                break;
            case "4":
                discount = 0.5;
                break;
            default:
                System.out.println("输入错误!");
                System.exit(-1);
        }
        while (true){
            System.out.println("请输入单价(元)");
            String singlePrice = scanner.nextLine();
            System.out.println("请输入数量");
            String count = scanner.nextLine();
            /*累加之前乘以打折系数*/
            totalPrice += (Double.valueOf(singlePrice)*Double.valueOf(count))*discount;
            System.out.println("总价为:"+String.format("%.2f",totalPrice)+"元");
        }
    }
}

运行效果

在这里插入图片描述

面试官: “好,现在商场又新加了一个需求: 又增加了满减活动,满300减100,满500减200,这个怎么办?”

张三欲暴起而伤人

在这里插入图片描述

面试官: “息怒息怒,这样吧,后半场我来写,首先我们用第一次面试时教你的简单工厂改造下你的代码”

简单工厂

“首先我们要知道在面向对象编程中,并不是类越多越好,类的划分是为了更好的封装,但是封装的基础就是抽象,只有对事物进行一个抽象才能分好类,我们将具有相同属性和功能的对象的抽象集合叫做类”

具体来说,就是打一折和九折只是形式不同,抽象分析后,优惠的方式(算法)应该是一个类。

现金收费抽象类CashSuper

public abstract class CashSuper {
    /**
     * @Description TODO 实际收取现金算法
     * @author LaoQin
     * @date 2020/03/13
     * @param money 原价
     * @return double 当前价
     */
    public abstract double acceptCash(double money);
}

正常收费子类CashNormal

public class CashNormal extends CashSuper {
    /**
     * @Description TODO 原价收费
     * @author LaoQin
     * @date 2020/03/13
     * @param money
     * @return double
     */
    @Override
    public double acceptCash(double money) {
        return money;
    }
}

打折收费子类CashRebate

public class CashRebate extends CashSuper{

    private  double rebate = 1.0;
    
    public CashRebate(double rebate){
        this.rebate  = rebate;
    }
    
    /**
     * @Description TODO 打折后返回打折金额
     * @author LaoQin
     * @date 2020/03/13
     * @param money
     * @return double
     */
    @Override
    public double acceptCash(double money) {
        return money*rebate;
    }
}

满减收费子类CashReturn

public class CashReturn extends CashSuper{
    private double moneyCondition = 0.0;//满
    private double moneyReturn = 0.0;//减

    public CashReturn(double moneyCondition, double returnCondition) {
        this.moneyCondition = moneyCondition;
        this.moneyReturn = returnCondition;
    }
    /**
     * @Description TODO 满减
     * @author LaoQin
     * @date 2020/03/13
     * @param money
     * @return double
     */
    @Override
    public double acceptCash(double money) {
        if(money>moneyCondition){//满足条件才能满减
            return money-moneyReturn;
        }
        return money;
    }
}

简单收费工厂CashFactory

public class CashFactory {
    /**
     * @param type 优惠分类
     * @return CashSuper
     * @Description TODO 现金收取工厂
     * @author LaoQin
     * @date 2020/03/15
     */
    public static CashSuper createCashAccept(String type) {
        CashSuper cashSuper = null;
        switch (type) {
            case "正常收费":
                cashSuper = new CashNormal();
                break;
            case "满300减100":
                cashSuper = new CashReturn(300, 100);
                break;
            case "8折":
                cashSuper = new CashRebate(0.8);
                break;
            default:
                throw new RuntimeException("输入错误");
        }
        return cashSuper;
    }
}

测试类FactoryTestApplication

import java.util.Scanner;

public class FactoryTestApplication {
    /**
     * @Description TODO 测试简单工厂实现
     * @author LaoQin
     * @date 2020/03/15
     * @param args
     * @return void
     */
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入金额");
        double money = Double.valueOf(scanner.nextLine());
        System.out.println("请选择折扣类型:");
        System.out.println("正常收费");
        System.out.println("满300减100:");
        System.out.println("8折");
        String type = scanner.nextLine();
        //创建对应收费子类
        CashSuper cashAccept = CashFactory.createCashAccept(type);


        double result = cashAccept.acceptCash(money);
        System.out.println("优惠后金额为:"+result);
    }
}

运行结果如图

在这里插入图片描述

面试官: “但是简单工厂只是为了解决对象创建的问题,商场如果要更改打折额度和返利额度就会频繁改动这个工厂,显然这不是最好的处理方式”

“我们设计模式可以大致分为以下4类”

  • 创建型
  • 结构型
  • 行为型
  • J2EE型

而工厂模式显然是属于创建型的,而我们这个问题显然不是一个创建型的问题,而是“行为型”,根据商场运营战略去动态调整我们的打折策略,所以这种场景比较适合的是“策略模式

策略模式

什么是策略模式?

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

张三: “Talk is cheap,show me the code”

面试官: “先别急,我们来看下策略模式的UML图”

在这里插入图片描述

首先有个策略父类,然后有几个不同算法类去继承并实现各自算法,乍一看和简单工厂很像

他们的区别就在于策略模式还是基于继承,但是却少了工厂类(因为不是创建型模式)

其核心就在于用一个Context(上下文)去管理维护一个对象的引用

张三: “说人话”

面试官: “概念有点抽象,我们直接看代码”

Context上下文管理类

public class Context {
    private CashSuper cashSuper;
    
    public Context(CashSuper cashSuper) {//构造方法传入具体收费策略
        this.cashSuper = cashSuper;
    }
    
    public double getResult(double money){//根据不同收费策略返回不同计算结果
        return cashSuper.acceptCash(money);
    }
}

这样我们就能忽略算法具体实现而直接调用

而我们就能使用以下代码去调用:

打8折调用示例

/**
 * @Description TODO 策略模式下打8折调用示例
 * @author LaoQin
 * @date 2020/03/15
 * @param args
 * @return void
 */
public static void main(String[] args) {
    Context context = new Context(new CashRebate(0.8));
    double result = context.getResult(100);
    System.out.println("打折后的金额为:"+result);
}

输出结果

打折后的金额为:80.0

面试官: “策略模式实现其实很简单,就是运用了OOP(面向对象程序设计)中继承特性,然后通过一个Context类的构造方法传入父类去实例化一个子类对象,调用其方法”

既然知道了策略模式,那么我们将 简单工厂和策略模式结合起来 实现

张三: “这么一说我就明白了,剩下的改造交给我来吧”

改造后的Context(加入工厂)

public class Context {
    private CashSuper cashSuper;

    public Context(String type) {//这里特别注意:传入的不再是一个对象,而是收费类型字符串
        switch (type) {
            case "正常收费":
                cashSuper = new CashNormal();
                break;
            case "满300减100":
                cashSuper = new CashReturn(300, 100);
                break;
            case "8折":
                cashSuper = new CashRebate(0.8);
                break;
            default:
                throw new RuntimeException("输入错误");
        }
    }

    public double getResult(double money){//根据不同收费策略返回不同计算结果
        return cashSuper.acceptCash(money);
    }
    
}

注意和改动的点都写到代码注释里了

调用示例

    /**
     * @Description TODO 简单工厂+策略模式测试方法
     * @author LaoQin
     * @date 2020/03/15
     * @param args
     * @return void
     */
    public static void main(String[] args) {
        Context context = new Context("8折");
        double result = context.getResult(100);
        System.out.println("打折后的金额为:"+result);

    }

这样我们会发现两种方式结合后我们在调用的时候看不到CashSuper和相关子类的影子

简单工厂会让客户端认识两个类CashSuper和CashFactory

而两种结合后客户端只需要认识Context一个类就行了

这样就降低了我们程序的耦合度

面试官: “张三你说的很好,我们继续深入解析一下‘策略模式’”

"回过头来反思一下 策略模式,策略模式是一种定义一系列算法的方法,从概念上来看,所有这些
算法完成的都是相同的工作,只是实现不同,它可以以相同的方式调用所有的算法,减少了各种算法类
与使用算法类之间的耦合[DPE]。”大鸟总结道。
“策略模式还有些什么优点?”小菜问道。
“策略模式的Strategy 类层次为Context 定义了-系列的可供重用的算法或行为。继承有助于析取
出这些算法中的公共功能[DP]。对于打折、返利或者其他的算法,其实都是对实际商品收费的一一种计算
方式,通过继承,可以得到它们的公共功能,你说这公共功能指什么?”
“公共的功能就是获得计算费用的结果GetResult,这使得算法间有了抽象的父类CashSuper。”
“对,很好。另外-一个策略模式的优点是简化了单元测试,因为每个算法都有自己的类,可以通过
自己的接口单独测试[DPE]。”
“每个算法可保证它没有错误,修改其中任-一个时也不会影响其他的算法。这真的是非常好。”

——摘自《大话设计模式》

张三通过面试

张三: “那我这算过了吗?”

面试官: “虽然你对设计模式不是很了解,但是你的学习能力还是很强的,综合考虑,给你通过吧!”

张三: “太好了,那我需要准备什么呢?”

面试官: “明天过来填个入职申请,回去看一看‘单一职责原则’,明天到公司提交一份学习报告”

张三: 好嘞!

-完-


下期预告:《张三的学习报告——单一职责原则》

注: 专栏《代码洁癖症》所有代码均已同步至github,详情请访问github主页

发布了52 篇原创文章 · 获赞 150 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/itkfdektxa/article/details/104878154