设计模式:详细拆解策略模式

策略模式

既然是详解,就不以案例开头了,直奔主题,先来看看什么是策略模式。

模式定义

定义一系列的算法,把它们一个个封装起来,并且使它们可相互替换。本模式 使得算法可独立于使用它的客户而变化。

结构

Strategy(策略接口):

  • 用来约束一系列具体的策略算法。
  • 定义了算法的公共接口,使得算法可以互换使用。

ConcreteStrategy(具体策略实现)

  • 具体的算法实现,继承自策略接口。
  • 每个具体策略类实现了策略接口中定义的算法

Context(上下文):

  • 负责和具体的策略类交互。
  • 通常上下文会持有一个真正的策略实现。
  • 上下文可以让具体的策略类来获取上下文的数据。
  • 甚至可以让具体的策略类来回调上下文的方法。

样例代码:

// 策略接口
interface Strategy {void algorithmInterface();
}// 具体策略A
class ConcreteStrategyA implements Strategy {@Overridepublic void algorithmInterface() {System.out.println("执行策略A的算法实现");}
}// 具体策略B
class ConcreteStrategyB implements Strategy {@Overridepublic void algorithmInterface() {System.out.println("执行策略B的算法实现");}
}// 具体策略C
class ConcreteStrategyC implements Strategy {@Overridepublic void algorithmInterface() {System.out.println("执行策略C的算法实现");}
}// 上下文类
class Context {private Strategy strategy;public Context(Strategy strategy) {this.strategy = strategy;}public void setStrategy(Strategy strategy) {this.strategy = strategy;}public void contextInterface() {strategy.algorithmInterface();}
}// 客户端代码
public class StrategyPatternDemo {public static void main(String[] args) {Context context = new Context(new ConcreteStrategyA());context.contextInterface();context.setStrategy(new ConcreteStrategyB());context.contextInterface();context.setStrategy(new ConcreteStrategyC());context.contextInterface();}
}

策略模式实现案例

我们以CRM中的报价场景为例,来看一下策略模式的使用,简化一下场景,会有如下的报价方案:

  • 对普通客户或者是新客户报全价:
  • 对老客户报的价格,统一折扣5%;
  • 对大客户报的价格,统一折扣10%。

根据策略模式的思路,我们大致要做如下内容:

  1. 首先需要定义出算法的接口。
  2. 然后把各种报价的计算方式单独出来,形成算法类。
  3. 对于Price类,把它当做上下文,在计算报价的时候,不再需要判断,直接使 用持有的具体算法进行运算即可。具体选择使用哪一个算法的功能挪出去,放到外部使 用的客户端去。

策略模式形成的类图如下:

基于SpringBoot的项目实现

// 策略接口
public interface Strategy {double calcPrice(double goodsPrice);
}// 普通客户策略
@Commpont
public class NormalCustomerStrategy implements Strategy {@Overridepublic double calcPrice(double goodsPrice) {// 普通客户不打折return goodsPrice;}
}// 老客户策略
@Commpont
public class OldCustomerStrategy implements Strategy {@Overridepublic double calcPrice(double goodsPrice) {// 老客户享受5%的折扣return goodsPrice * 0.95;}
}// 大客户策略
@Commpont
public class LargeCustomerStrategy implements Strategy {@Overridepublic double calcPrice(double goodsPrice) {// 大客户享受10%的折扣return goodsPrice * 0.90;}
}// 报价上下文类
public class Price {private Strategy strategy;// 构造函数,初始化策略public Price(Strategy strategy) {this.strategy = strategy;}// 设置策略public void setStrategy(Strategy strategy) {this.strategy = strategy;}// 计算并返回报价public double quote(double goodsPrice) {return strategy.calcPrice(goodsPrice);}
}public enum ClientType{//正常客户normal,//老客户old,//大客户large;}@Service
public class ClientServiceImpl implement ClientService{@Autowired
private ApplicationContext context;//……public ResData createQutePrice(String clientId){ResData result = new ResData();Client client = ClientMapper.findById(clientId);//……客户其他认证逻辑Double nowPrice = XXMapper.queryNowPrice();result.setPrice(getPriceUtil(client.getClientType()).qute(nowPrice ));return result;
}private Price getPriceUtil(ClientType type){Strategy strategy = null; switch(type){case old:strategy = (Strategy)context.getBean(OldCustomerStrategy.class);break;case large:strategy = (Strategy)context.getBean(LargeCustomerStrategy.class);break;default:strategy = (Strategy)context.getBean(NormalCustomerStrategy.class);break;}Price price = new Price(strategy);return price;
}}

使用该写法,虽然相比if-else来讲要更为麻烦,但是随着报价场景的增加,我们仅仅通过新增实现类和调整getPriceUtil方法即可完成扩展。

策略模式详解

策略模式的本质

策略模式的本质是:

分离算法,选择实现

策略模式的功能是把具体的算法实现从具体的业务处理中独立出来,把它们实现成 为单独的算法类,从而形成一系列的算法,并让这些算法可以相互替换。 策略模式的重心不是如何来实现算法,而是如何组织、调用这些算法,从而让程序结构更灵活,具有更好的维护性和扩展性。

策略模式一个很大的特点就是各个策略算法的平等性。对于一系列具体的策略算法, 大家的地位是完全一样的,正是因为这个平等性,才能实现算法之间可以相互替换。 所有的策略算法在实现上也是相互独立的,相互之间是没有依赖的。 所以可以这样描述这一系列策略算法:策略算法是相同行为的不同实现。

从设计原则角度

