大话设计模式——策略模式

时间:2022-08-13 22:01:21

大话设计模式——策略模式

需求

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

简单实现

利用Java的GUI编程,作出一个可视化界面如下。这里利用了IntelliJ IDEA的GUI Designer来进行界面功能编写,会用的话还是挺方便的。

![](http://oryvttsjs.bkt.clouddn.com/myblog/_image/2017-07-10-15-08-26.jpg)业务逻辑还是比较简单的,主要就是确定按钮和重置按钮的监听事件。源码配合注释如下:
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

/**
* Created by Mr.sorrow on 2017/6/30.
*/

public class Market {
private JButton confirm; //确定按钮
private JButton reset; //重置按钮
private JTextField price; //单价文本框
private JTextField num; //数量文本框
private JLabel sumLable; //总计
private JPanel mainPanel;
private JTextArea item; //商品条目
private JScrollPane itemScoller;

private double totalPrice = 0.0;

public Market() {
confirm.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {

double itemPrice = Double.valueOf(price.getText());
int itemNum = Integer.valueOf(num.getText());
totalPrice += itemPrice * itemNum;
item.setText(item.getText() + "单价:" + price.getText() + ",数量:" + num.getText() + ",合计:" + itemPrice * itemNum + "\n");
sumLable.setText(totalPrice + "");
}
});

reset.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
price.setText("0.00");
num.setText("0");
item.setText("");
sumLable.setText("0.00");
totalPrice = 0.0;
}
});
}

public static void main(String[] args) {
JFrame frame = new JFrame("Market");
frame.setContentPane(new Market().mainPanel);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setVisible(true);
}
}

反思

这样一个简单的收银软件,当某一天商品价格计算方式变了,比如打折、优惠券等等,我们必须手动更改代码才能够继续正确的计算。当不同的节日可能需要不同的折扣,优惠券的大小也每个人不尽相同。参照第一讲的简单工厂模式,我们可以写出各种不同的收费子类,比如打八折、满1000减300、满2件免单便宜的一件等等。
仔细想想,这样优化的缺点还存在一个小问题,每一种优惠方式都写一个收费子类,那么子类会越来越多,因为促销方式也会各种各样。所以,我们需要针对各种促销方式进行分类,同一类的只写一个子类。比如打五折和打八折是一类,只不过折数不同,需要我们在类的设计时将折扣数传进去,显然类的构造函数中添加参数即可轻而易举实现。

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

简单工厂实现

  1. 收费基类Charge;

    //收费抽象父类
    public abstract class Charge {
    public abstract double acceptMoney(double productMoney);
    }
  2. 正常收费子类ChargeNormal;

    //正常收费直接返回商品价格
    public class ChargeNormal extends Charge{
    @Override
    public double acceptMoney(double productMoney) {
    return productMoney;
    }
    }
  3. 折扣收费子类ChargeRebate;

    //打折返回折扣价
    public class ChargeRebate extends Charge {

    private double discount;

    public ChargeRebate(double discount) {
    this.discount = discount;
    }

    @Override
    public double acceptMoney(double productMoney) {
    return productMoney * discount;
    }
    }
  4. 满减收费子类ChargeReturn;

    //满xxx减xxx(并非每满xxx减xxx)
    public class ChargeReturn extends Charge{

    private double moneyOver;
    private double moneyReturn;

    public ChargeReturn(double moneyOver, double moneyReturn) {
    this.moneyOver = moneyOver;
    this.moneyReturn = moneyReturn;
    }

    @Override
    public double acceptMoney(double productMoney) {
    if(productMoney >= moneyOver)
    return productMoney - moneyReturn;
    else
    return productMoney;
    }
    }
  5. 收费工厂类。

    //简单工厂
    public class ChargeFactory {
    public static Charge createCharge(String chargeType){
    Charge chargeInstance = null;
    switch (chargeType) {
    case "正常收费":
    chargeInstance = new ChargeNormal();
    break;
    case "打8折":
    chargeInstance = new ChargeRebate(0.8);
    break;
    case "满100减20":
    chargeInstance = new ChargeReturn(100, 20);
    break;
    }
    return chargeInstance;
    }
    }

简单工厂这种方法实现的UML图如下,可以看到工厂类和其他收费方式类是依赖关系,依赖关系一般不是成员变量,这一点在第二讲有详细提及。

大话设计模式——策略模式

再次反思

可以看到这里利用简单工厂模式已经进行了一些优化,当商场推出新的促销政策,我们需要编写一个ChargeXxx收费子类继承自Charge,同时也需要在工厂类中添加一个switch分支用来生成实例对象。商场的频繁更换促销政策,我们需要不停的改动工厂类并且编译部署,这一点其实并不是很好。这么一想,第一讲的计算器程序是不是也存在这个问题?我猜大概是因为计算器运算符一共也就那么多,并不是随便自定义的,所以简单工厂模式问题不大。

面对算法的时常变动,应该有更好的办法(其他的设计模式)。

策略模式

策略模式(Strategy):它定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式的算法变化,不会影响到算法使用的客户。
商场收银时如何促销,用打折还是返利,其实都是一些算法,用工厂来生成算法对象,这并没有错,但算法本身只是一种策略,最重要的是这些算法是随时可能互相替换的。这些就是变化点,而封装变化点是面向对象的一种重要的思维方式。
策略模式的UML图如下:

