设计模式——2_1 命令(Command)

news2024/9/28 1:25:22

文章目录

  • 定义
  • 图纸
  • 一个例子:空调和他的遥控器
    • 只有控制面板的空调
    • 遥控器
    • 可以撤销的操作
  • 碎碎念
    • 命令和Runnable
    • 命令和事务

定义

把请求封装成一个对象,从而使你可以用不同的请求对客户进行参数化,对请求排队或记录请求日志,以及支持可撤销的操作

在职责链中,我们把不同的动作分支组合在一起,让请求在不同的分支中进行流通,他可以是逻辑上的流通,也可以是封装成一个参数对象在里面流通。而在命令模式中,这种思想进一步升级,他会不分青红皂白的把所有请求封装成命令对象



图纸

在这里插入图片描述

各位道友应该知道有个叫枪的东西吧,一种装上子弹就可以开火的设备

和由枪和子弹构成的模式一样,命令模式是由 实际执行操作的【Receiver】命令对象【ConcreteCommand】 构成,如果把 Receiver 看成 ,那么 ConcreteCommand 就是 子弹

每次要开枪执行指令)之前,你都要获取子弹获取对应的 ConcreteCommand 对象),上膛组合 Receiver 和 ConcreteCommand 对象),最后扣动扳机调用 Receiver 的 Action 方法



一个例子:空调和他的遥控器

假定你入职了一家空调公司,接手了和立式空调有关的模块。不久之后,公司决定给这个立式空调增加一个遥控器(原有的立式空调全部都是通过控制面板上的按钮进行操作的)。正当你摩拳擦掌准备大干一番的时候,前辈留给你的紧耦合代码却让你差点当场骂娘。困难面前,你是要说服上司放弃遥控器的企划,还是连夜删库跑路?又或者重构已有的代码……

铺垫有点太长了,总之这次的例子开始了:


只有控制面板的空调

言归正传,前辈留下的代码是这样的:

在这里插入图片描述

AirConditioner(空调)

/**
 * 空调
 */
public class AirConditioner {

    public static final int MAX_TEMPERATURE = 32;//最高温度
    public static final int MIN_TEMPERATURE = 18;//最低温度
    private static final String[] MODE_ARRAY = {"制冷模式", "睡眠模式", "送风模式"};

    /**
     * 温度
     */
    private Float temperature;

    /**
     * 当前模式
     */
    private Integer modePoint;

    /**
     * 是否是开启的
     */
    private boolean isOn;

    /**
     * 控制面板
     */
    private ControlPanel controlPanel;

    public AirConditioner() {
        controlPanel = ControlPanel.createControlPanel(this);
    }

    public static String[] getModeArray() {
        return MODE_ARRAY.clone();
    }

    public Float getTemperature() {
        return temperature;
    }

    public void setTemperature(Float temperature) {
        if (temperature == null || (temperature >= MIN_TEMPERATURE && temperature <= MAX_TEMPERATURE)) {
            this.temperature = temperature;
        }
    }

    public String getMode() {
        return MODE_ARRAY[modePoint];
    }

    public Integer getModePoint() {
        return modePoint;
    }

    public void setModePoint(Integer modePoint) {
        this.modePoint = modePoint;
    }

    public boolean isOn() {
        return isOn;
    }

    public void setOn(boolean on) {
        isOn = on;
    }
}

ControlPanel

/**
 * 控制面板
 */
public class ControlPanel {

    private final AirConditioner airConditioner;

    private ControlPanel(AirConditioner airConditioner) {
        this.airConditioner = airConditioner;
    }

    public static ControlPanel createControlPanel(AirConditioner airConditioner) {
        ControlPanel controlPanel = new ControlPanel(airConditioner);
        controlPanel.off();
        return controlPanel;
    }

    /**
     * 开机
     */
    public void on() {
        if (!airConditioner.isOn()) {
            //关机模式才可以执行这个动作
            airConditioner.setOn(true);//设定开机
            airConditioner.setTemperature(26f);//默认26度
            airConditioner.setModePoint(0);//默认第一个模式
        }
    }

