设计模式详解之策略模式

作者:刘文慧

策略模式是一种应用广泛的行为型模式,核心思想是对算法进行封装,委派给不同对象来管理,本文将着眼于策略模式进行分享。

一、概述

我们在进行软件开发时要想实现可维护、可扩展,就需要尽量复用代码,并且降低代码的耦合度,而设计模式就是一种可以提高代码可复用性、可维护性、可扩展性以及可读性的解决方案。

大家熟知的23种设计模式,可以分为创建型模式、结构型模式和行为型模式三大类。其中,行为型模式可用于描述程序中多个类和多个对象如何协作完成复杂的任务,涉及不同对象间的职责分配、算法的抽象化。策略模式是一种应用广泛的行为型模式,本文将着眼于策略模式进行学习分享。

二、基本概念

策略模式的核心思想是对算法进行封装,委派给不同对象来管理。这样,我们就可以定义一系列算法,将每个算法封装到具有公共接口的一系列具体策略类中,从而使它们可以灵活替换,并让算法可以在不影响到客户端的情况下发生变化。同时,策略模式仅仅封装算法(包括添加、删除),但其并不决定在何时使用何种算法,算法的选择由客户端来决定。

比如,我们旅游时可以选择的出行策略有很多种:自行车、汽车、火车、飞机,每种出行策略都有各自的使用方法,只要能到目的地,我们可以随意更换各种策略。再比如我们去逛商场,商场会有很多促销活动:满减、返利等,这些促销方式本质上都是一些算法,而算法本身也是一种策略,随时都可以互相替换,针对同一件商品,今天满500减50、明天满300返100购物券,这些策略之间同样可以互换。

那么,我们应该如何使用策略模式呢?下面将从结构和使用步骤两个层面,对策略模式进行概念性介绍。

2.1 结构

策略模式包含三种类,分别是抽象策略类、具体策略类、环境类,它们各自负责完成特定任务,并且相互之间存在紧密的联系。

2.2 使用

有了上述的基本概念,我们将策略模式的使用步骤概括为:

  • step1:创建抽象策略类,为具体策略定义好一个公共接口;

  • step2:创建具体策略类,其通过接口来实现抽象策略类,同时封装了具体的算法;

  • step3:创建环境类,持有一个抽象策略类的引用,提供给客户端调用。

三、使用示例

除了双11购物狂欢节,每年都会打造很多其他的促销活动。试想一下,如果每种大促活动都使用一种促销模式,未免太过枯燥,于用户、商家、平台而言都不友好。因此,为了提升用户购买体验、突出商家营销特点,需要面向不同大促活动使用不同的策略进行促销。这里以促销策略为例,简单分析策略模式如何使用。

3.1 代码实现

//step1:定义抽象策略角色(Strategy):所有促销活动的共同接口
public interface Strategy {  
    void show();
}
​
//step2:定义具体策略角色(Concrete Strategy):不同类型的具体促销策略
//618大促活动 A
public class ConcreteStrategyA implements Strategy {
    @Override
    public void show() {
        System.out.println("618大促");
    }
}
​
//99大促活动 B
public class ConcreteStrategyB implements Strategy {
    @Override
    public void show() {
        System.out.println("99大促");
    }
}
​
//双11大促活动 C
public class ConcreteStrategyC implements Strategy {
    @Override
    public void show() {
        System.out.println("双11大促");
    }
}
​
//step3:定义环境角色(Context):把促销活动推送给用户,这里可理解为淘宝平台
public class Context{
    //持有抽象策略的引用
    private Strategy myStrategy;
    //生成构造方法,让平台根据传入的参数(type)选择促销活动
    public Context(Strategy strategyType) {
        this.myStrategy = strategyType;
    }
    //向用户展示促销活动
    public void taoPlatformShow(String time) {
        System.out.println(time + "的促销策略是:");
        myStrategy.show();
    }
}
​
//step4:客户端调用,需事先明确所有每种策略类如何使用
public class StrategyPattern{
  public static void main(String[] args){
        Context_TaoPlatform context;
    
        String time1 = "9月";
        Strategy strategyB = new ConcreteStrategyB();
        context = new Context(strategyB);
        context.taoPlatformShow(time1);
    
        String time2 = "11月";
        Strategy strategyC = new ConcreteStrategyC();
        context = new Context(strategyC);
        context.taoPlatformShow(time2);
    
        String time3 = "6月";
        Strategy strategyA = new ConcreteStrategyA();
        context = new Context(strategyA);
        context.taoPlatformShow(time3);
  }   
}

