策略模式

策略模式

案列

现在的生活中在线支付给我们的生活带来了极大的方便,出门可以不用带钱,只要带上手机出门就可以消费了。而支付方式也是多种多样的,可以选择支付宝,微信,银行卡等多种支付方式。张三所在公司最近就需要根据用户选择的支付方式,去调用不同的支付接口对账户进行扣款,下面是他写的代码:

1.直接就是消费者类:

// 消费者类
public class Consumer {
    // 设置默认支付方式
    private String payType = "AliPay";

    public void pay(int money) {
        if ("AliPay".equals(payType)) {
            System.out.println("使用支付宝支付~~");
            // 调用支付宝支付接口。。。
            System.out.println("扣款成功,消费:" + money + "元");
        } else if ("WeChatPay".equals(payType)) {
            System.out.println("使用微信支付~~");
            // 调用微信支付接口。。。
            System.out.println("扣款成功,消费:" + money + "元");
        } else if ("BankCardPay".equals(payType)) {
            System.out.println("使用银行卡支付~~");
            // 调用银行卡支付接口。。。
            System.out.println("扣款成功,消费:" + money + "元");
        }
    }

    public void setPayType(String payType) {
        this.payType = payType;
    }
}

2.客户端使用:

public class Main {
    public static void main(String[] args) {
        Consumer consumer = new Consumer();
        int money = 100;
        consumer.pay(money);
        consumer.setPayType("WeChatPay");
        consumer.pay(money);
        consumer.setPayType("BankCardPay");
        consumer.pay(money);
    }
}

3.使用结果:

使用支付宝支付~~
扣款成功,消费:100元
使用微信支付~~
扣款成功,消费:100元
使用银行卡支付~~
扣款成功,消费:100元

张三通过在pay()方法中判断用户选择的支付方式分别调用不用的支付接口,最终完成的任务。但是代码中一个很明显的缺点就是pay()方法非常庞大,它包含了支付接口的实现代码,使得方法会很长。同时方法中的if...else...语句如果在扩展其他支付方式时会有更多的判断。还有就是方法中的调用接口代码不能给其他需要调用支付接口的方法进行复用。基于此张三很快想到了可以使用策略模式来对代码进行改进。

模式介绍

策略模式作为一种软件设计模式,指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。比如每个人都要“交个人所得税”,但是“在美国交个人所得税”和“在中国交个人所得税”就有不同的算税方法。

在策略模式中,我们可以定义一些独立的类来封装不同的算法,每一个类封装一种具体的算法,在这里,每一个封装算法的类我们都可以称之为一种策略(Strategy),为了保证这些策略在使用时具有一致性,一般会提供一个抽象的策略类来做规则的定义,而每种算法则对应于一个具体策略类。

