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

一、引言


       在上一篇博客中我们介绍了工厂模式中的简单工厂和工厂方法模式,它们较好的解决了对象的创建问题,降低了代码各模块之间的耦合性,方便了系统的改进和升级。但是,在一些需要经常改动的应用中,工厂模式需要频繁改动工厂类,以致代码需要频繁编译和部署,所以工厂模式在此场景下不是最好的方式。

       考虑一个商场结算系统,由于商场经常举行不同的促销活动,如打折、满减等,其结算方案需要经常改动。现在先分析一下这个例子,其实每一种结算方案都是一种算法,或者说是一种解决问题的策略,而且这些策略要被频繁的替换,这就是变化点,而封装变化正是面向对象的一种很重要的思维方式策略模式就是专门用于封装策略的变化的


二、分析


       策略模式将每一中策略封装起来,并从中抽象出一个策略父类,从而构建了一个策略家族,从而使这个策略家族中的每一种策略都可以互相替换,并且替换过程不会对客户端产生影响。

       在策略模式中每一中策略都被封装成了一个具体策略类ConcretStrategy,这些类都继承自一个策略父类Strategy,在策略父类中定义了执行策略的接口;该模式还将策略的更换封装成了一个上下文类Context,该类维护了一个Strategy类型的引用,用不同的ConcretStrategy进行配置,并且定义了一个供客户端调用策略的接口方法ContextInterface()。下图即为策略模式的UML图。



图-1 策略模式UML图


三、实现


       下面我们将以商场收费系统为例,来说明策略模式的具体实现。首先要定义一个抽象策略类CashStrategy,用以定义所有收费策略的公共接口receiveMoney(doubic money),在所有具体收费策略子类中将重写该方法。

<span style="font-size:14px;">/**
 * 收银系统的终极父类,所有用于计算收银总额的算法子类均继承自该类。
 */
abstract class CashStrategy {
    
    public abstract double receiveMoney(double money);
    
}/*CashStrategy*/</span>
       接着,我们定义了三种收费策略:正常收费、打折收费和满减返现收费,它们均继承抽象策略类并重写了receiveMoney方法已实现各自的功能。
/**
 * 正常收费类
 */
class NormalCash extends CashStrategy {

    @Override
    public double receiveMoney(double money) {
        return money;
    }// receiveMoney
        
}/*NormalCash*/

/**
 * 打折收费类
 */
class RebateCash extends CashStrategy {

    public double rebate;
    
    /**
     * RebateCash构造函数,确定打折额度
     * @param rebate 打折额度
     */
    public RebateCash(double rebate) {
        this.rebate = rebate;
    }//constructor
    
    @Override
    public double receiveMoney(double money) {
        return (money * rebate);
    }// receiveMoney
    
}/*RebateCash*/

/**
 * 返现收费类
 */
class ReturnCash extends CashStrategy {

    public double limite;
    public double count;
    
    /**
     * ReturnCash构造函数,确定满减的上限和满减的额度。
     * @param limite 满减上限
     * @param count  满减额度
     */
    public ReturnCash(double limite, double count) {
        super();
        this.limite = limite;
        this.count = count;
    }// constructor

    @Override
    public double receiveMoney(double money) {
        double temp = 0;
        temp = money - Math.floor(money / limite) * count;
        return temp;
    }// receiveMoney
    
}/*ReturnCash*/

       接下来就要定义上下文类Context,该类是策略模式中最核心的一个类。该类维护了一个终极父类的引用,该成员变量在创建context类时被指向了一个具体策略子类对象。这样在策略发生变化时,就不必对Contex类的代码进行更改,而只需要在客户端创建Contex类时,传递不同的具体策略子类对象即可,具体实现请看代码。

/**
 * 上下文类,该类维护了一个终极父类的引用,有一个调用子类方法的接口函数。
 */
class CashContext {
    
    protected CashStrategy cs;  // 维护了一个抽象策略类型的引用
    
    /**
     * 在构造上下文类时就传递给cs一个具体策略类的对象实例,这样在收费策略发生变化时就不需要
     * 改动Context类,只需要在构造Context对象时传入不同的具体策略类对象即可。
     * @param cs 具体的收费策略
     */
    public CashContext(CashStrategy cs) {
        this.cs = cs;
    }// constructor
    
    /**
     * 通过该方法,用户可以执行具体策略类中的功能
     * @param money
     * @return
     */
    public double cashInterface(double money) {
        return cs.receiveMoney(money);
    }// cashInterface
    
}/*CashContex*/
       最后是客户端程序
/**
 * 策略模式例程,编写一个超市收银系统的代码,能够根据商品的价格、数量
 * 来计算收银总额,并将商品列表打印出来。注意预留商品打折和返现接口。
 */
public class StrategyPatternDemo {   
     