3.2 结果输出

9月的促销策略是:
99大促
11月的促销策略是:
双11大促
6月的促销策略是:
618大促

3.3 UML图

3.4 与简单工厂模式的区别

从上面的代码示例及类图可以看出来,策略模式和上一篇文章中介绍的简单工厂模式很像,两者主要区别在于Context类和工厂类。为了方便对比,我们把这两个类的代码单独拎出来看看:

public class Context_TaoPlatform{
    //持有抽象策略的引用
    private Strategy myStrategy;
    //生成构造方法,让平台根据传入的参数(type)选择促销活动
    public TaoPlatform(Strategy strategyType) {
        this.myStrategy = strategyType;
    }
    //向用户展示促销活动
    public void taoPlatformShow(String time) {
        System.out.println(time + "的促销策略是:");
        myStrategy.show();
    }
}

public class Factory{
    public static Shirt exhibit(String ShirtName){
        switch(ShirtName){
            case "女款衬衫":
                return new WomenShirt();
            case "男款衬衫":
                return new MenShirt();
            default:
                return null;
        }
    }
}

首先看一下接收参数:工厂类Factory中的exhibit()方法接收字符串,返回一个Shirt对象;环境类Context_TaoPlatform初始化时需要接收一个Strategy对象。也就是说:工厂类中是根据接收的条件创建一个相应的对象,而Context类接收的是一个对象,可以调用方法去执行此对象的方法。

举个例子:笔有很多种,假设有一个工厂专门负责生产不同用途的笔。

工厂模式:根据用户给出的目的来生产不同用途的笔,如:要写毛笔字就生产毛笔、要写钢笔字就生产钢笔。即根据用户给出的某种属性,生产能做出相应行为的一种对象返回给用户,重点在于创建何种对象。

策略模式:用工厂生产的笔去出做对应的行为,如:用毛笔写毛笔字、用钢笔写钢笔字。即根据用户给出的某种对象,执行相应的方法,重点在于选择何种行为。

四、JDK源码赏析

这里以Comparator比较器为例,通过分析其源码实现来深入理策略模式。

在JDK中,我们调用数组工具类Arrays的一个排序方法sort()时,可以使用默认的排序规则(升序),也可以自定义一种排序的规则,即自定义实现升序或降序的排序。源码如下:

public class Arrays{
    public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            //若没有传入Comparator接口的实现类对象,调用默认的升序排序方法
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                //jdk5及之前的传统归并排序,新版本中LegacyMergeSort.userRequested默认false
                legacyMergeSort(a, c);
            else
                //改进后的归并排序
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }
}

此时我们需要传入两个参数:一个是待排序的数组,另一个则是Comparator接口的实现类对象。其中,Comparator接口是一种函数式接口,该接口中定义了一个抽象方法int compare(T o1, T o2),用于定义具体的排序规则。这里Comparator接口就是策略模式中的抽象策略接口,它定义了一个排序算法,而具体策略(具体的排序算法)将由用户来定义,那么Arrays就是一个环境类,sort() 方法可以传入一个策略c ,让Arrays根据这个策略进行排序任务。

public class demo {
    public static void main(String[] args) {
​
        Integer[] data = {12, 2, 3, 2, 4, 5, 1};
        // 实现降序排序
        Arrays.sort(data, new Comparator<Integer>() {
             // 排序策略 降序
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });
        System.out.println(Arrays.toString(data)); //[12, 5, 4, 3, 2, 2, 1]
    }
}

