zoukankan      html  css  js  c++  java
  • 策略模式

    策略模式

    案列

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

    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.cnblogs.com/phoegel/p/14248318.html

  • 相关阅读:
    sys.exc_info()方法:获取异常信息
    tempfile模块:生成临时文件和临时目录
    fnmatch模块:用于文件名的匹配
    pathlib模块用法详解
    linecache模块 随机读取文件指定行
    fileinput模块:逐行读取多个文件
    asyncio异步IO--协程(Coroutine)与任务(Task)详解
    Python中协程异步IO(asyncio)详解
    删除某个时间段之前的文件
    Mac入门--如何使用brew安装多个PHP版本
  • 原文地址:https://www.cnblogs.com/phoegel/p/14248318.html
Copyright © 2011-2022 走看看