    /**
     * 客户端程序入口,首先要根据需要创建Context类,然后调用具体策略的功能接口计算最终的收
     * 费金额
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        double total = 0;
        double actualBill = 0;
        CashContext context = null;
        LinkedList<String> prodList = new LinkedList<>();
        
        // 根据要求的收费策略创建Context类
        System.out.print("请输入收费策略(1:正常收费;2:打八折;3:满300返100):");
        String stgy = sc.nextLine();
        switch(stgy) {
        case "1": case "正常收费": 
            context = new CashContext(new NormalCash());
            break;
        case "2": case "打八折":
            context = new CashContext(new RebateCash(0.8));
            break;
        case "3": case "慢300返100":
            context = new CashContext(new ReturnCash(300, 100));
            break;
        default:
            sc.close();
            throw new Exception("不正确的收费策略!");            
        }// switch
        
        // 输入商品列表并计算收费金额
        int ID = 1;
        System.out.print("请输入商品价格:");
        String price = sc.nextLine();
        while(!price.equals("over")) {
            double single = Double.parseDouble(price);
            int num = 0;
            System.out.print("请输入商品数量:");
            num = Integer.parseInt(sc.nextLine());
            prodList.add((ID++) + "\t\t" + price + "\t\t" + num);
            
            total = single * num + total;
            System.out.print("请输入商品价格:");
            price = sc.nextLine();
        }// while
        actualBill = context.cashInterface(total);
        sc.close();
        
        // 显示
        System.out.println("序号" + "\t\t" + "单价" + "\t\t\t" + "数量");
        for(String str : prodList)
            System.out.println(str);
        System.out.println("总额:" + String.format("%.2f", total));
        System.out.println("应收金额:" + String.format("%.2f", actualBill));
    }// main
    
}/*StrategyPatternDemo*/
执行结果:



四、策略模式与简单工厂结合


      从上面的例子我们可以看到,策略模式封装了策略的变化,但是却又把判断分支语句放回了客户端中,解决这个问题的思路是将策略模式与简单工厂(点击转到简单工厂模式)结合起来,Context类的构造方法写成简单工厂类中ceatInstance()方法的形式。

       下面的CashFactoryContext继承了CashContext类,只是对构造方法进行了升级,将客户端中的判断分支语句转移到了Context类的构造方法中。

class CashFactoryContext extends CashContext {
    
    public CashFactoryContext(String strategy) throws Exception {
        super(null);
        switch(strategy) {
        case "1": case "正常收费": 
            cs = new NormalCash();
            break;
        case "2": case "打八折":
            cs = new RebateCash(0.8);
            break;
        case "3": case "慢300返100":
            cs = new ReturnCash(300, 100);
            break;
        default:
            throw new Exception("不正确的收费策略!");            
        }// switch
    }// constructor
    
}/*CashFactoryContext*/
客户端代码:

public class StrategyFactoryDemo {

    /**
     * 客户端程序入口,可以看出与策略模式相比,客户端程序里少了判断语句,并且用户仅用到了
     * CashFactoryContext一个类
     * @param args
     * @throws Exception
     */
    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        double total = 0;
        double actualBill = 0;
        CashFactoryContext factoryContext = null;
        LinkedList<String> prodList = new LinkedList<>();
        
        // 根据要求的收费策略创建Context类
        System.out.print("请输入收费策略(1:正常收费;2:打八折;3:满300返100):");
        String stgy = sc.nextLine();
        factoryContext = new CashFactoryContext(stgy);
        
        // 输入商品列表并计算收费金额
        int ID = 1;
        System.out.print("请输入商品价格:");
        String price = sc.nextLine();
        while(!price.equals("over")) {
            double single = Double.parseDouble(price);
            int num = 0;
            System.out.print("请输入商品数量:");
            num = Integer.parseInt(sc.nextLine());
            prodList.add((ID++) + "\t\t" + price + "\t\t" + num);
            
            total = single * num + total;
            System.out.print("请输入商品价格:");
            price = sc.nextLine();
        }// while
        actualBill = factoryContext.cashInterface(total);
        sc.close();
        
        // 显示
        System.out.println("序号" + "\t\t" + "单价" + "\t\t\t" + "数量");
        for(String str : prodList)
            System.out.println(str);
        System.out.println("总额:" + String.format("%.2f", total));
        System.out.println("应收金额:" + String.format("%.2f", actualBill));
    }// main
    
}/*StrategyFactoryDemo*/
执行结果:


读到这里大家可能已经产生了一些疑惑,似乎修改后的策略模式与简单工厂点击转到工厂模式的区别不大,但是实际上,策略模式依然与工厂模式存在一个重要的区别。在简单工厂模式中,客户端要想获得一个子类对象,就必须要知道抽象功能父类、工厂类和具体功能子类共三个类,但是在改进的策略模式中,具体策略子类的变换和创建都被封装在了Context类中,与客户端隔离开来,在客户端中只需要用到抽象策略类和Context类两个类即可实现所需的功能,进一步降低了耦合。


五、总结

       策略模式是一种定义一系列策略(算法)的方法,这些策略完成的都是同一类工作,只是实现的方式不同,策略模式以相同的方式调用所有策略,降低了使用策略的类与各具体策略类之间的耦合

       在基本的策略模式中,选择所用具体策略的职责有客户端承担,并没有解除客户端需要选择判断的压力,将策略模式与简单工厂模式结合后,选择具体策略的操作被封装到了Context类中,减轻了客户端的职责。但是在添加或删除新的策略子类时,还还需要修改Context构造方法中的判断分支结构,违背了开放封闭原则。当然,这一缺点可以通过Java反射机制得以解决,我们将在后续的博文中进行介绍,读者此时可以先不必深究。



猜你喜欢

转载自blog.csdn.net/u013916933/article/details/51404608
今日推荐