在上面这个测试类中,我们在调用Arrays.sort()方法时,第二个参数传递的是Comparator接口的子实现类对象。由此可见,Comparator充当的是抽象策略角色,而具体的子实现类充当的是具体策略角色,环境角色类 Arrays应该持有抽象策略的引用来调用。那么,Arrays.sort()方法究竟有没有使用Comparator子实现类中的compare()方法?下面再看看TimSort.sort()方法,源码如下:

class TimSort<T> {
    static <T> void sort(T[] a, int lo, int hi, Comparator<? super T> c,
                         T[] work, int workBase, int workLen) {
        assert c != null && a != null && lo >= 0 && lo <= hi && hi <= a.length;
​
        int nRemaining  = hi - lo;
        if (nRemaining < 2)
            return;  // Arrays of size 0 and 1 are always sorted
​
        // If array is small, do a "mini-TimSort" with no merges
        if (nRemaining < MIN_MERGE) {
            int initRunLen = countRunAndMakeAscending(a, lo, hi, c);
            binarySort(a, lo, hi, lo + initRunLen, c);
            return;
        }
        ...
    }   
        
    private static <T> int countRunAndMakeAscending(T[] a, int lo, int hi,Comparator<? super T> c) {
        assert lo < hi;
        int runHi = lo + 1;
        if (runHi == hi)
            return 1;
​
        // Find end of run, and reverse range if descending
        if (c.compare(a[runHi++], a[lo]) < 0) { // Descending
            while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) < 0)
                runHi++;
            reverseRange(a, lo, runHi);
        } else {                              // Ascending
            while (runHi < hi && c.compare(a[runHi], a[runHi - 1]) >= 0)
                runHi++;
        }
        return runHi - lo;
    }
}

上面的代码最后会执行到countRunAndMakeAscending()方法中,在执行判断语句时调用了compare()方法。那么如果只用了compare()方法,在调用Arrays.sort()方法时只要传具体compare()重写方法的类对象。

五、优缺点及适用场景

5.1 优点

  • 具体策略类之间可自由切换,由于具体策略类都实现同一个抽象策略接口,所以它们之间可以自由切换。

  • 支持“开闭原则”,扩展增加一个新策略时只需添加一个具体策略类即可,基本不需要改变原有的代码。

  • 避免使用多重条件选择语句(if else),充分体现面向对象设计思想。

5.2 缺点

  • 客户端必须知道所有的具体策略类,并理解不同具体策略的区别、自行决定使用哪一个策略类。

  • 策略模式将产生很多具体策略类,在一定程度上增加了系统中类的个数(可通过使用享元模式在一定程度上减少对象数量)。

5.3 适用场景

  • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到具体策略类中。

  • 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句,就能避免使用难以维护的多重条件选择语句,并体现面向对象涉及的概念。

  • 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节,提高算法的保密性与安全性。

  • 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为。

六、总结

策略模式是一个比较容易理解和使用的设计模式,它仅封装算法,方便新算法插入系统中、老算法从系统中退休。本文在分析策略模式的缺点时提到,策略模式并不决定在何时使用何种算法,算法选择由客户端来决定,虽然这在一定程度上提高了系统的灵活性,但客户端需要理解所有具体策略类之间的区别,以便选择合适的算法,增加了客户端的使用难度。

策略模式和工厂模式有一定相似之处,在于它们的模式结构,因此有时候会让人混淆不清。实际上,这两者之间存在较多差异:工厂模式是创建型模式,作用是创建对象,它关注对象如何创建,主要解决的是资源的统一分发,将对象的创建完全独立出来,让对象的创建和具体的使用客户无关;策略模式是行为型模式,作用是让一个对象在许多行为中选择一种行为,它关注行为如何封装,通过定义策略族来实现策略的灵活切换与扩展,并让策略的变化独立于使用策略的客户。

另外,很多场景下策略模式和工厂模式可以结合使用,共同发挥优势起到相辅相成的作用。比如,策略模式的缺点之一是用户必须清楚所有的具体策略算法,这样具体策略难免暴露出去,并且要由上层模块初始化,这与迪米特法则相悖(最少知识原则),而上层模块和底层模之间的解耦,可以让工厂模式来完成。两者结合之后,对于上层模块而言不需要知道每种具体策略,只要通过Context就可以实现策略模式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/26684.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LayUI前框框架普及版