    /**
     * 关机
     */
    public void off() {
        if (airConditioner.isOn()) {
            //开机状态才可以执行这个动作
            airConditioner.setOn(false);
            airConditioner.setTemperature(null);
            airConditioner.setModePoint(null);
        }
    }

    /**
     * 温度上升1
     */
    public void addTemperature() {
        if (airConditioner.isOn()) {
            airConditioner.setTemperature(airConditioner.getTemperature() + 1);
        }
    }

    /**
     * 温度下降1
     */
    public void lessenTemperature() {
        if (airConditioner.isOn()) {
            airConditioner.setTemperature(airConditioner.getTemperature() - 1);
        }
    }

    /**
     * 下一模式
     */
    public void nextMode() {
        Integer modePoint = airConditioner.getModePoint();
        if (airConditioner.isOn()) {
            if (modePoint + 1 >= AirConditioner.getModeArray().length) {
                airConditioner.setModePoint(modePoint + 1);
            } else {
                airConditioner.setModePoint(0);
            }
        }
    }
}

在这个只有控制面板的立式空调里面,前辈通过 AirConditioner(空调) 来表示一部空调的底层函数,然后通过 ControlPanel(控制面板)Client 提供操作空调内部属性的接口

虽然它可以如预期一般完成任务,但这种设计绝算不上优雅,他的问题主要体现在 控制面板 和空调底层方法之间的耦合过于紧密,现在只有一种类型的空调,如果有底层方法不相同的第二种类型的空调出现,那么这个 控制面板 是无法与其兼容的


遥控器

遥控器 的引入改变了现态,很显然,遥控器 至少需要拥有和 控制面板 一样的效果,而且一个 遥控器 必须可以同时对应多个 空调 对象。也就是说,你只有在最终调用遥控器上面的任务的时候才会知道到底要调用哪个空调上的方法,做出来的效果应该是这样的:

在这里插入图片描述

我们新增了 RemoteController(遥控器) 作为遥控器的实现,而 RemoteControllerControlPanel 的实现唯一的区别就在于 ControlPanelAirConditioner 从他诞生的时候就被定义且无法修改,而 RemoteControllerAirConditioner 在调用命令的时候才会被指定

除此之外,程序中出现了大量的重复代码,这些重复代码分布在 控制面板遥控器 的每一个对应方法中

有没有办法把他们解耦?我的意思是把调用者和他的实现解耦,也就是说做成这样的效果:

在这里插入图片描述


放在本例中,他长这样:

在这里插入图片描述

Executor & Pool

/**
 * 空调命令执行器
 */
public abstract class ACExecutor implements Cloneable {

	/**
	 * 执行命令
	 */
	public abstract void execute(AirConditioner airConditioner);

	/**
	 * 克隆方法,覆盖Object中的clone
	 */
	public ACExecutor clone() {
		try {
			return (ACExecutor) super.clone();
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
			throw new RuntimeException(e);//异常原样抛出
		}
	}
}

public class OnExecutor extends ACExecutor {

	@Override
	public void execute(AirConditioner airConditioner) {
		if (!airConditioner.isOn()) {
			//关机模式才可以执行这个动作
			airConditioner.setOn(true);//设定开机
			airConditioner.setTemperature(26f);//默认26度
			airConditioner.setModePoint(0);//默认第一个模式
		}
	}
}

public class OffExecutor extends ACExecutor {

	@Override
	public void execute(AirConditioner airConditioner) {
		if (airConditioner.isOn()) {
			//开机状态才可以执行这个动作
			airConditioner.setOn(false);
			airConditioner.setTemperature(null);
			airConditioner.setModePoint(null);
		}
	}
}

public class AddTemperatureExecutor extends ACExecutor {

	@Override
	public void execute(AirConditioner airConditioner) {
		if (airConditioner.isOn()) {
			airConditioner.setTemperature(airConditioner.getTemperature() + 1);
		}
	}
}

