设计模式学习笔记 - 设计模式与范式 -行为型:15.命令模式:如何利用命令模式实现一个游戏后端架构

概述

行为型设计模式只剩下3个模式了,它们分别是:命令模式、解释器模式、中介模式。这 3 个设计模式使用频率低、理解难度大,只在特定的应用场景下才会用到,所以这 3 个设计模式你只需要稍微了解即可。

本章学习其中的命令模式。在学习这个模式的过程中,你可能遇到的最大疑惑是,感觉命令模式没啥用,是一种过度设计,有更加简单的设计思路可以替代。所以,本章讲解的重点是这个模式的设计意图,带你搞清楚到底什么情况下才真正需要使用它。


命令模式的原理解读

命令模式的英文翻译是 Command Design Pattern。在 GoF 的《设计模式》一书中,它是这么定义的:

The command pattern encapsulates a request as an object, thereby letting us parameterize other objects with different requests, queue or log requests, and support undoable operations.

翻译成中文:命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同的请求注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能。

这里再进一步解释下,不然还是有点绕口。

落实到编码实现,命令模式用的最核心的实现手段,是将函数封装成对象。在大部分编程语言中,函数没办法作为参数传递给其他函数,也没法复制给变量。借助命令模式,可以将函数封装成对象。具体来说就是,设计一个包含这个函数的类,实例化一个对象传来传去,这样就可以实现把函数像对象一样使用。从实现的角度来说,它类似我们之前讲过的回调。

当我们把函数封装成对象之后,对象就可以存储下来,方便控制执行。所以,命令模式的主要作用和应用场景,是用来控制命令的执行,比如,异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等等,这才是命令模式能发挥独一无二作用的地方。

命令模式的实战讲解

现在,再结合一个具体的例子来解释下。

假设我们正在开发一个类似《QQ 卡丁车》这样的手游。这种游戏本身的复杂度集中在客户端。后端基本上只负责数据(比如积分、生命值、装备)的更新和查询,所以,后端逻辑相对于客户端来说,要简单很多。

考虑到你可能对游戏开发不熟忙着哩稍微说一些背景知识。

为提高性能,我们会把玩家的信息保存在内存中。在游戏进行的过程中,只更新内存中的数据,游戏结束后,再将内存中的数据存档,也就是持久化到数据库中。为了降低实现的难度,一般来说,同一个游戏场景里的玩家,会被分配到同一台服务商器上。这样,一个玩家拉取同一个游戏场景中的其他玩家信息,就不需要跨服务器去查找了,实现起来就简单了许多。

一般来说,游戏客户端和服务端之间的数据交互是比较频繁的,所以,为了节省网络连接建立的开销,客户端和服务器之间一般采用长连接的方式来通信。通信的格式有很多种,比如 Protocol Buffer、JSON、XML,甚至可以是自定义格式。不管是什么格式,客户端发送给服务器的请求,一般都包括两部分内容:指令和数据。其中,指令也可以称为事件,数据是执行这个指令所需的数据。

服务器在接受到客户端的请求之后,会解析出指令和数据,并且根据指令的不同,执行不同的处理逻辑。对于这样的一个业务场景,一般有两种架构实现思路。

常用的一种实现思路是利用多线程。一个线程接收请求,接收到请求之后,启动一个新的线程来处理请求。具体来讲,就是同一个一个主线程来接收客户端发来的请求。每当接收到请求后,就从一个专门用来处理请求的线程池中,捞出一个空闲线程来处理。

另一种实现思路是在一个线程内轮询接受请求和处理请求。这种处理方式不太场景。尽管它无法利用多线程多核处理的优势,但是对于 IO 密集型的业务来说,它避免了多线程不停切换对性能的损耗,并且克服了多线程编程 Bug 比较难调试的缺点,也算式手游后端服务器开发中比较常见的架构模式了。

接下来就重点讲解下第二种实现方式。

整个手游后端服务器轮询获取客户端发来的请求,获取到请求之后,借助命令模式,把请求包含的数据和处理逻辑封装为命令对象,并存储在内存队列中。然后,再从队列中取出一定数量的命令来执行。执行完后,再重新开始新的一轮轮询。具体代码如下所示。