LayUI 一、课程目标 1. 【了解】LayUI框架 2. 【理解】LayUI基础使用 3. 【掌握】LayUI页面元素 4. 【掌握】LayUI内置模块二、LayUI基本使用 2.1 概念 layui&#xff08;谐音&#xff1a;类UI) 是一款采用自身模块规范编写的前端 UI 框架&#xff0…

阿里云nginx配置https踩坑(配置完后访问显示无法访问此网站)

本人小前端一枚&#xff0c;最近在玩服务器部署自己的东西时踩了个坑&#xff01;&#xff01;&#xff01; server {listen 443 ssl;server_name localhost;ssl_certificate 证书.com.pem;ssl_certificate_key 证书.com.key;#后台管理静态资源存放location / { #文件目…

springboot+vue新闻稿件java在线投稿管理系统

本文介绍了新闻稿件管理系统的开发全过程。通过分析新闻稿件管理系统管理的不足&#xff0c;创建了一个计算机管理新闻稿件管理系统的方案。文章介绍了新闻稿件管理系统的系统分析部分&#xff0c;包括可行性分析等&#xff0c;系统设计部分主要介绍了系统功能设计和数据库设计…

微信小程序开发实战 ②②(全局数据共享)

作者 : SYFStrive 博客首页 : HomePage &#x1f4dc;&#xff1a; 微信小程序 &#x1f4cc;&#xff1a;个人社区&#xff08;欢迎大佬们加入&#xff09; &#x1f449;&#xff1a;社区链接&#x1f517; &#x1f4cc;&#xff1a;觉得文章不错可以点点关注 &#x1f4…

基于flask的web应用开发——访问漂亮的html页面以及页面跳转

目录 0. 前言1. html基本知识2. 编写html文本3. 在Flask中设置访问html4. 实现点击跳转 0. 前言 本节学习如何在flask应用程序下让用户访问你提前制作好的html页面 操作系统&#xff1a;Windows10 专业版 开发环境&#xff1a;Pycahrm Comunity 2022.3 Python解释器版本&am…

Arcgis进阶篇(6)——如何将Arcgis Pro的离线数据发布成服务

常常因为Arcgis Server&#xff08;或者GeoScene Server&#xff09;昂贵的价格&#xff0c;而导致小项目技术选型选择开源的GIS Server&#xff08;如GeoServer等&#xff09;。但用完之后&#xff0c;发现后者实在拉跨&#xff0c;使用对比差异巨大。那就只能另想办法&#x…

基于轻量级YOLOv5n/s/m三款模型开发构建基于无人机视角的高空红外目标检测识别分析系统,对比测试分析性能

有关于无人机目标检测和红外场景下的目标检测的项目在我之前的文章中都有实践经历了&#xff0c;但是将无人机和红外场景结合的目标检测项目还是很少的&#xff0c;本文的核心想法就是基于高空无人机场景开发构建目标检测系统。 前面相关博文如下&#xff0c;感兴趣的话可以自…

Ubuntu本地快速搭建web小游戏网站,公网用户远程访问【内网穿透】

文章目录 前言1. 本地环境服务搭建2. 局域网测试访问3. 内网穿透3.1 ubuntu本地安装cpolar内网穿透3.2 创建隧道3.3 测试公网访问 4. 配置固定二级子域名4.1 保留一个二级子域名4.2 配置二级子域名4.3 测试访问公网固定二级子域名 转载自cpolar极点云的文章&#xff1a;在Ubunt…

BGP状态机

BGP协议基本概念 BGP是一种外部网管协议(EGP),与OSPF、RIP等内部网管协议(IGP)不同,其着眼点不在于自动发现网络拓扑,而在于AS之间选择最佳路由和控制路由的传播。 自治系统AS( Autonomous System) 由同一个技术管理机构管理、使用统一选路策略的一些路由器的集合。 …

CDN如何进行内容缓存与内容预热