  • 策略模式很好地体现了开一闭原则。策略模式通过把一系列可变的算法进行封装,并定义出合理的使用结构,使得在系统出现新算法的时候,能够很容易地把新的算法加入到已有的系统中,而已有的实现不需要做任何修改。这在前面的示例中已经体现出来了,好好体会一下。
  • 策略模式还很好地体现了里氏替换原则。策略模式是一个扁平结构,一系列的实现算法其实是兄弟关系,都是实现同一个接口或者继承的同一个父类。这样只要使用策略的客户保持面向抽象类型编程,就能够使用不同策略的具体实现对象来配置它,从而实现一系列算法可以相互替换。

策略模式与If-else

看了前面的示例,很多朋友会发现,每个策略算法具体实现的功能,就是原来在f-else 结构中的具体实现。 没错,其实多个if-elseif语句表达的就是一个平等的功能结构,你要么执行if,要么 执行else,或者是elseif,这个时候,if块中的实现和else块中的实现从运行地位上来讲是平等的。 而策略模式就是把各个平等的具体实现封装到单独的策略实现类了,然后通过上下 文来与具体的策略类进行交互。所以其实很多地方都在讲策略模式能消除遍地if-else是不准确的,if-else并没有消失,只是代码形式改变了,但是使用策略模式替换if-else的优势是不可否认的,将原本if-else内的逻辑抽离出来,可以达到修改具体内容而不破坏外层框架的目的。因此多个f-else语句可以考虑使用策略模式。

Strategy的扩展

在前面的示例中,Strategy都是使用接口来定义的,这也是常见的实现方式。但是如 果多个算法具有公共功能的话,可以把Strategy实现成为抽象类,然后把多个算法的公 共功能实现到Strategy中。

// 抽象策略类,包含公共功能
public abstract class Strategy {// 公共方法,所有策略类都会用到public void commonMethod() {System.out.println("这是一个公共方法");}// 抽象方法,具体的策略算法实现public abstract void algorithmInterface();
}// 具体策略A
public class ConcreteStrategyA extends Strategy {@Overridepublic void algorithmInterface() {System.out.println("执行策略A的算法实现");}
}// 具体策略B
public class ConcreteStrategyB extends Strategy {@Overridepublic void algorithmInterface() {System.out.println("执行策略B的算法实现");}
}// 具体策略C
public class ConcreteStrategyC extends Strategy {@Overridepublic void algorithmInterface() {System.out.println("执行策略C的算法实现");}
}

Context与Strategy的关系

在策略模式中,通常是上下文(Context)使用具体的策略实现对象。反过来,策略实现对象也 可以从上下文获取所需要的数据。因此可以将上下文当作参数传递给策略实现对象,这 种情况下上下文和策略实现对象是紧密耦合的。 在这种情况下,上下文封装看具体策略对象进行算法运算所需要的数据,具体策略 对象通过回调上下文的方法来获取这些数据。 甚至在某些情况下,策略实现对象还可以回调上下文的方法来实现一定的功能,这种使用场景下,上下文变相充当了多个策略算法实现的公共接口。在上下文定义的方法 可以当作是所有或者是部分策略算法使用的公共功能。

但是需要注意,由于所有的策略实现对象都实现同一个策略接口,传入同一个上 下文,可能会造成传入的上下文数据的浪费,因为有的算法会使用这些数据, 而有的算法不会使用,但是上下文和策略对象之间交互的开销是存在的。

以一个公司结算的场景案例来说:

很多企业的工资支付方式是很灵活的,可支付方式是比较多的,比如,人民币现金支付、美元现金支付、银行转账到工资账户、银行转账到工资卡;一些创业型的企业为了留住骨干员工,还可能有工资转股权等方式。总之一句话,工资支付方式很多。

随着公司的发展,会不断有新的工资支付方式出现,这就要求能方便地扩展;另外工资支付方式不是固定的,是由公司和员工协商确定的,也就是说可能不同的员工采用的是不同的支付方式,甚至同一个员工,不同时间采用的支付方式也可能会不同,这就要求能很方便地切换具体的支付方式。

要实现这样的功能,显然策略模式是一个很好的选择。在实现这个功能的时候,不同的策略算法需要的数据是不一样,比如,现金支付就不需要银行账号,而银行转账就需要账号。

这就导致在设计策略接口中的方法时,不太好确定参数的个数,而且,就算现在把所有的参数都列上了,扩展性难以保障,加入一个新策略,就需要修改接口。我们根据上面对于Context与Strategy的关系分析,基于将上下文当作参数传递给策略对象的方式以策略模式实现上述需求

先定义工资支付的策略接口,也就是定义一个支付工资的方法:

/*** 支付工资的策略接口,公司有多种支付工资的算法* 比如,现金、银行卡、现金加股票、现金加期权、美元支付等*/
public interface PaymentStrategy {/*** 公司给某人真正支付工资* @param ctx 支付工资的上下文,里面包含算法需要的数据*/public void pay(PaymentContext ctx);
}

这里先简单实现人民币现金支付和美元现金支付方式,当然并不是真地去实现跟银行的交互,只是示意一下:

/*** 人民币现金支付*/
public class RMBCash implements PaymentStrategy {public void pay(PaymentContext ctx) {System.out.println("现在给" + ctx.getUserName() + "人民币现金支付" + ctx.getMoney() + "元");}
}/*** 美元现金支付*/
public class DollarCash implements PaymentStrategy {public void pay(PaymentContext ctx) {System.out.println("现在给" + ctx.getUserName() + "美元现金支付" + ctx.getMoney() + "元");}
}