public class LessenTemperatureExecutor extends ACExecutor {

	@Override
	public void execute(AirConditioner airConditioner) {
		if (airConditioner.isOn()) {
			airConditioner.setTemperature(airConditioner.getTemperature() - 1);
		}
	}
}

public class NextModeExecutor extends ACExecutor {

	@Override
	public void execute(AirConditioner airConditioner) {
		Integer modePoint = airConditioner.getModePoint();
		if (airConditioner.isOn()) {
			if (modePoint + 1 >= AirConditioner.getModeArray().length) {
				airConditioner.setModePoint(modePoint + 1);
			} else {
				airConditioner.setModePoint(0);
			}
		}
	}
}

/**
 * 空调命令执行器的对象池
 */
public class ACExecutorPool {

	//单例相关
	private static final ACExecutorPool INSTANCE = new ACExecutorPool();

	public static ACExecutorPool getInstance() {
		return INSTANCE;
	}

	//原型池
	private final Map<Class<? extends ACExecutor>, ACExecutor> prototypePool = new HashMap<>();

	public ACExecutor createOnExecutor() {
		return getNewObjectByPool(OnExecutor.class);
	}

	public ACExecutor createOffExecutor() {
		return getNewObjectByPool(OffExecutor.class);
	}

	public ACExecutor createAddTemperatureExecutor() {
		return getNewObjectByPool(AddTemperatureExecutor.class);
	}

	public ACExecutor createLessenTemperatureExecutor() {
		return getNewObjectByPool(LessenTemperatureExecutor.class);
	}

	public ACExecutor createNextModelExecutor() {
		return getNewObjectByPool(NextModeExecutor.class);
	}

	/**
	 * 从原型池中获取对应的新命令对象
	 */
	private ACExecutor getNewObjectByPool(Class<? extends ACExecutor> c) {
		if (prototypePool.containsKey(c)) {
			return prototypePool.get(c).clone();
		} else {
			try {
				ACExecutor prototype = c.newInstance();
				prototypePool.put(c, prototype);
				return prototype.clone();
			} catch (InstantiationException | IllegalAccessException e) {
				e.printStackTrace();
				throw new RuntimeException("初始化命令对象原型池的时候出现了异常,具体异常为:" + e.getCause(), e);
			}
		}
	}
}

ControlPanel & RemoteController

/**
 * 控制面板
 */
public class ControlPanel {

	private final AirConditioner airConditioner;

	private ControlPanel(AirConditioner airConditioner) {
		this.airConditioner = airConditioner;
	}

	public static ControlPanel createControlPanel(AirConditioner airConditioner) {
		ControlPanel controlPanel = new ControlPanel(airConditioner);
		controlPanel.off();
		return controlPanel;
	}

	/**
	 * 开机
	 */
	public void on() {
		ACExecutorPool.getInstance().createOnExecutor().execute(airConditioner);
	}

	/**
	 * 关机
	 */
	public void off() {
		ACExecutorPool.getInstance().createOffExecutor().execute(airConditioner);
	}

	/**
	 * 温度上升1
	 */
	public void addTemperature() {
		ACExecutorPool.getInstance().createAddTemperatureExecutor().execute(airConditioner);
	}

	/**
	 * 温度下降1
	 */
	public void lessenTemperature() {
		ACExecutorPool.getInstance().createLessenTemperatureExecutor().execute(airConditioner);
	}

	/**
	 * 下一模式
	 */
	public void nextMode() {
		ACExecutorPool.getInstance().createNextModelExecutor().execute(airConditioner);
	}
}

/**
 * 遥控器
 */
public class RemoteController {

	/**
	 * 开机
	 */
	public void on(AirConditioner airConditioner) {
		ACExecutorPool.getInstance().createOnExecutor().execute(airConditioner);
	}