大话设计模式——策略模式

  • Strategy:所有策略的基类
  • StrategyA、StrategyB、StrategyC:三个策略子类
  • Context:上下文,包含一个父类Strategy引用,指向具体的子类策略对象(聚合关系)

策略模式的代码实现:

  • 抽象算法基类(网上也有是用接口)

    public abstract class Strategy{
    public abstract void function();
    }
  • 具体算法子类(B、C类似)

    public class StrategyA extends Strategy{
    public void function(){
    ...
    }
    }
  • Context类

    public class Context{
    private Strategy strategy;
    public Context(Strategy strategy){
    this.strategy = strategy;
    }
    public void contextInterface(){
    strategy.function();
    }
    }
  • 客户端代码(GUI代码)

    public static void main(String[] args){
    Context context;
    context = new Context(new StrategyA());
    context.contextInterface();

    context = new Context(new StrategyB());
    context.contextInterface();
    }

通过策略模式,客户端代码仅仅需要Context类的实例对象,而不需要去了解任意一种策略是如何实现的。当策略的改变,客户端也不用去理会,而是Context去解决。

引入策略模式

  • 原来写的Charge相当于抽象策略,而ChargeNormal、ChargeRebate和ChargeReturn则相当于具体的策略A、B、C,这些都可以原封不动的照搬过来。

  • 需要重新实现的是Context类,代码如下:

    public class ChargeContext{
    private Charge charge;
    public ChargeContext(Charge charge){
    this.charge = charge;
    }
    public double getResult(double productMoney){
    return charge.acceptMoney(productMoney);
    }
    }
  • 客户端代码需要写switch语句来根据不同的折扣方式产生不同的折扣策略对象进而产生不同的ChargeContext对象,再通过调用getResult()方法来获得实际收银金额。

    ...
    ChargeContext cc;
    switch(String type){
    case "正常收费":
    cc = new ChargeContext(new ChargeNormal());
    break;
    case "打8折":
    cc = new ChargeContext(new ChargeRebate(0.8));
    break;
    case "满100减20":
    cc = new ChargeContext(new ChargeReturn(100, 20));
    break;
    }
    ...
    cc.getResult();

继续反思

现在,如果再增加一种折扣措施,客户端代码必然要进行更改——添加一个Switch分支,这好像又回到了第一讲出现的问题,这种问题最后是用简单工厂模式去解决的。所以,这里再巧妙的引入简单工厂模式与策略模式相结合,一定能解决这一问题。

再次实现

  • 引入简单工厂模式一定在是在Context类中实现,观察现有的代码,能动手术的地方应该就是构造函数这一块;
  • 更改后的代码如下:

    public class ChargeContext{
    private Charge charge;
    public ChargeContext(String type){
    switch(type){
    case "正常收费":
    charge = new ChargeNormal();
    break;
    case "打8折":
    charge = new ChargeRebate(0.8);
    break;
    case "满100减20":
    charge = new ChargeReturn(100, 20);
    break;
    }
    }
    public double getResult(double productMoney){
    return charge.acceptMoney(productMoney);
    }
    }
  • 客户端代码现在则相对来说明显简单的多,从头至尾仅仅需要和ChargeContext有关,而任何策略类统统不用去管。

    ChargeContext cc = new ChargeContext("打8折");
    cc.getResult();

最后反思

  • 其实策略模式与简单工厂模式有一些共同的地方,本质都是利用面向对象语言的特性进行封装解耦。
  • 在单纯使用策略模式时,客户端仍然需要判断何种情形采用何种算法,然后转给Context对象,客户端仍然顶着选择判断的压力;当引入简单工厂模式,压力则转移给了Context类,这样的设计思路是完全合理的。
  • 回想我们在反思为什么要引入策略模式,原因是当时觉得如果商场再推出一个促销政策,需要在工厂类中添加一个switch分支用来生成实例对象。商场的频繁更换促销政策,我们需要不停的改动工厂类并且编译部署。想想引入策略模式,这些工作好像我们都还需要做,可能只是对象变了,好像并没有解决这一问题。书中也在最后提及这一点,Java的反射会解决这一问题。
  • 那么引入策略模式究竟好在哪?其实还是体现在客户端代码里,简单工厂需要认识俩个类:Charge和ChargeFactory,而策略模式仅需要客户端认识ChargeContext一个类即可,耦合度更低。
  • 在平时开发中,如果遇到不同时间(情形)采取不同的业务规则,都可以尝试使用策略模式来处理这些变化的可能性。
  • 策略模式简化了单元测试,因为每个算法都有自己的类,可以通过自己的接口单独测试。每个算法可以保证没有错误,修改其中一个也不会影响其他算法。


个人公众号:每日推荐一篇技术博客,坚持每日进步一丢丢…欢迎关注,想建个微信群,主要讨论安卓和Java语言,一起打基础、用框架、学设计模式,菜鸡变菜鸟,菜鸟再起飞,愿意一起努力的话可以公众号留言,谢谢…

大话设计模式——策略模式