如何利用命令模式实现一个手游后端架构?

news/2024/9/18 15:16:29/ 标签: 命令模式

命令模式的原理解读

命令模式的英文翻译是 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.

翻译成中文就是下面这样:

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

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

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

命令模式的实战解析

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

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

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

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

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

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

我们接下来就重点讲一下第二种实现方式。

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

public interface Command {void execute();
}public class GotDiamondCommand implements Command {// 省略成员变量public GotDiamondCommand(/*数据*/) {//...}@Overridepublic void execute() {// 执行相应的逻辑}
}
//GotStartCommand/HitObstacleCommand/ArchiveCommand类省略public class GameApplication {private static final int MAX_HANDLED_REQ_COUNT_PER_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 if...queue.add(command);}int handledCount = 0;while (handledCount < MAX_HANDLED_REQ_COUNT_PER_LOOP) {if (queue.isEmpty()) {break;}Command command = queue.poll();command.execute();}}}
}

命令模式 VS 策略模式

看了刚才的讲解,你可能会觉得,命令模式跟策略模式、工厂模式非常相似,那它们的区别在哪里呢?

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

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

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

有了刚刚的铺垫,接下来,我们再来看命令模式跟策略模式的区别。你可能会觉得,命令的执行逻辑也可以看作策略,那它是不是就是策略模式了呢?实际上,这两者有一点细微的区别。

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

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

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


http://www.ppmy.cn/news/1524955.html

相关文章

能力追上博士生,OpenAI发布最强o1系列模型

9月13日凌晨1点&#xff0c;OpenAI发布o1系列模型&#xff0c;包括o1-preview&#xff08;下称o1预览版&#xff09;和o1-mini。针对这一消息&#xff0c;该公司创始人Sam Altman在X上表示&#xff1a;“no more patience, jimmy.&#xff08;需要耐心等待的时刻结束了&#xf…

蓝桥杯DS18B20程序源码

蓝桥杯DS18B20程序源码解析 蓝桥杯&#xff0c;作为一项全国瞩目的电子设计竞赛&#xff0c;其核心挑战在于参赛者需深度融合单片机编程与各类电子元件的应用能力。在众多项目中&#xff0c;涉及DS18B20数字温度传感器的程序源码尤为引人注目&#xff0c;它巧妙地将单片机技术…

开源项目低代码表单FormCreate中ElementPlus表单使用校验规则示例

在开源项目低代码表单FormCreate 中&#xff0c;可以通过 validate 配置项为表单组件设置验证规则。无论是内置的表单组件还是自定义的表单组件&#xff0c;都支持表单校验。本文将详细介绍验证规则的使用方法&#xff0c;并提供一些示例来帮助您更好地理解和应用这些功能。 源…

ISO 21434与网络安全管理系统(CSMS)的协同作用

ISO/SAE 21434与CSMS&#xff08;网络安全管理系统&#xff09;之间的关系主要体现在以下几个方面&#xff1a; 提供指导框架&#xff1a;ISO/SAE 21434《道路车辆—网络安全工程》是一项国际标准&#xff0c;它为汽车行业提供了实施网络安全管理系统的国际认可的方法和最佳实…

模板匹配应用(opencv的妙用)

在图像处理领域&#xff0c;模板匹配是一种常用的技术&#xff0c;用于在一幅大图中寻找与给定模板最匹配的区域。OpenCV作为一个强大的计算机视觉库&#xff0c;提供了cv2.matchTemplate()函数来实现模板匹配功能。本文将详细介绍OpenCV中的模板匹配技术&#xff0c;包括其原理…

qmt量化交易策略小白学习笔记第64期【qmt编程之获取获取期权全推数据--code_list全推tick数据】

qmt编程之获取期权数据 qmt更加详细的教程方法&#xff0c;会持续慢慢梳理。 也可找寻博主的历史文章&#xff0c;搜索关键词查看解决方案 &#xff01; 获取期权全推数据 获取全推tick数据的函数是用户主动调用的工具。所谓"全推tick数据"&#xff0c;指的是以t…

Android 应用使用theme处理全局焦点框

背景 我的应用有个需求&#xff0c;要求处理keycode事件&#xff0c;进行焦点移动处理&#xff0c;必须定制指定的焦点框。而系统的焦点框是固定了&#xff0c;为了保证平台的一致性&#xff0c;没办法直接修改。 问题 经过沟通&#xff0c;大部分都是自行修改了自己的backg…

ARM base instruction -- cset

CSET Conditional Set sets the destination register to 1 if the condition is TRUE, and otherwise sets it to 0. 如果条件为TRUE&#xff0c;则条件集将目标寄存器设置为1&#xff0c;否则设置为0。 32-bit variant Applies when sf 0. CSET <Wd>, <…

Spark MLlib模型训练—推荐算法 ALS(Alternative Least Squares)