	/**
	 * 关机
	 */
	public void off(AirConditioner airConditioner) {
		ACExecutorPool.getInstance().createOffExecutor().execute(airConditioner);
	}

	/**
	 * 温度上升1
	 */
	public void addTemperature(AirConditioner airConditioner) {
		ACExecutorPool.getInstance().createAddTemperatureExecutor().execute(airConditioner);
	}

	/**
	 * 温度下降1
	 */
	public void lessenTemperature(AirConditioner airConditioner) {
		ACExecutorPool.getInstance().createLessenTemperatureExecutor().execute(airConditioner);
	}

	/**
	 * 下一模式
	 */
	public void nextMode(AirConditioner airConditioner) {
		ACExecutorPool.getInstance().createNextModelExecutor().execute(airConditioner);
	}
}

在这种实现方式里,我们把具体执行的操作封装到 ACExecutor(空调执行器) 类簇中,为 所有的操作定义了自己的执行类;然后在 client 点击 控制面板遥控器 上的对应按钮的时候,把让他们获取对应的 执行器对象,并执行对应的操作。而 控制面板遥控器 根本不关心 执行器 的底层做了什么操作,这让他们从命令的执行者变成了命令的调度者


听起来很复杂,但是这种复杂是 颗粒度 细致程度导致的(五个命令导致我们需要五个对应的执行者子类),流程上其实是很简单的。以开机为例,我们的程序其实是这样做的:

在这里插入图片描述

这种让程序变得复杂的写法是有价值的,至少可以让你在以下两种情况可以少写很多代码:

  1. 当出现新的操作面板,比如手机APP控制空调的时候,我就可以通过创建新的控制器类并让他调用已有的执行者来实现,而不需要再重复执行者里面的代码
  2. 如果出现了接口一致,但行为不一致的空调,那我依然可以继续使用控制面板和遥控器,只需要建立对应的执行器类簇即可

而这正是一个标准的命令实现


可以撤销的操作

一般我讲到 ”这正是xxx的实现“ 的时候,例子就结束了,但这次是例外

请留意一下上例的一个配角类—— ACExecutorPool,这个类的作用是为我们的程序产出可靠的执行类对象。在上例中,这一个类里,用到了两种模式,分别是 单例原型

单例很好理解,为了让全局都用一个对象池

为什么要用原型呢?每个执行器都用单例不是更节省吗?


这种情况下会用原型只有一种可能,那就是执行器应该是带状态的,而且他的状态是有意义的

那你会说了,不对啊,上例的执行器哪有状态。别急,业务来了


某日,接到通知,我们需要在 控制面板 上添加一个 返回(back) 按钮,用于撤销我们刚刚执行的命令,要怎么实现呢?

如果你没有使用命令模式实现这种功能费死劲,但是在命令模式的框架下,你可以这样做:

在这里插入图片描述

我们在 ACExecutor 中提供了用于回滚的方法 back,而在 控制面板 中我们通过添加 history 列表的方式存储已经执行过的执行器,以便我们回滚

