设计模式——2_8 策略(Strategy)

文章目录

  • 定义
  • 图纸
  • 一个例子:如何切换坦克的攻击方式
          • GameElement(游戏元素)
          • TankFactory(坦克工厂)
          • Tank(坦克)
    • 医疗车和飞行车
    • 策略模式
          • Behavior(行为)
          • Tank
          • TankFactory
  • 碎碎念
    • 策略和状态
    • 为什么我们需要策略模式,是继承不好用吗?
      • 那我们为什么要用组合呢?

定义

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




图纸

在这里插入图片描述




一个例子:如何切换坦克的攻击方式

假定我们现在要设计一个RTS(即时战略)游戏,这个游戏中玩家主要通过战车工厂生产坦克来实现对对手进行攻击以取得胜利,为此我们必须创建属于坦克的类,就像这样:

在这里插入图片描述

GameElement(游戏元素)
/*** 游戏元素*/
public class GameElement {/*** 所属者* 一般来说这个应该是玩家,这里简化用String代替*/private String owner;/*** 生命值*/private int HP;/*** 单位类型*/private String type;public GameElement(String type, String owner, int HP) {this.type = type;this.owner = owner;this.HP = HP;}public String getType() {return type;}public void setType(String type) {this.type = type;}public String getOwner() {return owner;}public void setOwner(String owner) {this.owner = owner;}public int getHP() {return HP;}public void setHP(int HP) {this.HP = HP;}
}
TankFactory(坦克工厂)
/*** 战车工厂*/
public class TankFactory extends GameElement {public TankFactory(String owner) {super("战车工厂", owner, 20);}/*** 制造一辆坦克*/public Tank makeTank() {return new Tank(getOwner());}
}
Tank(坦克)
/*** 坦克类*/
public class Tank extends GameElement {public Tank(String owner) {super("坦克", owner, 10);}public void attack(GameElement e) {if(e.getOwner().equals(getOwner())){System.out.println("无法攻击友军");}else {e.setHP(e.getHP() - 5);}}public void move() {System.out.println("坦克进行了移动");}
}

首先,我们将所有游戏内玩家可以操控的元素通过一个根类体现出来,也就是 GameElement(游戏元素)。所有的游戏元素都必然会有自己所属的 owner(玩家)HP(生命值)type(类型)

接着,我们给 Tank(坦克) 赋予了 attack(攻击)move(移动) 两种能力,每个 Tank 在攻击的时候都会先判断被攻击者是否是友军,如果不是的话,降低被攻击方的生命值

最后,所有的坦克都是从 TankFactory(战车工厂) 中被生产出来的,而且 TankFactory 自身也是一个 GameElement 所以有自己的 所属玩家生命值



医疗车和飞行车

为了增加游戏的可玩性,我们添加了两种不同的坦克,功能是这样的:

  1. 医疗车:可以攻击己方单位,攻击时给对方回血
  2. 飞行车:移动时既可以在地面跑,也可以在空中飞行

加上原有的坦克,我们的游戏中出现了三种类型的坦克,他们的本质都是坦克,都应该拥有 moveattack 方法,只是不同类型的坦克需要对对应的方法进行重写。于是第一种解决方案跃然纸上,我必须立刻使用继承!就像这样:

在这里插入图片描述

我们通过继承实现了各种坦克的行为,这是没问题的,可是在编码的时候却很别扭。为了迁就 FlyTank 的飞行能力,我们给所有坦克类上都添加了 fly 方法。但事实上,这个方法除了对 FlyTank 有意义之外毫无作用


那你会说了,那我直接把 Tank 根类做成一个抽象类,默认实现一个空的 fly 方法不就行了吗?

这个办法可以,只不过治标不治本,将来策划突发奇想,整一个会飞的医疗车咋办?那我就只能再建一个 FlyCureTank

到这里,我们陷入了泥潭,我们创建新的子类不再是因为存在不同类型的内容,而是在不断的把原有内容进行排列组合,每有一种新组合,我们就必须成倍的增长子类(这取决于有多少种攻击方式和多少种移动方式),这显然违背了我们的初衷

而且将来不熟悉程序的人接手我们的代码时,他一定会奇怪,为什么在 Tank 类里面,会有一个空的 fly 方法,他真的有存在的意义吗?


我们不禁要思考,有没有办法可以直接把 movefly 内的算法直接提取出来,复用他?

答案当然是肯定的,要不然为啥要讲策略模式



策略模式

就像这样:

在这里插入图片描述