Spark MLlib模型训练—推荐算法 ALS(Alternative Least Squares) 如果你平时爱刷抖音,或者热衷看电影,不知道有没有过这样的体验:这类影视 App 你用得越久,它就好像会读心术一样,总能给你推荐对胃口的内容。其实这种迎合用户喜好的推荐,离不开机器学习中的推荐算法。 在…

SimPO

添加链接描述 优化点两个&#xff1a; gamma长度正则 gamma 原始DPO把两部分母拿出来是 − l o g π r e f ( w i n ) π r e f ( l o s e ) -log \frac{\pi_{ref}(win)}{\pi_{ref}(lose)} −logπref​(lose)πref​(win)​ 由于构建数据集时常常把不满意的大模型输出作为…

操作系统 ---- 调度算法【先来先服务(FCFS)、最短作业优先(SJF)、最高响应比优先(HRRN)】

目录 一、常见的调度算法 1. 先来先服务&#xff08;FCFS, First-Come, First-Served&#xff09; 2. 最短作业优先&#xff08;SJF, Shortest Job First&#xff09; 3. 优先级调度&#xff08;Priority Scheduling algorithm&#xff0c;PSA&#xff09; 4. 轮转调度&am…

python 函数 封装

封装 函数的参数是&#xff1a;变量 def 函数(参数):print(参数)if __name__ __main__:函数(参数)函数(参数2)函数的参数是&#xff1a; 字典 import requests# 定义一个字典 data {} 地址 "https://webdriveruniversity.com/" 请求方法 getdata["url"…

Hi3519DV500_Uboot环境变量的编译和烧录

Hi3519DV500_Uboot环境变量的编译和烧录 U-Boot 环境变量在嵌入式系统的引导加载程序中扮演着重要的角色。它们用于存储系统启动时所需的配置信息&#xff0c;如启动命令、内核和根文件系统的位置、串口波特率等。以下是一些常用的 U-Boot 环境变量及其用途&#xff1a; bootde…

安宝特案例 | AR如何大幅提升IC封装厂检测效率?

前言&#xff1a;如何提升IC封装厂检测效率&#xff1f; 在现代电子产品的制造过程中&#xff0c;IC封装作为核心环节&#xff0c;涉及到复杂处理流程和严格质量检测。这是一家专注于IC封装的厂商&#xff0c;负责将来自IC制造商的晶圆进行保护、散热和导通处理。整个制程繁琐…

使用vuex模仿el-table

1、vuex 在main.js引入 import Vue from vue; import Vuex from vuex;Vue.use(Vuex);const store new Vuex.Store({state: {// 定义要传递的数据datas: []},mutations: {// 定义修改数据的 mutationSET_DATAS(state, newDatas) {state.datas newDatas;}},actions: {// 定义…

Java面试篇基础部分-Java的类加载机制

JVM的类加载 JVM在运行Java文件的时候,类加载分为5个阶段:加载、验证、准备、解析、初始化。在类初始化加载完成之后,就可以使用这个类的信息了。当这个类不需要使用的时候,就可以从JVM进行卸载。 加载 加载是指JVM读取Class文件的操作,并且根据Class的文件描述创建对应的…

聚鼎科技:现在做装饰画是靠谱的吗

在生活的各个角落&#xff0c;艺术以多种形式存在着&#xff0c;而装饰画作为其中的一种&#xff0c;一直以其独特的魅力填充着我们的世界。但在这个快速变化的时代&#xff0c;许多人会问&#xff1a;现在做装饰画还是一个靠谱的选择吗? 装饰画的市场依旧充满生机。随着人们对…

Java ssm超市订单管理系统(详细文档)

学生选课系统论文 摘要 随着互联网趋势的到来&#xff0c;各行各业都在考虑利用互联网将自己推广出去&#xff0c;最好方式就是建立自己的互联网系统&#xff0c;并对其进行维护和管理。在现实运用中&#xff0c;应用软件的工作规则和开发步骤&#xff0c;采用Java技术建设小…

Linux从入门到开发实战(C/C++)Day10-线程

1.概念&#xff1a; 线程是一个函数&#xff0c;是os调度的基本单位 Linux内核在2.2版本之前&#xff0c;是没有线程的概念&#xff0c;只有有限个进程&#xff08;4096&#xff09; 在2.4版本中&#xff0c;有了线程的概念&#xff0c;而且可…

安全工具 | 使用Burp Suite的10个小tips

Burp Suite 应用程序中有用功能的集合 img Burp Suite 是一款出色的分析工具&#xff0c;用于测试 Web 应用程序和系统的安全漏洞。它有很多很棒的功能可以在渗透测试中使用。您使用它的次数越多&#xff0c;您就越发现它的便利功能。 本文内容是我在测试期间学到并经常的主要…