CDN的启用与管理 1、打开火伞云融合CDN系统控制后台-CDN管理 2、查看加速域名下的全部CDN服务&#xff0c;可以看到有部分厂商暂时处于未启用状态&#xff0c;这是因为这些厂商要求进行域名所有权校验后方可使用&#xff08;如果已经处于已启用状态的厂商则不用额外进行操作&…

Redis数据类型之String——字符串、数值、bitmap

Redis数据类型之String——字符串、数值、bitmap 注意索引位置一般从左到右 0开始&#xff0c;叫正向索引。从右到左-1开始叫反向索引 字符串 字符串有很多操作set、get、append、setrange、getrange等&#xff0c;每个都有自己对应的用处 SET SET key value 设置指定 key …

UniFi USW-Flex 室内-室外 POE 交换机

选择理由 选择理由是是因为要户外使用&#xff0c;对比下户外可以使用的 POE 交换机并不是很多。 UniFi USW-Flex 室内-室外 5 端口 PoE 千兆交换机能够支持在户外和户内使用。 户外使用需要具有基本的防水性能&#xff0c;尤其是冬天比较寒冷的时候也需要具备一定的环境耐受…

【数据结构】——树的相关习题

目录 一、选择填空判断题题1题2题3题4题5题6题7题8题9 二、应用题题10&#xff08;遍历序列&#xff09;题11&#xff08;存储结构&#xff09;题12 13&#xff08;二叉树/树、森林之间的转换&#xff09;题14&#xff08;线索二叉树&#xff09; 一、选择填空判断题 题1 1、设…

JVM那些事 (含经典面试题)

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 前言: 1. JVM&#xff1a;Java 虚拟机&#x…

由于找不到iutils.dll而造成的错误,要怎么去解决?

在使用电脑或运行某些软件时&#xff0c;有时会遇到“找不到iutils.dll”的错误提示。这个错误通常表示缺少iutils.dll文件或者该文件已经损坏。如果你遇到了这个问题&#xff0c;不要担心&#xff0c;因为有很多方法可以解决这个问题。下面我们一起来看看找不到iutils.dll的问…

API电商 ERP 数据管理

没有 API&#xff0c;应用之间的通信将会被扼杀&#xff1b;软件开发者将不断重写并执行相同功能的软件&#xff1b;创新的脚步将会放缓。 API 随处可见。大到一个软件系统&#xff0c;小到几行程序&#xff0c;只要具备了一定的特征&#xff0c;都可以被称作 API。那么&#…

【网络】基础知识1

目录 网络发展 独立模式 网络互联 局域网LAN 广域网WAN 什么是协议 初识网络协议 协议分层 OSI七层模型 TCP/IP四层&#xff08;或五层&#xff09;模型 OSI和TCP/IP对比 网络传输流程 什么是报头 局域网通信原理 同网段的主机通讯 跨网段的主机通讯 数据包封装…

数据治理8大核心模块建设

数据治理是一个去中心化、多元参与的系统工程。一个全面且明确的数据治理体系&#xff0c;可以帮助组织构建生态式、协同化治理路径&#xff0c;最大化地提升整体数据质量&#xff0c;实现数据战略&#xff0c;激活新型生产力。 本文以元数据、主数据、数据标准、数据质量、数…

jenkins主从节点安装及pipeline构建

一、背景 通过Jenkins主节点配置的pipeline下发给从节点执行&#xff0c;从而兼容容器化执行 二、安装主节点 docker-compose.yml jenkins:user: rootrestart: alwaysimage: jenkinsci/blueoceancontainer_name: jenkins# network_mode: hostports:- "8081:8080"-…

【算法与数据结构】209.长度最小的子数组

文章目录 题目一、暴力穷解法二、滑动窗口法完整代码 所有的LeetCode题解索引&#xff0c;可以看这篇文章——【算法和数据结构】LeetCode题解。 题目 一、暴力穷解法 思路分析&#xff1a;这道题涉及到数组求和&#xff0c;那么我们很容易想到利用两个for循环来写&#xff0c;…