Behavior(行为)
/*** 攻击行为*/
public abstract class AttackBehavior {/*** 使用策略的元素*/private GameElement user;public GameElement getUser() {return user;}public void setUser(GameElement user) {this.user = user;}public abstract void attack(GameElement e);
}/*** 普通攻击*/
public class BasicAttack extends AttackBehavior {@Overridepublic void attack(GameElement e) {if (e.getOwner().equals(getUser().getOwner())) {System.out.println("无法攻击友军");} else {e.setHP(e.getHP() - 5);}}
}/*** 医疗攻击*/
public class CureAttack extends AttackBehavior{@Overridepublic void attack(GameElement e) {if(e.getOwner().equals(getUser().getOwner())){e.setHP(e.getHP() + 3);//恢复三点生命值}else {System.out.println("无法攻击敌军");}}
}/*** 飞行策略*/
public abstract class FlyBehavior {/*** 使用策略的元素*/private GameElement user;public GameElement getUser() {return user;}public void setUser(GameElement user) {this.user = user;}public abstract void fly();
}public class NoFly extends FlyBehavior{@Overridepublic void fly() {//不会飞}
}/*** 螺旋桨飞行*/
public class PropellerFly extends FlyBehavior{@Overridepublic void fly() {System.out.println("采用螺旋桨飞行");}
}
Tank
/*** 坦克类*/
public class Tank extends GameElement {/*** 攻击策略*/private AttackBehavior attackBehavior;/*** 飞行策略*/private FlyBehavior flyBehavior;public Tank(String owner) {super("坦克", owner, 10);//默认使用普通攻击setAttackBehavior(new BasicAttack());//默认无法飞行setFlyBehavior(new NoFly());}public void attack(GameElement e) {attackBehavior.attack(e);//调用攻击策略的攻击方法}public void move() {System.out.println("坦克进行了移动");}/*** 飞行*/public void fly() {flyBehavior.fly();//调用飞行策略的飞行方式}public void setAttackBehavior(AttackBehavior attackBehavior) {this.attackBehavior = attackBehavior;attackBehavior.setUser(this);}public void setFlyBehavior(FlyBehavior flyBehavior) {this.flyBehavior = flyBehavior;flyBehavior.setUser(this);}
}
TankFactory
/*** 战车工厂*/
public class TankFactory extends GameElement {public TankFactory(String owner) {super("战车工厂", owner, 20);}/*** 制造一辆基础坦克*/public Tank makeBasicTank() {return new Tank(getOwner());}/*** 制造一辆医疗车*/public Tank makeCureTank() {Tank tank = makeBasicTank();tank.setAttackBehavior(new CureAttack());return tank;}/*** 制造一辆用螺旋桨飞行的坦克*/public Tank makePropellerTank() {Tank tank = makeBasicTank();tank.setFlyBehavior(new PropellerFly());return tank;}
}

我们去掉了 Tank 所有因为不同的行为方式而出现的子类

那这些行为方式去哪了呢?他们去了 Behavior(行为) 类簇中

我们把攻击和飞行这两种行为,独立到两个不同的算法类簇中,这样一来不同的攻击方式和飞行方式之间可以随意的组合。而在这个过程中,我们不需要动 Tank 的类簇,这跟他没关系。

请注意 TankFactory 在这个实现中做的事情,他没有根据不同类型的坦克产出不同的子类,而是给他们装上对应的算法类对象,以实现不同坦克的体现


这种写法让你不单在创建 Tank 对象的时候可以随意组合行为模式,他甚至在游戏进行的过程中都可以改变一辆坦克的行为模式,只要改变对应 Tank 对象的对应行为类对象就可以实现了,这是继承无法触及的领域

而这正是一个标准的策略模式实现




碎碎念

策略和状态

策略模式和状态模式就像是本体和镜像之间的关系

他们都是实现算法和算法调用者之间的解耦,而且实现这种解耦的方式都是通过把 不同的算法 封装成 算法类簇 实现的


那他们有区别吗?

  • 策略模式通过从外部直接切换对象即将要执行的算法来改变对象的行为
  • 状态模式通过改变内部的状态来实现算法对象的切换以改变对象的行为

这就是他们本质的区别

策略模式一定是外界 直接 驱动的,他把组装的“权限“交给了外部。所以对上级代码来说,当前这个对象将要执行哪个算法对象是显而易见的

状态模式不是这样的,一个对象内部状态的改变未必都是外部驱动的,他也可以是内部执行某些动作后触发的变化。而且即使外部去驱动这种变化,上级代码看到的也是自己改变状态的动作,而不是直接和算法对象之间的关联。也就是说,上级代码对执行算法的对象内部到底发生什么事情感知不强,甚至有的时候完全无感,毕竟他只是推倒了多米诺骨牌的第一张


这样的区别直接导致了状态模式的可拓展性终究比不上策略模式。在新的算法簇加入的情况下,策略模式可以通过新增一个算法类的方式直接把新的算法加入到程序中;而对状态模式来说,你只能去修改已被封装的代码

那你会说,不对啊,为什么我用状态模式要新增算法类的时候不可以对应的新增一个算法调用类的子类的?

从技术上来说是没问题的,而且对应的例子在前面的 状态模式 一文里面的转笔刀里已经演示过了。可是你有没有想过如果算法越来越多,是不是意味着每一种新算法都要对应一种新的算法调用类。这样一来,算法类簇和算法调用类簇之间甚至有可能形成 平行类层次(这个概念在前面的 工厂方法 一文已经聊过,这里不再赘述)



为什么我们需要策略模式,是继承不好用吗?

如果把设计模式比喻成一部小说,那么策略模式就是在我心目中当之无愧的主人公

几乎所有的 OO 设计模式出现的初衷,都是为了把改变的部分和不变的部分解耦,以实现代码的复用和动态组合


那你会说了,不对啊,继承不是也实现这个效果吗?

我们把多个类所共有的共有部分抽象出来,并通过继承的形式在子类中实现个性化,这难道不是继承诞生的初衷吗?

假如我们再往前推一步,就会发现其实策略模式所能实现的所有效果,我们用继承也都可以实现:在上例中,我们完全可以把多功能车设置成一个超类,然后把攻击车、医疗车和飞天坦克作为多功能车的子类

但是策略模式放弃了继承,而是采用把部分算法“委托”出去的方式来组合。这不仅是给我们提供了一种解题思路,而是给我们提供了一种思考方式,尝试把继承转移到组合中

那我们为什么要用组合呢?

因为 有一个是一个 有用得多

【是一个】是系统设计者预设的 模具,在实现效果的时候,你只能去找合适的模具;如果没有,你就得再刻一个

【有一个】是在 搭积木。框架的设计者创造的是积木块,是写字时用的点竖横折钩;没有人规定积木要如何组合在一起,就像没有人规定笔画要如何组合一样(当然乱组合的话别人不认识这个字,但别人不认不意味着你不能这样写)。而处理具体业务的程序员的任务则是把组件组装起来以产出更大的组件或成品

高下立判,【有一个】的方式为系统带来了巨大的弹性,他不仅实现了模块颗粒度的细化,还实现这些模块之间在执行期间的动态组合


当然,即便如此,抽象、继承、多态这些概念也没有什么问题,还是那句话,知识怎么可能会有问题,问题一定是出在使用他的人身上。这些概念不会让你立刻就能设计出优秀的系统,她们是锛凿斧锯,可是设计师关注的不是如何切割这块木材,而是如何搭建一个具有弹性的系统,一如搭建一间可以屹立百年的高楼





万分感谢您看完这篇文章,如果您喜欢这篇文章,欢迎点赞、收藏。还可以通过专栏,查看更多与【设计模式】有关的内容

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

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

相关文章

MySQL高级(索引语法、创建索引、查看索引、删除索引)

创建索引 create [unique | fulltext] index index_name on table_name (index_col_name,...); 查看索引 show index from table_name; 删除索引 drop index index_name on table_name; 案例演示: 先来创建一张表 tb_user,并且查询测试数据。 cre…

男女恋爱话术聊天对话回复矩阵版h5微信抖音QQ快手小程序app开发

男女恋爱话术聊天对话回复矩阵版h5微信抖音QQ快手小程序app开发 支持SAAS、支持独立加密、支持独立开源、价格不同。 买就送28W话术数据库 聊天神器 支持16种Al语气、男女版切换、矩阵运营、送后端转化渠道 支持微信公众号微信小程序抖音快手小程序可打包APP 这是一款什么软…

java开发的4套智慧系统源码 智慧校园系统源码 智慧工地系统源码 智慧城管系统源码

4套java智慧系统源码 智慧校园系统源码 智慧工地系统源码 智慧城管系统源码 3D 智能导诊系统源码 Java智慧校园系统源码 智慧学校源码 微信小程序电子班牌 智慧校园系统简介: 智慧校园的建设逐渐被师生、家长认可接受,智慧校园通过对在校师生、教务等…

MySQL学习笔记(数据类型, DDL, DML, DQL, DCL)

Learning note 1、前言2、数据类型2.1、数值类型2.2、字符串类型2.3、日期类型 3、DDL总览数据库/表切换数据库查看表内容创建数据库/表删除数据库/表添加字段删除字段表的重命名修改字段名(以及对应的数据类型) 4、DML往字段里写入具体内容修改字段内容…

MySQL学习笔记2——基础操作

基础操作 一、增删改查1、添加数据2、删除数据3、修改数据4、查询语句 二、主键三、外键和连接1、外键2、连接 一、增删改查 1、添加数据 INSERT INTO 表名[(字段名[,字段名]…)] VALUES (值的列表); --[]表示里面的内容可选添加数据分为插入数据记录和插入查询结果 插入数据…

Patreon是什么?如何用虚拟卡订阅Patreon上的艺术家?详细教程

Patreon是什么? Patreon 是目前世界上最受欢迎的会员平台之一。 它在过去几年中引起了不小的轰动,目前被全球的内容创作者所使用。 内容创作者和艺术家通常很难让粉丝在经济上支持他们。 通过使用像 Patreon 这样的平台,创作者和艺术家可以…

arcgis使用面shp文件裁剪线shp文件报错

水系数据裁剪,输出为空: ArcGIS必会的几个工具的应用 --提取、分割、融合、裁剪(矢)、合并、追加、镶嵌、裁剪(栅)、重采样_arcgis分割-CSDN博客 下面的方法都不行: ArcGIS Clip(裁…

Linux 安装系统可视化监控工具 Netdata

目录 About 监控工具 NetdataLinux 系统安装 Netdata关于 openEuler1、查看内核信息2、查看主机信息3、查看 dnf 包管理器的版本 Netdata 安装1、更新系统环境相关 rpm 包2、查看 netdata 包信息3、安装 netdata 包4、编辑 netdata.conf 配置5、启动 netdata 服务6、查看 netda…

liunx环境变量学习总结

环境变量 在操作系统中,环境变量是一种特殊的变量,它们为运行的进程提供全局配置信息和系统环境设定。本文将介绍如何自定义、删除环境变量,特别是对重要环境变量PATH的管理和定制,以及与环境变量相关的函数使用。 自定义环境变…

李廉洋:4.9黄金屡创新高。黄金原油晚间最新分析建议。

但当下不管是战争因素所带来的避险情绪影响还是美国降息与否所带来的经济影响都无疑还是支撑着黄金继续走高,那么接下来,只要市场不出现较大的利空影响,黄金都不会有较大的回调力度,所以我们当下不管是短线还是长线仍旧以继续看多…

基于SSM+Jsp+Mysql的物流管理系统

开发语言:Java框架:ssm技术:JSPJDK版本:JDK1.8服务器:tomcat7数据库:mysql 5.7(一定要5.7版本)数据库工具:Navicat11开发软件:eclipse/myeclipse/ideaMaven包…

到底有什么是 Node.js 无法实现的?

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,它使得 JavaScript 能够脱离浏览器在服务器端运行。Node.js 以其非阻塞 I/O 和事件驱动的特性而广受欢迎,尤其在构建快速、可伸缩的网络应用方面表现出色。然而,尽管 Node.js 非常强…

定制一套ERP系统怎么样?大概要多少钱?

定制一套ERP系统怎么样?大概要多少钱?这篇内容3000字,纯手打。全部都是我们9年来沉淀的实际经验。 作为有9年系统定制开发经验的乙方厂商,定制ERP系统可以粗略划分为3个方向: ERP管理系统模板,可直接查看和…

MySQL中数据库、表的操作

文章目录 一、管理数据库1.1、连接数据库1.2、创建库1.3、选择数据库1.4、修改数据库名称1.5、查看数据库信息1.6、删除库 二、定义数据表字段2.1、数据表字段的数据类型2.2、数据表字段属性2.3、约束讲解2.3.1、约束的定义1)为什么需要约束2)什么是约束…

Matplotlib实现数据可视化

Matplotlib是Python中应用较为广泛的绘图工具之一,首次发布于2007年。它在函数设计上参考了MATLAB,因此名字以"Mat"开头,中间的"plot"代表绘图功能,结尾的"lib"表示它是一个集合。Matplotlib支持众…

Unity多线程简单示例

using UnityEngine; using System.Threading;public class texxxst : MonoBehaviour {Thread thread;void Start(){// 创建一个新的线程,并传入要执行的方法thread new Thread(new ThreadStart(DoWork));// 启动线程thread.Start();}void DoWork(){for (int i 0; …

高颜值高性能的开源免费自托管照片和视频备份方案:Immich

Immich:安全存储您的珍贵记忆,高颜值且高性能的自托管照片与视频备份解决方案,让您随时随地无忧回顾美好时光。- 精选真开源,释放新价值。 概览 在数字化时代,我们的照片和视频越来越多,如何安全、有效地备…

solidworks镜像实体怎么用

在SolidWorks中,镜像实体功能用于复制并反转实体或特征,使其沿着指定的基准面对称。以下是使用SolidWorks镜像实体的基本步骤: 1. 打开模型:首先打开SolidWorks软件,并加载您想要镜像的三维实体模型。 2. 找到镜像命…

揭秘操作系统:核心功能与Linux系统解析

1.引言 在先前探讨中,我们了解到计算机主机内部的硬件资源需要一种高效管控手段,由此催生了操作系统的诞生。操作系统(Operating System,简称OS),是计算机生态系统中不可或缺的核心组件,以其复杂…