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

news2024/10/7 10:23:43

文章目录

  • 定义
  • 图纸
  • 一个例子:如何切换坦克的攻击方式
          • 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 {

    @Override
    public void attack(GameElement e) {
        if (e.getOwner().equals(getUser().getOwner())) {
            System.out.println("无法攻击友军");
        } else {
            e.setHP(e.getHP() - 5);
        }
    }
}

/**
 * 医疗攻击
 */
public class CureAttack extends AttackBehavior{

    @Override
    public 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{

    @Override
    public void fly() {
        //不会飞
    }
}

/**
 * 螺旋桨飞行
 */
public class PropellerFly extends FlyBehavior{

    @Override
    public 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.coloradmin.cn/o/1583218.html

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

相关文章

深入浅出 -- 系统架构之微服务中OpenFeign最佳实践

前面我们讲了一下 Ribbon 和 RestTemplate 实现服务端通信的方法,Ribbon 提供了客户端负载均衡,而 RestTemplate 则对 http 进行封装,简化了发送请求的流程,两者互相配合,构建了服务间的高可用通信。 但在使用后也会发…

打开Visual Studio后出现Visual Assist报错弹窗

安装了新的VA插件后发现无论如何清理打开VS都会报这个旧版VA报错弹窗,修复VS、重装VA都解决不了 后来进到VS安装目录,删掉一个可疑文件后弹窗再也不出现了

设计模式之迭代器模式(上)

迭代器模式 1)概述 1.概念 存储多个成员对象(元素)的类叫聚合类(Aggregate Classes),对应的对象称为聚合对象。 聚合对象有两个职责,一是存储数据,二是遍历数据。 2.概述 迭代器模式(Iterator Patter…

微信朋友圈定时神器必须拥有!随时随地轻松发圈!

在微信朋友圈这个社交平台上,很多人都希望通过发布内容来进行个人推广或商业营销。但是,在忙碌或是节假日时,想要随意发布内容也会有些麻烦。 今天我要向大家分享的是一个微信朋友圈定时发布的神器,让您随时随地轻松发圈&#xf…

ES11-12

1-ES11-Promise.allSettled Promise.allSettled0)方法返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象数组,每个对象表示对应的promise结果。 简单来说不管成功失败都会调用.then(),然后处理成功和失败的结果 const promises [ …

嵌入式学习第三十二天!(队列)

1. 队列的定义: 队列:是只允许一端进行数据插入,而另一端进行数据删除的线性表。(先进先出FIFO),如下图所示。 队列的应用:缓冲区,即解决高速设备和低速设备数据交互的时候&#xff…

K8S容器空间不足问题分析和解决

如上图,今天测试环境的K8S平台出现了一个问题,其中的一个容器报错:Free disk space below threshold. Available: 3223552 bytes (threshold: 10485760B),意思服务器硬盘空间不够了。这个问题怎么产生的,又怎么解决的呢…

“人工智能+数字人”,让数字技术赋能多领域智能化管理、数字化服务

AI数字人结合了语音合成、语音识别、语义理解、图像处理、虚拟形象驱动等多项AI核心技术,可以实现导览服务、信息播报、互动交流、业务咨询等智能化功能。 如今,AI数字人逐渐被政务、文旅、展馆展厅、博物馆、数字会议、金融、校园等等领域多元化应用&am…

RuoYi-Vue若依框架-vue前端给对象添加字段

处理两个字段的时候有需求都要显示在下拉框的同一行,这里有两种解决方案,一是后端在实体类添加一个对象,加注解数据库忽略处理,在接口处拼接并传给前端,二是在前端获取的数据数组内为每个对象都添加一个字段&#xff0…

Qt | QObject 类中的成员函数存取属性值与动态属性、用反射机制获取属性的信息

1、注册自定义类型与 QMetaType 类 ①、QMetaType 类用于管理元对象系统中命名的类型,该类用于帮助 QVariant 中的类型以及队列中信号和槽的连接。它将类型名称与类型关联,以便在运行时动态创建和销毁该名称。 ②、QMetaType::Type 枚举类型定义了 QMetaType 支持的类型。其…

Golang | Leetcode Golang题解之第11题盛最多水的容器

题目&#xff1a; 题解&#xff1a; func maxArea(height []int) int {res : 0L : 0R : len(height) - 1for L < R {tmp : math.Min(float64(height[L]), float64(height[R]))res int(math.Max(float64(res), tmp * float64((R - L))))if height[L] < height[R] {L} el…

Severt基本使用

severt是让我们自己写一些类,然后把这些类给加载Tomcat中&#xff0c;后续Tomcat收到HTTP请求(来自于浏览器)&#xff0c;就会执行到咱们上面写的代码.从而通过这些代码,完成一定的业务逻辑. 创建项目 此处创建的是一种新的项目的形式称为Maven项目,Maven是Java 中的一个的构建…

微服务项目sc2024第一个子项目

1. 第一个子项目 2.pom文件 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apa…

【机器学习300问】66、ReLU激活函数相对于Sigmoid和Tanh激活函数的优点是什么?ReLU它有局限性吗?如何改进?

一、ReLU相对于Sigmoid和Tanh的优点 &#xff08;1&#xff09;计算效率高 ReLU函数数学形式简单&#xff0c;仅需要对输入进行阈值操作&#xff0c;大于0则保留&#xff0c;小于0则置为0。Sigmoid和Tanh需要指数运算但ReLU不需要。所以相比之下它会更快&#xff0c;降低了神经…

MySQL innoDB存储引擎多事务场景下的事务执行情况

一、背景 在日常开发中&#xff0c;对不同事务之间的隔离情况等理解如果不够清晰&#xff0c;很容易导致代码的效果和预期不符。因而在这对一些存在疑问的场景进行模拟。 下面的例子全部基于innoDB存储引擎。 二、场景&#xff1a; 2.1、两个事务修改同一行记录 正常来说&…

每日一题 — 最大连续 1 的个数III

解法一&#xff1a;暴力枚举 先定义left和right双指针&#xff0c;left先固定在起始位置&#xff0c;遍历right当值等于1的时候&#xff0c;直接跳过&#xff0c;等于0的时候&#xff0c;zero计数器加一当zero等于k的时候&#xff0c;就开始记录此时最大长度是多少然后left加一…

urwid,一个好用的 Python 库!

更多资料获取 &#x1f4da; 个人网站&#xff1a;ipengtao.com 大家好&#xff0c;今天为大家分享一个好用的 Python 库 - urwid。 Github地址&#xff1a;https://github.com/urwid/urwid Urwid 是一个功能强大的 Python 库&#xff0c;用于创建基于文本的用户界面&#xf…

Linux入门攻坚——18、SELinux、Bash脚本编程续

SELinux——Secure Enhanced Linux&#xff08;安全加强的Linux&#xff09;&#xff0c;工作于Linux内核中。 SELinux 主要作用就是最大限度地减小系统中服务进程可访问的资源&#xff08;最小权限原则&#xff09;。采用委任式存取控制&#xff0c;是在进行程序、文件等细节权…

蓝桥杯2023A-05-互质数(Java)

5.互质数 题目描述 给定 a, b&#xff0c;求 1 ≤ x < a^b 中有多少个 x 与 a^b 互质。由于答案可能很大&#xff0c;你只需要输出答案对 998244353 取模的结果。 输入格式 输入一行包含两个整数分别表示 a, b&#xff0c;用一个空格分隔。 输出格式 输出一行包含一个…

【MPI并行程序】完美解决Attempting to use an MPI routine before initializing MPI

文章目录 错误原因解决方案 最近在写并行程序&#xff0c;犯了一个小错误&#xff0c;记录一下&#xff0c;以防止以后再犯。 Attempting to use an MPI routine before initializing MPI&#xff08;在初始化 MPI 之前尝试使用 MPI 例程&#xff09; 错误原因 这个错误通常是因…