算法的封装与切换——策略模式

在软件开发中,我们也常常会遇到类似的情况,实现某一个功能有多条途径,每一条途径对应一种算法,此时我们可以使用一种设计模式来实现灵活地选择解决途径,也能够方便地增加新的解决途径。本章我们将介绍一种为了适应算法灵活性而产生的设计模式——策略模式。

24.1 电影票打折方案

Sunny软件公司为某电影院开发了一套影院售票系统,在该系统中需要为不同类型的用户提供不同的电影票打折方式,具体打折方案如下:

(1) 学生凭学生证可享受票价8折优惠;

(2) 年龄在10周岁及以下的儿童可享受每张票减免10元的优惠(原始票价需大于等于20元);

(3) 影院VIP用户除享受票价半价优惠外还可进行积分,积分累计到一定额度可换取电影院赠送的奖品。

该系统在将来可能还要根据需要引入新的打折方式。

为了实现上述电影票打折功能,Sunny软件公司开发人员设计了一个电影票类MovieTicket,其核心代码片段如下所示:

//电影票类  
class MovieTicket {  
    private double price; //电影票价格  
    private String type; //电影票类型  

    public void setPrice(double price) {  
        this.price = price;  
    }  

    public void setType(String type) {  
        this.type = type;  
    }  

    public double getPrice() {  
        return this.calculate();  
    }  

         //计算打折之后的票价  
    public double calculate() {  
                  //学生票折后票价计算  
        if(this.type.equalsIgnoreCase("student")) {  
            System.out.println("学生票:");  
            return this.price * 0.8;  
        }  
                  //儿童票折后票价计算  
        else if(this.type.equalsIgnoreCase("children") && this.price >= 20 ) {  
            System.out.println("儿童票:");  
            return this.price - 10;  
        }  
                  //VIP票折后票价计算  
        else if(this.type.equalsIgnoreCase("vip")) {  
            System.out.println("VIP票:");  
            System.out.println("增加积分!");  
            return this.price * 0.5;  
        }  
        else {  
            return this.price; //如果不满足任何打折要求,则返回原始票价  
        }  
    }  
}

编写如下客户端测试代码:

class Client {  
    public static void main(String args[]) {  
        MovieTicket mt = new MovieTicket();  
        double originalPrice = 60.0; //原始票价  
        double currentPrice; //折后价  

        mt.setPrice(originalPrice);  
        System.out.println("原始价为:" + originalPrice);  
        System.out.println("---------------------------------");  

        mt.setType("student"); //学生票  
        currentPrice = mt.getPrice();  
        System.out.println("折后价为:" + currentPrice);  
        System.out.println("---------------------------------");  

        mt.setType("children"); //儿童票  
        currentPrice = mt.getPrice();  
        System.out.println("折后价为:" + currentPrice);  
    }  
}

编译并运行程序,输出结果如下所示:

原始价为:60.0
---------------------------------
学生票:
折后价为:48.0
---------------------------------
儿童票:
折后价为:50.0

它至少存在如下三个问题:

(1) MovieTicket类的calculate()方法非常庞大,它包含各种打折算法的实现代码,在代码中出现了较长的if…else…语句,不利于测试和维护。

(2) 增加新的打折算法或者对原有打折算法进行修改时必须修改MovieTicket类的源代码,违反了“开闭原则”,系统的灵活性和可扩展性较差。

(3) 算法的复用性差。

策略模式结构并不复杂,其结构如图24-1所示:

 

● Context(环境类):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。