这时候 ACExecutor 的对象就绝不能用单例了,因为他的属性是一种凭证,用于证明这个执行器对象有没有执行成功。此时原型模式就是你比较合适的选择了,因为执行器对象是会被大量创建的,原型可以有效的降低创建执行器的开销(复制初始属性

而这也是命令模式所能实现的功能之一



碎碎念

命令和Runnable

Java的多线程模块中用到了很标准的命令模式

我们通过 Thread 来管控线程,但是线程具体如何执行是由 Runnable 来决定的

也就是说 Thread 本质上其实就是 ReceiverRunnable 才是掌握具体内容的执行器


命令和事务

命令模式中的执行器的颗粒度你是可以自己掌握的,你可以只让他执行单体命令,也可以让他执行多个指令捆绑在一起的复合指令

这就是SQL中的事务一样,他是一个具有 原子性 的整体,一荣俱荣,一损俱损




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

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

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

相关文章

2024美国数学建模竞赛A题完整版思路+代码+数据+后续高质量参考论文

2024 MCM 问题A: 资源 用性和性 &#xff08;完整版见文末&#xff09; 题目翻译&#xff1a; 虽然有些动物物种存在于通常的雌雄两性之外&#xff0c;但大多数物种基本上是雌性或雄性。尽管许 多物种在出生时呈现1:1的性别比&#xff0c;但其他物种则偏离了均等的性别比。这…

jsp粉丝社区系统Myeclipse开发mysql数据库web结构java编程计算机网页项目

一、源码特点 JSP 粉丝社区系统是一套完善的java web信息管理系统&#xff0c;对理解JSP java编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,Myeclipse8.5开发&#xff0c;数据库为Mysql5.0&…

在Vue中如何构建复杂表单?

概述 很有可能&#xff0c;在我们的软件工程旅程中&#xff0c;我们至少要构建一次复杂的表单。本文将介绍如何创建一个复杂的表单&#xff0c;该表单可以使用一些Vue特性(如v-for和v-model)逐步增强。它还提供了一些基本的Vue核心功能的复习&#xff0c;这些功能将在您日常使…

Swift 入门之自定义类型的模式匹配(Pattern Matching)

概览 小伙伴们都知道 Swift 是一门简洁、类型安全、极富表现力以及“性感迷人”的编程语言。 和大多数语言一样&#xff0c;在 Swift 中也有一些隐藏着的、不为人知的宝藏特性。利用它们我们可以极大增加撸码的愉悦和成就感。 其中&#xff0c;模式匹配&#xff08;Pattern …

Linux---动静态库

动静态库的相关概念 静态库&#xff08;.a&#xff09;&#xff1a;程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库动态库&#xff08;.so&#xff09;&#xff1a;程序在运行的时候才去链接动态库的代码&#xff0c;多个程序共享使用库的…

【HarmonyOS】鸿蒙开发之ArkTs初步认识——第2.1章

ArkTs简介 ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript&#xff08;简称TS&#xff09;生态基础上做了进一步扩展&#xff0c;继承了TS的所有特性&#xff0c;是TS的超集。 以下图可以展示Js&#xff0c;TS&#xff0c;ArkTs的关系 ArkTs基础语…

比收费还好用,6个自学python必看网站

今天给大家分享几个自学python经常用到的网站&#xff0c;非常实用&#xff0c;建议收藏&#xff01; 1.中文版官方教程 https://docs.python.org/zh-cn/3/tutorial/errors.html#defining-clean-up-actions 你可以从这里下载Python、使用、学习Python。官方文档自然是最权威的…

【vue】报错 Duplicate keys detected 解决方案

错误描述&#xff1a;Duplicate keys detected. This may cause an update error.错误直译&#xff1a;检测到重复的键。这可能会导致错误。错误原因&#xff1a;有相同父元素的多个子元素的v-for有相同的key值。 解决方法&#xff1a; return:{dataList:[{name:张三&#xf…

10秒搞定!隔壁奶奶都能搞定的幻兽帕鲁、雾锁王国开服指南

最近《幻兽帕鲁》和《雾锁王国》非常火热&#xff0c;玩过的小伙伴们都说非常上头&#xff01;有跟朋友对战需求的小伙伴们可以通过本文拥有一台高性价比的专用服务器&#xff0c;随时可以用来跟朋友一起玩游戏&#xff01; 敲重点&#xff01;&#xff01;&#xff01; 步骤…

86.网游逆向分析与插件开发-物品使用-物品丢弃的逆向分析与C++代码的封装

内容参考于&#xff1a;易道云信息技术研究院VIP课 上一个内容&#xff1a;物品使用的逆向分析与C代码的封装-CSDN博客 码云地址&#xff08;ui显示角色数据 分支&#xff09;&#xff1a;https://gitee.com/dye_your_fingers/sro_-ex.git 码云版本号&#xff1a;7563f86877c…

关于bypassuac的探究——思考

我们所使用的几个api&#xff0c;如RegCreateKeyExA、RegSetKeyExA都是直接修改注册表的操作&#xff0c;这种操作应该被归类为敏感操作&#xff0c;那么这里会不会被杀软拦截呢&#xff0c;去测试一下 windows defender正常上线 获取到的权限也是bypassuac后的权限 再看一下…

备战蓝桥杯---搜索(BFS基础1)

如果DFS是时光回溯&#xff0c;那么BFS则是影子分身。 下面是它的定义&#xff1a; 下面直接看题&#xff1a; 十分经典&#xff0c;在这注意存的时候可以用i*mj的形式&#xff0c;可以当作模板&#xff0c;下面是AC代码&#xff1a; #include<bits/stdc.h> using name…

阿里云AI通义千问出bug,今天修复了,一切都是莫名其妙,国产AI又可以了?

怎么隔一天就好了&#xff1f; 引言我的处理感想再次提问AI代码结尾 引言 前天我的阿里云AI 通义千问 不是抽风了嘛 详情见 阿里云AI通义千问出bug,解决不了直接弃,开始对国产AI由支持变失望 就是我的一些对话莫名消失了 我的处理 我在这里进行了反馈 但是没有回应 我以为…

NUXTJS安装始终报错无法正常运行问题解决

近日在了解NuxtJS&#xff0c;按照官方给出方法进行安装后&#xff0c;不是报错&#xff0c;就是安装成功后运行不了。执行npm run dev后始终运行出错&#xff0c;判断肯定是对应版本问题&#xff0c;沿着这方向研究&#xff0c;最终运行成功了。 文档地址&#xff1a;安装 - …

后端——go系统学习笔记(不断更新中......)

数组 固定大小 初始化 arr1 : [3]int{1, 2, 3} arr2 : [...]int{1, 2, 3} var arr3 []int var arr4 [4]int切片 长度是动态的 初始化 arr[0:3] slice : []int{1,2,3} slice : make([]int, 10)len和cap len是获取切片、数组、字符串的长度——元素的个数cap是获取切片的容量—…

telnet笔记

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、场景二、介绍1.测试端口2.访问百度3. 简单的爬虫 前言 最近telnet命令用的比较多&#xff0c;所以记录一下。 一、场景 ping应该是大家最常用的命令&…

算法学习——华为机考题库6(HJ36 - HJ40)

算法学习——华为机考题库6&#xff08;HJ36 - HJ40&#xff09; HJ36 字符串加密 描述 有一种技巧可以对数据进行加密&#xff0c;它使用一个单词作为它的密匙。下面是它的工作原理&#xff1a;首先&#xff0c;选择一个单词作为密匙&#xff0c;如TRAILBLAZERS。如果单词中…

51单片机学习笔记 --步进电机驱动说明

文章目录 工作原理代码编写驱动方式全步进驱动半步进驱动微步进驱动 工作原理 工作原理简要说明&#xff0c;和单片机一起配合使用的步进电机多为28BYJ28 五线四相步进电机&#xff0c;配合ULN2003驱动板进行控制&#xff0c;如图所示&#xff0c;对于扭矩、精度要求较高的还有…

HiSilicon352 android9.0 开机视频调试分析

一&#xff0c;开机视频概念 开机广告是在系统开机后实现播放视频功能。 海思Android解决方案在原生Android基础上&#xff0c;增加了开机视频模块&#xff0c;可在开机过程中播放视频文件&#xff0c;使用户更好的体验系统开机过程。 二&#xff0c;模块结构 1. 海思自研开机…

舔狗送女生生日祝福在线源码,可设置查看密码,附带搭建教程

&#x1f389; 女神专属生日祝福源码 &#x1f389; ✨ 在这个特别的日子里&#xff0c;想要为心中的女神送上一份独一无二的生日祝福吗&#xff1f;快来试试这款专为舔狗设计的生日祝福源码吧&#xff01; &#x1f48c; 这款源码不仅可以展示你满满的诚意和祝福&#xff0c…