public interface Command {void execute();
}public class GotDiamondCommand implements Command {// 省略成员变量public GotDiamondCommand(/*数据*/) {// ...}@Overridepublic void execute() {// 执行相应逻辑}
}public class GotStartCommand implements Command {// 省略成员变量public GotStartCommand(/*数据*/) {// ...}@Overridepublic void execute() {// 执行相应逻辑}
}public class HitObstacleCommand implements Command {// 省略成员变量public HitObstacleCommand(/*数据*/) {// ...}@Overridepublic void execute() {// 执行相应逻辑}
}public class ArchiveCommand implements Command {// 省略成员变量public ArchiveCommand(/*数据*/) {// ...}@Overridepublic void execute() {// 执行相应逻辑}
}public class GameApplication {private static final int MAX_HANDLED_REQ_COUNT_REP_LOOP = 100;private Queue<Command> queue = new LinkedList<>();public void mainLoop() {while (true) {List<Request> requests = new ArrayList<>();// 省略从epoll或者select中获取数据,并封装成Request的逻辑,// 注意设置超时时间,如果很长时间没有收到请求,就继续下面的逻辑for (Request request : requests) {Event event = request.getEvent();Command command = null;if (event.equals(Event.GOT_DIAMOND)) {command = new GotDiamondCommand(/*数据*/);} else if (event.equals(Event.GOT_STAR)) {command = new GotStartCommand(/*数据*/);} else if (event.equals(Event.HIT_OBSTACLE)) {command = new HitObstacleCommand(/*数据*/);} else if (event.equals(Event.ARCHIVE)) {command = new ArchiveCommand(/*数据*/);} // 一堆else ifqueue.add(command);}int handledCount = 0;while (handledCount < MAX_HANDLED_REQ_COUNT_REP_LOOP) {if (queue.isEmpty()) {break;}Command command = queue.poll();command.execute();}}}
}

命令模式 VS 策略模式

看了上面的讲解,你可能会觉得命令模式跟策略模式、工长模式非常相似,它们的区别在哪里呢?另外,你可能会感觉学过的很多设计模式都很相似。

实际上,每个设计模式都应该由两部分组成:第一部分是应用场景,即这个模式可以解决哪类问题;第二部分是解决方案,即这个设计模式的设计思路和代码实现。不过,代码实现并不是模式必须包含的。如果你只是单纯的关注解决方案这一部分,甚至只关注代码实现,就会产生大部分设计模式都很相似的错觉。

实际上,设计模式之间的主要区别在于设计意图,也就是应用场景。单纯地看设计思路或代码实现,有些模式确实很相似,比如策略模式和工厂模式。

之前讲策略模式的时候,有讲到过,策略模式包含策略的定义、创建和使用三个部分,从代码结构上来看,它非常像工厂模式。它们的区别在于,策略模式更注重 “策略” 或 “算法” 这个特定的应用场景,用来解决根据运行时状态从一组策略中选择不同策略的问题,而工厂模式侧重封装对象的创建过程,这里的对象没有任何业务场景的限定,可以是策略,但也可以是其他东西。从设计意图上看,这两个模式完全是两回事。

接下来,再看看命令模式和策略模式的区别。你可能会觉得,命令的执行逻辑页可以看做策略,那它是不是就是策略模式了呢?实际上,这两种有一点细微的区别。

在策略模式中,不同的策略具有相同的目的、不同的实现、互相之间可以替换。比如,BubbleSort、SelectionSort 都是为了实现排序,只不过一个是用冒泡算法来实现,另一个是用选择排序算法来实现。而在命令模式中,不同的命令具有不同的目的,对应不同的处理逻辑,并且互相不可替换。

总结

命令模式在平时开发中并不常用,你稍微了解即可。

落实到编码实现,命令模式用到最核心的实现手段,就是将函数封装成对象。我们知道,在大部分编程语言中,函数是没法作为参数传递给其他函数的,也没法赋值给变量。借助命令模式,我们将函数封装成对象,这样就可以实现把函数像对象一样使用。

命令模式的主要作用和应用场景,是用来控制命令的执行,比如异步、延迟、排队执行命令、撤销重做命令、存储命令、给命令记录日志等等,这才是命令模式能发挥独一无二作用的地方。

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

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

相关文章

C++11 数据结构1 线性表的概念,线性表的顺序存储,实现,测试

一 线性表的概念 线性结构是一种最简单且常用的数据结构。 线性结构的基本特点是节点之间满足线性关系。 本章讨论的动态数组、链表、栈、队列都属于线性结构。 他们的共同之处&#xff0c;是节点中有且只有一个开始节点和终端节点。按这种关系&#xff0c;可以把它们的所有…

双指针问题的常见剪枝

双指针问题的常见剪枝&#xff1a; 一.leecode第15题&#xff1a; 给定一个包含 n 个整数的数组 nums&#xff0c;判断 nums 中是否存在三个元素 a &#xff0c;b &#xff0c;c &#xff0c;使得 a b c 0 &#xff1f;请找出所有和为 0 且 不重复 的三元组。 示例 1&…

力扣376 摆动序列 Java版本

文章目录 题目描述代码 题目描述 如果连续数字之间的差严格地在正数和负数之间交替&#xff0c;则数字序列称为 摆动序列 。第一个差&#xff08;如果存在的话&#xff09;可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。 例如&#xff0c; [1, 7, …

leetcode每日一题第四十六天

递归解法 class Solution { public:int search(vector<int>& nums, int target) {return midsearch(nums,target,0,nums.size()-1);}int midsearch(vector<int>& nums, int target, int low,int high){if(low < high){int mid (lowhigh) / 2;if(nums[…

深入了解Fcgiwrap:使CGI脚本与Nginx无缝集成

在Web开发的世界中&#xff0c;CGI&#xff08;Common Gateway Interface&#xff09;是一种早期的服务器端脚本技术&#xff0c;它允许客户端传输信息给应用程序&#xff0c;并获得应用程序返回的响应。随着时间的推移&#xff0c;更高效的解决方案如PHP、Python和Node.js等脚…

【实践篇】RabbitMQ实现队列延迟功能汇总

前言 记录下RabbitMQ实现延迟队列功能的所有实践内容。 前期准备&#xff0c;需要安装好docker、docker-compose的运行环境。 一、安装RabbitMQ 开启RabbitMQ的WEB管理功能。-CSDN博客 二、实现延迟队列的两种方式 RabbitMQ实现延迟队列的两种方式。-CSDN博客 三、实践文…

ChatGLM2-6B_ An Open Bilingual Chat LLM _ 开源双语对话语言模型

ChatGLM2-6B_ An Open Bilingual Chat LLM _ 开源双语对话语言模型 文章目录 ChatGLM2-6B_ An Open Bilingual Chat LLM _ 开源双语对话语言模型一、介绍二、使用方式1、环境安装2、代码调用3、从本地加载模型 4、API 部署 三、低成本部署1、模型量化2、CPU 部署3、Mac 部署4、…

在Windows 10中打开PowerShell的几种方法,总有一种适合你

PowerShell是一种比命令提示符更强大的命令行shell和脚本语言。自Windows10发布以来,它已成为默认选择,并且有许多方法可以打开它。 PowerShell和命令提示符之间的区别是什么 PowerShell的使用更复杂,但它比命令提示符强大得多。这就是为什么它成为超级用户和it专业人员的…

指针全套知识

一&#xff0c;指针基础 指针是C语言中的一种重要数据类型&#xff0c;主要用于存储内存地址。以下是一些关于指针的基本知识&#xff1a; 1. **指针的定义**&#xff1a;指针是一种特殊类型的变量&#xff0c;用于存储内存地址。指针变量的定义形式为 <数据类型> *<…

从0开始创建单链表

前言 这次我来为大家讲解链表&#xff0c;首先我们来理解一下什么是单链表&#xff0c;我们可以将单链表想象成火车 每一节车厢装着货物和连接下一个车厢的链子&#xff0c;单链表也是如此&#xff0c;它是将一个又一个的数据封装到节点上&#xff0c;节点里不仅包含着数据&…

防错设计及原理

目录 1、防错的作用 2、防错的原理 2.1断根原理 2.2保险原理 2.3自动原理 2.4相符原理 2.5顺序原理 2.6隔离原理 2.7层别原理 2.8复制原理 2.9警告原理 2.10缓和原理 防错法&#xff08;Poka-Yoke&#xff09;&#xff0c;又称愚巧法、防呆法&#xff0c;是一种在作…

C++ 类和对象(一)

目录 0.前言 1.面向过程&面向对象 1.1面向过程编程&#xff08;PP&#xff09; 1.2面向对象编程&#xff08;OOP&#xff09; 1.3从C到C 2.类的引入 2.1C语言中的结构体 2.2C中类的引入 2.3结构体与类的区别 2.4为什么引入类 3.类的定义 3.1声明与定义不分离 …

Blast生态借贷协议Pac Finance陷“清算”风波,兄弟项目ParaSpace曾上演内斗

Blast生态协议又出事了。4月11日晚间&#xff0c;有用户发现借贷协议Pac Finance上出现了大量ezETH清算&#xff0c;涉及金额达2400 万美元。官方回应称&#xff0c;系一位智能合约工程师的操作导致Pac Finance发行清算阈值在没有事先通知团队的情况下被意外更改。 目前社区内…

【MATLAB源码-第8期】基于matlab的DPSK的误码率仿真,差分编码使用汉明码(hanming)。

1、算法描述 差分相移键控常称为二相相对调相&#xff0c;记作2DPSK。它不是利用载波相位的绝对数值传送数字信息&#xff0c;而是用前后码元的相对载波相位值传送数字信息。所谓相对载波相位是指本码元初相与前一码元初相之差。差分相移键控信号的波形如概述图所示。 假设相对…

元宇宙漫谈|下一代社交平台是什么样子?

社交是人类历史上极为关键的社会活动&#xff0c;它必不可缺&#xff0c;方式也在不断变化。互联网和通信技术的盛行&#xff0c;带来的是以信息、电话、视频等交流模式的迭代。随即&#xff0c;社交媒体也开始蓬勃发展&#xff0c;成为主要承载大众注意力价值的载体&#xff0…

C++类引用的好处

简化代码&#xff1a;引用可以简化代码&#xff0c;使其更加易读和易懂。通过使用引用&#xff0c;可以避免在函数参数中复制大型对象&#xff0c;从而提高代码的效率和性能。 传递大型对象的效率高&#xff1a;使用引用作为函数参数传递大型对象时&#xff0c;不需要进行对象…

成都百洲文化传媒有限公司电商领域的新锐力量

在电商服务领域&#xff0c;成都百洲文化传媒有限公司凭借其专业的服务理念和创新的策略&#xff0c;正逐渐成为行业内的翘楚。这家公司不仅拥有资深的电商团队&#xff0c;还以其精准的市场定位和高效的服务模式&#xff0c;赢得了众多客户的信赖和好评。 一、专业团队&#…

UDP网络程序

上一章中&#xff0c;我们介绍了socket&#xff0c;以及TCP/UDP协议。这一章带大家实现几个UDP协议的网络服务。我们需要一个 服务端和一个客户端。 1.服务端实现 1.1socket函数 #include <sys/types.h> #include <sys/socket.h>int socket(int domain, in…

Sora视频生成模型:开启视频创作新纪元

随着人工智能技术的飞速发展&#xff0c;视频生成领域也迎来了前所未有的变革。Sora视频生成模型作为这一领域的佼佼者&#xff0c;凭借其卓越的性能和创新的应用场景&#xff0c;受到了广泛的关注与好评。本文将对Sora视频生成模型进行详细介绍&#xff0c;带您领略其魅力所在…

Linux——十个槽位,RWX

Linux——RWX 十个槽位 - 表示文件 d 表示文件夹 l 表示软链接 r权&#xff0c;针对文件可以查看文件内容 针对文件夹&#xff0c;可以查看文件夹内容&#xff0c;如ls命令 w权&#xff0c;针对表示可以修改此文件 针对文件夹&#xff0c;可以在文件夹内&#…