 下面是上下文的实现以及使用:

/*** 支付工资的上下文,每个人的工资不同,支付方式也不同*/
public class PaymentContext {/*** 应被支付工资的人员,简单点,用姓名来代替*/private String userName = null;/*** 应被支付的工资金额*/private double money = 0.0;/*** 支付工资的方式的策略接口*/private PaymentStrategy strategy = null;// 构造方法,传入被支付工资的人员,应支付的金额和具体的支付策略// @param userName 被支付工资的人员// @param money 应支付的金额// @param strategy 具体的支付策略public PaymentContext(String userName, double money, PaymentStrategy strategy) {this.userName = userName;this.money = money;this.strategy = strategy;}// 只有getter方法,让策略算法在实现的时候,根据需要来获取上下文中的数据public String getUserName() {return userName;}public double getMoney() {return money;}/*** 立即支付工资*/public void payNow() {// 使用客户希望的支付策略来支付工资this.strategy.pay(this);}
}public class Client {public static void main(String[] args) {// 创建相应的支付策略PaymentStrategy strategyRMB = new RMBCash();PaymentStrategy strategyDollar = new DollarCash();// 准备小李的支付工资上下文PaymentContext ctx1 = new PaymentContext("小李", 5000, strategyRMB);// 向小李支付工资ctx1.payNow();// 切换一个人,给Petter支付工资PaymentContext ctx2 = new PaymentContext("Petter", 8000, strategyDollar);ctx2.payNow();}
}

基本的策略模式框架已经搭建完成,如果接下来需要增加一种支付方式,要求能支付到银行,基于以上代码,扩展方式有两种。

扩展方式一:通过扩展上下文对象(Context)来准备新的算法需要的数据

/*** 扩展的支付上下文对象*/
public class PaymentContext2 extends PaymentContext {/*** 银行账号*/private String account = null;/*** 构造方法,传入被支付工资的人员,应支付的金额和具体的支付策略* @param userName 被支付工资的人员* @param money 应支付的金额* @param account 支付到的银行账号* @param strategy 具体的支付策略*/public PaymentContext2(String userName, double money, String account, PaymentStrategy strategy) {super(userName, money, strategy);this.account = account;}public String getAccount() {return account;}
}/*** 算法策略的实现* 支付到银行卡*/
public class Card implements PaymentStrategy {public void pay(PaymentContext ctx) {// 这个新的算法自己知道要使用扩展的支付上下文,所以强制造型一下PaymentContext2 ctx2 = (PaymentContext2) ctx;System.out.println("现在给" + ctx2.getUserName() + "的"+ ctx2.getAccount() + "账号支付了" + ctx2.getMoney() + "元");// 连接银行,进行转账,就不去管了}
}public class Client {public static void main(String[] args) {// 创建相应的支付策略PaymentStrategy strategyRMB = new RMBCash();PaymentStrategy strategyDollar = new DollarCash();// 准备小李的支付工资上下文PaymentContext ctx1 = new PaymentContext("小李", 5000, strategyRMB);// 向小李支付工资ctx1.payNow();// 切换一个人,给Petter支付工资PaymentContext ctx2 = new PaymentContext("Petter", 8000, strategyDollar);ctx2.payNow();// 测试新添加的支付方式PaymentStrategy strategyCard = new Card();PaymentContext2 ctx3 = new PaymentContext2("小王", 9000, "010998877656", strategyCard);ctx3.payNow();}
}

 通过代码实现,可以看出,这种扩展方式是新增加一种支付到银行卡的策略实现,然后通过继承来扩展支付上下文,其中添加新的支付方式需要的新数据,比如银行卡账户,并在客户端使便用新的上下文和新的策略实现就可以了,这样已有的实现都不需要改变,完全遵循开一闭原则。

扩展方式二:通过策略的构造方法传入新算法所需数据 

/*** 支付到银行卡*/
public class Card2 implements PaymentStrategy {// 账号信息private String account = "";/*** 构造方法,传入账号信息* @param account 账号信息*/public Card2(String account) {this.account = account;}public void pay(PaymentContext ctx) {System.out.println("现在给" + ctx.getUserName() + "的"+ this.account + "账号支付了" + ctx.getMoney() + "元");// 连接银行,进行转账,就不去管了}
}// 直接在客户端测试就可以了。示例代码如下:
public class Client {public static void main(String[] args) {// 测试新添加的支付方式PaymentStrategy strategyCard2 = new Card2("010998877656");PaymentContext ctx4 = new PaymentContext("小", 9000, strategyCard2);ctx4.payNow();}
}

 

  • 对于扩展上下文的方式:

    • 优点:所有策略的实现风格更统一,策略需要的数据都统一从上下文来获取,这样在使用方法上也很统一;在上下文中添加新的数据,别的相应算法也可以用得上,可以视为公共的数据。
    • 缺点:如果这些数据只有一个特定的算法来使用,那么这些数据有些浪费;每次添加新的算法都去扩展上下文,容易形成复杂的上下文对象层次,也未见得有必要。
  • 对于在策略算法的实现上添加自己需要的数据的方式:

    • 优点:实现起来简单,容易理解。
    • 缺点:实现风格与其他策略不一致,其他策略都是从上下文中来获取数据,而这个策略的实现一部分数据来自上下文,一部分数据来自自己,有些不统一;外部使用这些策略算法的时候也不一,难于以一个统一的方式来动态切换策略算法。

适用策略模式的场景

  1. 当出现有许多相关的类,仅仅是行为有差别的情况下,可以使用策略模式来使用多个行为中的一个来配置一个类的方法,实现算法动态切换。

  2. 当出现同一个算法,有很多不同实现的情况下,可以使用策略模式来把这些“不同的实现”实现成为一个算法的类层次。

  3. 当需要封装算法中,有与算法相关数据的情况下,可以使用策略模式来避免暴露这些跟算法相关的数据结构。

  4. 当出现抽象一个定义了很多行为的类,并且是通过多个f-else语句来选择这些行为的情况下,可以使用策略模式来代替这些条件语句。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

C++ | Leetcode C++题解之第318题最大单词长度乘积

题目&#xff1a; 题解&#xff1a; class Solution { public:int maxProduct(vector<string>& words) {unordered_map<int,int> map;int length words.size();for (int i 0; i < length; i) {int mask 0;string word words[i];int wordLength word.s…

深入解析Java虚拟机(JVM)内存模型-全面掌握JVM内存管理

Java虚拟机(JVM)的内存模型是Java开发者必须掌握的核心知识之一。无论你是刚入门的新手,还是经验丰富的老手,深入理解JVM内存模型都能帮助你写出更高效、更稳定的Java程序。本文将带你全面剖析JVM内存模型的各个组成部分,深入探讨其工作原理,并通过实例讲解如何进行内存优化。让…

C#-读取测序数据的ABI文件并绘制svg格式峰图

本地环境&#xff1a;win10&#xff0c;visual studio 2022 community 目录 前言问题描述实现效果解决思路实现要点ABI文件的组织方式svg绘制问题变色碱基值 动态设置svg图像宽度 前言 本文是在已有的代码基础上进行的开发&#xff0c;前期已经实现&#xff1a; ABI文件的解析…

【从零搭建SpringBoot3.x 项目脚手架】- 1. 工程初始化

为什么会有这个系列文章 在项目开发中&#xff0c;大多项目依旧沿用的是 JDK 8 Spring Boot 2.x 系列的技术栈&#xff0c;没有Spring Boot 3.x 的上手实践机会。在个人学习探索 Spring Boot 3.x 的过程中&#xff0c;遇到多数第三方框架集成和问题排查的技术问题&#xff0c…

[极客大挑战 2019]Secret File-web

打开题目 查看源码 直接访问Archive_room.php 第二个页面是个点击框&#xff0c;这里bp抓包确认&#xff1b;若是直接SECRET&#xff0c;会跳到end.php 直接访问secr3t.php 代码审计一下 playload&#xff1a;secr3t.php?fileflag.php 改为php协议读取权限 secr3t.php?f…

[图解]SysML建模电磁轨道炮-01块定义图

1 00:00:00,490 --> 00:00:04,000 我们是用EA复刻一个网络上的案例 2 00:00:06,370 --> 00:00:09,240 电磁轨道炮&#xff0c;它的原理很简单 3 00:00:09,490 --> 00:00:10,960 初中物理就可以理解 4 00:00:11,670 --> 00:00:14,010 首先&#xff0c;电流形成磁…

polyfit曲线拟合

一、简介 polyfit函数是matlab中用于进行曲线拟合的一个函数。其数学基础是最小二乘法曲线拟合原理。曲线拟合&#xff1a;已知离散点上的数据集&#xff0c;即已知在点集上的函数值&#xff0c;构造一个解析函数&#xff08;其图形为一曲线&#xff09;使在原离散点上尽可能接…

快讯 | 苹果携手OpenAI,ChatGPT即将登陆iOS 18

在数字化浪潮的推动下&#xff0c;人工智能&#xff08;AI&#xff09;正成为塑造未来的关键力量。硅纪元视角栏目紧跟AI科技的最新发展&#xff0c;捕捉行业动态&#xff1b;提供深入的新闻解读&#xff0c;助您洞悉技术背后的逻辑&#xff1b;汇聚行业专家的见解&#xff0c;…

【反序列化漏洞】serial靶机详解

一、安装靶机 首先创建新的虚拟机。 然后选择客户机版本为Ubuntu 64位。 然后选择使用现有磁盘&#xff0c;选择下载的vmdk磁盘文件即可。剩下的都是默认 二、信息收集 发现主机192.168.204.143 访问 扫描端口nmap -A 192.168.204.143 -p-&#xff0c;发现只有ssh:22和http:8…

Java:线程安全

引子 首先来看一段代码: private static int count 0;public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(()->{for (int i 0; i < 50000; i) {count;}});Thread t2 new Thread(()->{for (int i 0; i < 50000; i) {…

如何解决C#字典的线程安全问题

前言 我们在上位机软件开发过程中经常需要使用字典这个数据结构&#xff0c;并且经常会在多线程环境中使用字典&#xff0c;如果使用常规的Dictionary就会出现各种异常&#xff0c;本文就是详细介绍如何在多线程环境中使用字典来解决线程安全问题。 1、非线程安全举例 Dictio…

Vue+live2d实现虚拟人物互动(一次体验叙述)

目录 故事的开头&#xff1a; 最终的实现效果&#xff1a; 实现步骤&#xff1a; 第一步&#xff1a;下载重要文件 第二步&#xff1a;创建vue项目文件&#xff0c;将刚下载文件拷贝到public目录下 第三步&#xff1a;在index.html文件中引入js 第四步&#xff1a;使用&…

【数据结构算法经典题目刨析(c语言)】顺序表和链表的区别(图文详解)

&#x1f493; 博客主页&#xff1a;C-SDN花园GGbond ⏩ 文章专栏&#xff1a;数据结构经典题目刨析(c语言) 目录 顺序表和链表的区别 一、底层存储空间 二、插入和删除操作 三、随机访问 四、空间利用率 五、应用场景 六、高速缓存 为什么顺序表的缓存利用率高于链表呢…

设计界的新宠:5款热门UI在线设计软件评测

随着用户界面设计行业的蓬勃发展&#xff0c;越来越多的设计师进入用户界面设计。选择一个方便的用户界面设计工具尤为重要&#xff01;除了传统的用户界面设计工具&#xff0c;在线用户界面设计工具也受到越来越多设计师的青睐。这种不受时间、地点、计算机配置限制的工作方法…

OpenCV||超详细的图像边缘检测

一、基本概念 1.图像边缘检测目的 特征提取&#xff1a;边缘是图像中亮度变化最显著的部分&#xff0c;它们通常对应于物体的轮廓、不同区域的边界等。通过边缘检测&#xff0c;可以从图像中提取出这些重要的特征信息&#xff0c;为后续处理如图像分割、目标识别等提供基础。 …

webfunny埋点系统如何进行部署?

hello 大家webfunny埋点系统做了不少功能更新&#xff0c;平常给大家分享比较多的是**webfunny前端监控系统**&#xff0c;最近有不少技术同学来了解webfunny埋点系统&#xff0c;今天主要给大家分享下webfunny埋点系统部署&#xff0c;分为本地部署和线上部署。 还没有试用和…

字节一面面经

1.redis了解吗&#xff0c;是解决什么问题的&#xff0c;redis的应用&#xff1f; Redis 是一种基于内存的数据库&#xff0c;常用的数据结构有string、hash、list、set、zset这五种&#xff0c;对数据的读写操作都是在内存中完成。因此读写速度非常快&#xff0c;常用于缓存&…

第三期书生大模型实战营之XTuner微调个人小助手认知

基础任务 使用 XTuner 微调 InternLM2-Chat-1.8B 实现自己的小助手认知&#xff0c;记录复现过程并截图。 任务结果截图 1. 创建虚拟环境 # 安装一些必要的库 conda install pytorch2.1.2 torchvision0.16.2 torchaudio2.1.2 pytorch-cuda12.1 -c pytorch -c nvidia -y # 安…

2024华数杯数学建模竞赛选题建议+初步分析

提示&#xff1a;DS C君认为的难度&#xff1a;C<A<B&#xff0c;开放度&#xff1a;A<B<C。 综合评价来看 A题适合对机械臂和机器人运动学感兴趣的同学&#xff0c;尤其是有一定编程和优化算法基础的同学。不建议非相关专业同学选择。 B题挑战较大&#xff0…

Go语言实现多协程文件下载器

文章目录 前言流程图主函数下载文件初始化分片下载worker分发下载任务获取下载文件的大小下载文件分片错误重试项目演示最后 前言 你好&#xff0c;我是醉墨居士&#xff0c;最近在开发文件传输相关的项目&#xff0c;然后顺手写了一个多协程文件下载器&#xff0c;代码非常精…