角色构成

  • Context(环境类):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。
  • Strategy(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。
  • ConcreteStrategy(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。

UML类图

策略模式 UML 类图

代码改造

下面张三就通过策略模式对代码进行改造:

1.首先定义出抽象的策略接口:

// 抽象策略接口,抽象策略类角色
public interface Pay {
    void deduction(int money);
}

2.三个具体的策略类:

支付宝:

// 支付宝支付,具体策略类角色
public class AliPay implements Pay {
    @Override
    public void deduction(int money) {
        System.out.println("使用支付宝支付~~");
        // 调用支付宝支付接口。。。
        System.out.println("扣款成功,消费:" + money + "元");
    }
}

微信支付

// 微信支付,具体策略类角色
public class WeChatPay implements Pay {
    @Override
    public void deduction(int money) {
        System.out.println("使用微信支付~~");
        // 调用微信支付接口。。。
        System.out.println("扣款成功,消费:" + money + "元");
    }
}

银行卡支付

// 银行卡支付,具体策略类角色
public class BankCardPay implements Pay {
    @Override
    public void deduction(int money) {
        System.out.println("使用银行卡支付~~");
        // 调用银行卡支付接口。。。
        System.out.println("扣款成功,消费:" + money + "元");
    }
}

3.消费者类充当环境类角色:

// 消费者类,环境类角色
public class Consumer {
    // 设置默认支付方式
    private Pay pay = new AliPay();

    public void pay(int money) {
        pay.deduction(money);
    }

    // 切换支付方式
    public void setPay(Pay pay) {
        this.pay = pay;
    }
}

4.客户端使用:

public class Main {
    public static void main(String[] args) {
        Consumer consumer = new Consumer();
        int money = 100;
        consumer.pay(money);
        consumer.setPay(new WeChatPay());
        consumer.pay(money);
        consumer.setPay(new BankCardPay());
        consumer.pay(money);
    }
}

5.使用结果:

使用支付宝支付~~
扣款成功,消费:100元
使用微信支付~~
扣款成功,消费:100元
使用银行卡支付~~
扣款成功,消费:100元

经过改造后使用结果和上面的结果相同,但不同的是现在的支付方式可以很好的进行扩展了。只需要新建一个具体支付策略类实现支付接口,使用时选择新建的支付接口就可以了。

代码应用

策略模式在开发中应用非常广泛,常见的就是比较器Comparator接口和java.awt.Container,下面简单分析一下在 java 容器布局中的应用。

1.首先是如下案列代码:

public class Main {
    public static void main(String[] args) {
        // 创建 JFrame 实例
        JFrame jf = new JFrame("layout example");
        // 设置宽高
        jf.setSize(500, 400);
        // 设置在窗口中间打开
        jf.setLocationRelativeTo(null);
        // 设置默认关闭操作
        jf.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        // 边界布局
        // 默认为0,0;水平间距10,垂直间距5
        JPanel borderLayoutPanel = new JPanel(new BorderLayout(10, 5));
        TitledBorder titledBorder = new TitledBorder("BorderLayout");
        borderLayoutPanel.setBorder(titledBorder);
        JButton north = new JButton("东");
        JButton south = new JButton("南");
        JButton east = new JButton("西");
        JButton west = new JButton("北");
        JButton middle = new JButton("中");
        borderLayoutPanel.add(north, BorderLayout.EAST);
        borderLayoutPanel.add(south, BorderLayout.SOUTH);
        borderLayoutPanel.add(east, BorderLayout.WEST);
        borderLayoutPanel.add(west, BorderLayout.NORTH);
        borderLayoutPanel.add(middle, BorderLayout.CENTER);
        // 将面板添加到窗体中
        jf.add(borderLayoutPanel, BorderLayout.WEST);

        // 变换布局按钮
        JButton changeToGridLayout = new JButton("changeToGridLayout");
        // 设置监听器
        changeToGridLayout.addActionListener(e -> {
            // 将边界布局改变为网格布局
            borderLayoutPanel.setLayout(new GridLayout(2, 3, 10, 5));
            titledBorder.setTitle("GridLayout");
            // 更新之后界面才会发生改变
            borderLayoutPanel.updateUI();
        });
        // 将按钮添加进窗体中
        jf.add(changeToGridLayout, BorderLayout.EAST);

        // 设置界面可见
        jf.setVisible(true);
    }
}

2.演示动图

strategy-awt

可以看到,我们通过修改borderLayoutPanel.setLayout(new GridLayout(2, 3, 10, 5))方法可以动态的设置界面的布局方式,除了设置为边界布局和网格布局外,还可以设置为流式布局(FlowLayout)、盒子布局(BoxLaYout)和空布局(null)等。下面是他们之间的 UML 类图关系:

awt 容器布局 UML 类图

通过源码我们可以看到,setLayout(LayoutManager mgr)方法不是JPanel类中的方法,而是父类Container中的方法,它扮演了环境类的角色,具体代码为:

public class Container extends Component {
    // 抽象布局对象
    LayoutManager layoutMgr;
    // 通过 setLayout() 方法修改
    public void setLayout(LayoutManager mgr) {
        layoutMgr = mgr;
        invalidateIfValid();
    }
}

而这里的抽象策略者角色就由LayoutManager接口扮演,而具体策略类有边界布局BorderLayout、流式布局FlowLayout、网格布局GridLayout、盒子布局BoxLayout等。在使用时设置不同的布局对象,就会呈现不同的展示效果。

总结

主要优点

  • 策略模式提供了对“开闭原则”的完美支持,用户可以在不修改原有系统的基础上选择算法或行为,也可以灵活地增加新的算法或行为。
  • 策略模式提供了管理相关的算法族的办法。策略类的等级结构定义了一个算法或行为族,恰当使用继承可以把公共的代码移到抽象策略类中,从而避免重复的代码。
  • 策略模式提供了一种可以替换继承关系的办法。如果不使用策略模式,那么使用算法的环境类就可能会有一些子类,每一个子类提供一种不同的算法。但是,这样一来算法的使用就和算法本身混在一起,不符合“单一职责原则”,决定使用哪一种算法的逻辑和该算法本身混合在一起,从而不可能再独立演化;而且使用继承无法实现算法或行为在程序运行时的动态切换。
  • 使用策略模式可以避免多重条件选择语句。多重条件选择语句不易维护,它把采取哪一种算法或行为的逻辑与算法或行为本身的实现逻辑混合在一起,将它们全部硬编码(Hard Coding)在一个庞大的多重条件选择语句中,比直接继承环境类的办法还要原始和落后。
  • 策略模式提供了一种算法的复用机制,由于将算法单独提取出来封装在策略类中,因此不同的环境类可以方便地复用这些策略类。

主要缺点

  • 客户端必须知道所有的策略类,并自行决定使用哪一个策略类。这就意味着客户端必须理解这些算法的区别,以便适时选择恰当的算法。换言之,策略模式只适用于客户端知道所有的算法或行为的情况。
  • 策略模式将造成系统产生很多具体策略类,任何细小的变化都将导致系统要增加一个新的具体策略类。
  • 无法同时在客户端使用多个策略类,也就是说,在使用策略模式时,客户端每次只能使用一个策略类,不支持使用一个策略类完成部分功能后再使用另一个策略类来完成剩余功能的情况。

适用场景

  • 一个系统需要动态地在几种算法中选择一种,那么可以将这些算法封装到一个个的具体算法类中,而这些具体算法类都是一个抽象算法类的子类。换言之,这些具体算法类均有统一的接口,根据“里氏代换原则”和面向对象的多态性,客户端可以选择使用任何一个具体算法类,并只需要维持一个数据类型是抽象算法类的对象。
  • 一个对象有很多的行为,如果不用恰当的模式,这些行为就只好使用多重条件选择语句来实现。此时,使用策略模式,把这些行为转移到相应的具体策略类里面,就可以避免使用难以维护的多重条件选择语句。
  • 不希望客户端知道复杂的、与算法相关的数据结构,在具体策略类中封装算法与相关的数据结构,可以提高算法的保密性与安全性。

参考资料

  • 大话设计模式
  • 设计模式Java版本-刘伟

本篇文章github代码地址:https://github.com/Phoegel/design-pattern/tree/main/strategy
转载请说明出处,本篇博客地址:https://www.jianshu.com/p/2e0c3856b49f

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335