● Strategy(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。

● ConcreteStrategy(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。

 

在使用策略模式时,我们需要将算法从Context类中提取出来,首先应该创建一个抽象策略类,其典型代码如下所示:

abstract class AbstractStrategy {  
    public abstract void algorithm(); //声明抽象算法  
}

然后再将封装每一种具体算法的类作为该抽象策略类的子类,如下代码所示:

 

class ConcreteStrategyA extends AbstractStrategy {  
    //算法的具体实现  
    public void algorithm() {  
       //算法A  
    }  
}

其他具体策略类与之类似,对于Context类而言,在它与抽象策略类之间建立一个关联关系,其典型代码如下所示:

class Context {  
private AbstractStrategy strategy; //维持一个对抽象策略类的引用  

    public void setStrategy(AbstractStrategy strategy) {  
        this.strategy= strategy;  
    }  

    //调用策略类中的算法  
    public void algorithm() {  
        strategy.algorithm();  
    }  
}

在Context类中定义一个AbstractStrategy类型的对象strategy,通过注入的方式在客户端传入一个具体策略对象,客户端代码片段如下所示:

Context context = new Context();  
AbstractStrategy strategy;  
strategy = new ConcreteStrategyA(); //可在运行时指定类型  
context.setStrategy(strategy);  
context.algorithm();  

24.3 完整解决方案

 

+

 为了实现打折算法的复用,并能够灵活地向系统中增加新的打折方式,Sunny软件公司开发人员使用策略模式对电影院打折方案进行重构,重构后基本结构如图24-2所示:

在图24-2中,MovieTicket充当环境类角色,Discount充当抽象策略角色,StudentDiscount、 ChildrenDiscount 和VIPDiscount充当具体策略角色。完整代码如下所示:

 环境类角色:

//电影票类:环境类  
class MovieTicket {  
    private double price;  
    private Discount discount; //维持一个对抽象折扣类的引用  

    public void setPrice(double price) {  
        this.price = price;  
    }  

    //注入一个折扣类对象  
    public void setDiscount(Discount discount) {  
        this.discount = discount;  
    }  

    public double getPrice() {  
        //调用折扣类的折扣价计算方法  
        return discount.calculate(this.price);  
    }  
}  

抽象策略类:

//折扣类:抽象策略类  
interface Discount {  
    public double calculate(double price);  
}  

具体策略类:

//学生票折扣类:具体策略类  
class StudentDiscount implements Discount {  
    public double calculate(double price) {  
        System.out.println("学生票:");  
        return price * 0.8;  
    }  
}   

//儿童票折扣类:具体策略类  
class ChildrenDiscount implements Discount {  
    public double calculate(double price) {  
        System.out.println("儿童票:");  
        return price - 10;  
    }  
}   

//VIP会员票折扣类:具体策略类  
class VIPDiscount implements Discount {  
    public double calculate(double price) {  
        System.out.println("VIP票:");  
        System.out.println("增加积分!");  
        return price * 0.5;  
    }  
}

为了提高系统的灵活性和可扩展性,我们将具体策略类的类名存储在配置文件中,并通过工具类XMLUtil来读取配置文件并反射生成对象,XMLUtil类的代码如下所示:

import javax.xml.parsers.*;  
import org.w3c.dom.*;  
import org.xml.sax.SAXException;  
import java.io.*;  
class XMLUtil {  
//该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象  
    public static Object getBean() {  
        try {  
            //创建文档对象  
            DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();  
            DocumentBuilder builder = dFactory.newDocumentBuilder();  
            Document doc;                             
            doc = builder.parse(new File("config.xml"));   

            //获取包含类名的文本节点  
            NodeList nl = doc.getElementsByTagName("className");  
            Node classNode=nl.item(0).getFirstChild();  
            String cName=classNode.getNodeValue();  

            //通过类名生成实例对象并将其返回  
            Class c=Class.forName(cName);  
            Object obj=c.newInstance();  
            return obj;  
        }     
        catch(Exception e) {  
            e.printStackTrace();  
            return null;  
        }  
    }  
}

在配置文件config.xml中存储了具体策略类的类名,代码如下所示:

<?xml version="1.0"?>  
<config>  
    <className>StudentDiscount</className>  
</config>

编写如下客户端测试代码:

class Client {  
    public static void main(String args[]) {  
        MovieTicket mt = new MovieTicket();  
        double originalPrice = 60.0;  
        double currentPrice;  

        mt.setPrice(originalPrice);  
        System.out.println("原始价为:" + originalPrice);  
        System.out.println("---------------------------------");  

        Discount discount;  
        discount = (Discount)XMLUtil.getBean(); //读取配置文件并反射生成具体折扣对象  
        mt.setDiscount(discount); //注入折扣对象  

        currentPrice = mt.getPrice();  
        System.out.println("折后价为:" + currentPrice);  
    }  
}

编译并运行程序,输出结果如下:

原始价为:60.0
---------------------------------
学生票:
折后价为:48.0

如果需要更换具体策略类,无须修改源代码,只需修改配置文件。

 

 

 

 

 

 

 

 

 

  

 

 

 

 

 

 

 

 

 

在客户端代码中只需注入一个具体策略对象,可以将具体策略类类名存储在配置文件中,通过反射来动态创建具体策略对象,从而使得用户可以灵活地更换具体策略类,增加新的具体策略类也很方便。策略模式提供了一种可插入式(Pluggable)算法的实现方案。

+

 

 

posted @ 2019-01-26 16:05  Archer-Fang  阅读(1090)  评论(0)    收藏  举报