前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >常见设计模式介绍

常见设计模式介绍

作者头像
韩伟
发布2021-12-05 13:41:15
5770
发布2021-12-05 13:41:15
举报
文章被收录于专栏:韩伟的专栏韩伟的专栏

策略模式 & 接口

? 设计模式的模式 ? “接口”,是为了你来扩展的我的程序;而不是我来扩展你的程序

设计目的1. 希望能对“具体”的实现进行替换、升级、并存 2. 不断积累各种“具体”的实现方案

设计要点1.把要完成的功能以“接口”定义 2.切换不同实现类的对象,实现不同的处理细节

例子

GameServer 网络模块,支持多种网络编码协议

网络数据编解码接口:

C++ ///@brief 编码器基类接口。 class Codec { public: Codec() {} virtual ~Codec() {} /** * @brief 把对象编码到缓冲区 */ virtual int Encode(char *buf, size_t len, const MsgObj &obj) = 0; /** * @brief 从缓冲区中把对象解码出来 */ virtual int Decode(const char *buf, size_t len, MsgObj *obj) = 0; /** * @brief 创建一个此网络编码的对象 */ virtual MsgObj *CreateMsgObj(MessageType type) = 0; /** * @brief 删除一个此网络编码的对象 * 对应于 CreateMsgObj() ,用于删除对象 */ virtual void DestroyMsgObj(MsgObj *obj) = 0; };

每个服务器启动时可选择具体编解码协议:

C++ int main(int argc, char **argv) { ....... Codec *codec = new JsonCodec(); // 选择策略 Server *server = new Server(); server->set_codec(codec); // 设置策略 ..... // 初始化服务器 int rt = game_server->Init(&cfg); if (rt) { std::cerr << "Server Init() error: " << rt << std::endl; return -1; } // 陷入阻塞执行 game_server->Start(); return 0; }

java.sql 包,支持各种数据库服务器

Java import java.sql.*; // 几乎全部是接口类(C++ 中的纯虚类) public class FirstExample { public static void main(String[] args) { try{ // 以反射方式选择策略,具体包含代码的类 Class.forName("com.mysql.jdbc.Driver"); // 设置策略:使用 MySQL Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/emp","root","123456"); // 具体数据库的操作 Statement stmt = conn.createStatement(); String sql; // SQL 也是一种“接口”,称为 DSL sql = "SELECT id, first, last, age FROM Employees"; ResultSet rs = stmt.executeQuery(sql); // 获取数据结果,也是接口 while(rs.next()){ //Retrieve by column name int id = rs.getInt("id"); int age = rs.getInt("age"); String first = rs.getString("first"); String last = rs.getString("last"); ...... } ...... }catch(SQLException se){ .... } } }

命令模式 & 数据驱动& 反射

? 命令模式,是实现数据驱动的一种面向对象的方法? 反射是实现命令模式的最常用手段

设计目的1. 不同的数据,以不同的方式处理,2. 希望入口模块保持简洁,避免大段的 if/else 和 switch/case3. 不同的行为具有不同的数据格式,不希望耦合复杂的数据处理

设计要点1. 把所有要处理的数据,都抽象为“命令”2. 每个“命令”对象,具备各自特有的数据格式,以及配套的数据处理方法,避免了大量的处理逻辑判断各自的数据格式正确性3. 和“策略模式”的关系:根据不同的数据结构,自动使用不同的“策略”

例子

GameServer 请求处理模块Handler

定义网络消息处理接口

C++ class Handler { public: virtual std::string GetName() = 0; virtual int Process(const MsgObj &request, MsgObj *response, Server *server) = 0; };

由于没有反射,采用模板类进行静态绑定,收到数据之后,根据命令本身的类型参数,进行类型转换

C++ template <typename Q, typename QT = Q, typename S = Q, typename ST = QT, typename NT = ST> class HandlerCast : public Handler { public: /** * @brief 处理业务逻辑的调用流程 * 从 Codec 和 Handler 之间转换请求、响应的类型,调用 Handle() */ virtual int Process(const MsgObj &request, MsgObj *response, Server *server) { // 数据命令转码 const StrMsgObjCast<Q> *request_obj = dynamic_cast<const StrMsgObjCast<Q> *>(&request); StrMsgObjCast<S> *response_obj = dynamic_cast<StrMsgObjCast<S> *>(response); QT req_obj; ST res_obj; QT *req_ptr = &req_obj; ST *res_ptr = &res_obj; int rt = 0; // 调用请求转码成为对象 ........ // 发起处理逻辑 rt = Handle(*req_ptr, res_ptr, request.fd(), request_obj->session_id(), server); // 调用响应对象转码 ......... return rt; } /** * @brief 具体处理逻辑 */ virtual int Handle(const QT &req_obj, ST *res_obj, int fd, int sess_id, Server *server) = 0; ...... };

实现网络消息处理的具体类

C++ // 删除房间命令,命令数据为 JSON 格式 class DeleteRoomHandler : public HandlerCast<Json::Value> { public: virtual std::string GetName(); virtual int Handle(const Json::Value &req_obj, Json::Value *res_obj, int fd, int sess_id, Server *server); };

注册消息处理模块

C++ int main(int argc, char **argv) { ...... // 网络等其他初始化代码 DispatchProcessor *processor = new DispatchProcessor(); Server *server = new Server(); server->set_processor(processor); //设置命令接收处理器 ....... // 业务逻辑组件 RoomCollection *rooms = new RoomCollection(); processor->Register(new CreateRoomHandler()); processor->Register(new DeleteRoomHandler()); // 注册一条命令 processor->Register(new EnterRoomHandler()); processor->Register(new LeaveRoomHandler()); processor->Register(new SendProgressHandler()); processor->Register(new SendFrameHandler()); ...... // 组装服务器对象 ...... game_server->AddComponent(rooms); ....... // 初始化服务器 int rt = game_server->Init(&cfg); if (rt) { std::cerr << "Server Init() error: " << rt << std::endl; return -1; } // 陷入阻塞执行 game_server->Start(); return 0; }

如果语言具备反射功能,可以把命令数据直接反序列化为一个命令对象,命令对象根据自己身上的属性进行操作,而不是通过“处理方法”的参数获取对象属性

“撤销(Undo)”和“重做(Redo)”

定义一个游戏中的角色行为命令

C++ class Command { public: virtual ~Command() {} virtual void execute() = 0; virtual void undo() = 0; };

移动一个单位

C++ class MoveUnitCommand : public Command { public: MoveUnitCommand(Unit* unit, int x, int y) : unit_(unit), x_(x), y_(y) {} virtual void execute() { unit_->moveTo(x_, y_); } private: Unit* unit_; int x_, y_; };

添加“撤销(Undo)”的操作代码

C++ class MoveUnitCommand : public Command { public: MoveUnitCommand(Unit* unit, int x, int y) : unit_(unit), xBefore_(0), yBefore_(0), x_(x), y_(y) {} virtual void execute() { // 保存移动之前的位置 // 这样之后可以复原。 xBefore_ = unit_->x(); yBefore_ = unit_->y(); unit_->moveTo(x_, y_); } virtual void undo() { unit_->moveTo(xBefore_, yBefore_); } private: Unit* unit_; int xBefore_, yBefore_; int x_, y_; };

如果需要撤销多次操作,可以设置一个队列:

? 命令模式是一种固定接口函数,但可以自定义属性的“策略模式”。? 由于命令方法需要处理的数据结构和“命令子类型”绑定,因此如何构建“命令子类型”对象成为一个重要问题,这里也有使用各种“创建型设计模式”的空间。

状态模式 & 状态机

状态模式,是“状态机”的一种面向对象的实现方法

设计目的

例子

游戏角色的动画系统

防止空中连续跳跃,防止跳跃中卧倒,但可以跳跃中攻击

定义一个角色状态基类

C++ class HeroineState { public: virtual ~HeroineState() {} virtual void handleInput(Heroine& heroine, Input input) {} virtual void update(Heroine& heroine) {} };

定义一个“卧倒”状态

C++ class DuckingState : public HeroineState { public: DuckingState() : chargeTime_(0) {} virtual void handleInput(Heroine& heroine, Input input) { if (input == RELEASE_DOWN) { // 改回站立状态…… heroine.setGraphics(IMAGE_STAND); } } virtual void update(Heroine& heroine) { chargeTime_++; if (chargeTime_ > MAX_CHARGE) { heroine.superBomb(); } } private: int chargeTime_; };

角色(Context)使用状态对象处理行为

C++ class Heroine { public: virtual void handleInput(Input input) { state_->handleInput(*this, input); } virtual void update() { state_->update(*this); } // 其他方法…… private: HeroineState* state_; };

后续问题:状态应该如何切换

? 对于要求灵活性高的系统,把“切换状态”做到某个行为处理的逻辑中

C++ // 根据每次输入的行为结果判断是否切换状态 void Heroine::handleInput(Input input) { HeroineState* state = state_->handleInput(*this, input); if (state != NULL) { delete state_; state_ = state; } } // 每个具体的状态都可以决定如何切换状态 HeroineState* StandingState::handleInput(Heroine& heroine, Input input) { if (input == PRESS_DOWN) { // 其他代码…… return new DuckingState(); } // 保持这个状态 return NULL; }

? 对于比较固定的系统,如有限状态机,可以另外写一个自动判断条件,切换状态的模块

C++ // 每一帧都判断是否需要切换状态 void Heroine::update() { state_->update(*this); state_ = st_mc_->next(state_); } HeroineState* StateMechine::next(HeroineState* state) { // 根据有限状态机来统一的切换状态 ...... }

Socks5 代理握手以及传输

socks5协议是一个交互握手协议

TCP

代理服务器作为管道,需要处理握手过程,双向、读写堵塞的 4 种状态

流程状态机

定义一个代理管理管道的状态基类,核心需要处理的方法是:onRead()/onWrite(),就是收网络包和发网络包,这两个方法会被 epoll 事件驱动所触发。

C++ ///会话的状态类,不同类会用它来实现不同状态下的行为 class SessionStat { public: SessionStat(); virtual ~SessionStat(); virtual int onRead(Side side, Session *thisSess) = 0; virtual int onWrite(Side side, Session *thisSess) = 0; virtual void reset(); virtual int onEnter(Session *sess); virtual bool onChkIdle(Session *sess, const __time_t &chkTime); virtual int getStateID(); ///处理进入状态方法,设置会话(上下文) int enter(Session *sess); };

每种状态一个子类

举例:等待客户端发送鉴权信息(用户名、密码)

C++ ///接收用户名密码信息,写入UIN字段 int WaitingAuth::onRead(Side side, Session *thisSess) { if(side == server) return 0; Socks5Session* sess = static_cast<Socks5Session*>(thisSess); int sock = sess->getSock(side); //读取验证数据包 int iErrNo = 0; ProtoPkg *pkg = sess->authPkg; int decodeRs = pkg->decode(sock, iErrNo); if (decodeRs == -1) return 0; else if(decodeRs == -2) { WRITE_ERR_LOG("解析Auth请求包时发生I/O错误! fd:%d errno:%d", sock, iErrNo); return -1; } //从数据包中读取UIN,设置到会话中 char err[2] = {0x01, 0x01}; Field *f = pkg->getField("UNAME"); char c[16]; //预计最长QQ号 if(f->num > 15) { WRITE_ERR_LOG("出现非法的UIN长度为%d,不能超过15。", f->num); ssize_t rt = write(sock, err, 2); //0x01代表长度超过了15位 WRITE_DBG_LOG("Send return msg:%d", rt); return -1; } memcpy(c, f->data, f->num); c[f->num] = 0x00; char *endptr; errno = 0; sess->UIN = strtoull(c, &endptr,10); if((errno == ERANGE && sess->UIN == ULONG_MAX) || (endptr == c) || sess->UIN == 0){ WRITE_ERR_LOG("接收到错误格式的UIN: %s", c); WRITE_ERR_LOG("%d %d %d %d %d %d %d %d %d %d %d",c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7], c[8], c[9], c[10]); err[1] = 0x02; //0x02代表UIN不是纯数字的 ssize_t rt = write(sock,err, 2); WRITE_DBG_LOG("Send return msg:%d", rt); return -1; } //提取密码字段,作为SessionKey f = pkg->getField("PASSWD"); if(f != NULL && f->num > 0) { memcpy(sess->logData.body.sessKey, f->data, MIN(f->num, sizeof(sess->logData.body.sessKey) - 1)); sess->logData.body.sessKey[MIN(f->num, sizeof(sess->logData.body.sessKey) - 1)] = 0; } //提取GameID字段 if(sess->GetProxyVer() >= 7) { sess->SetGameId((uint16_t)strtoul(sess->logData.body.sessKey, NULL, 10)); } WRITE_DBG_LOG("Auth success: ProxyVer=%hu, Uin=%llu, GameId=%hu, SessKey=%s", (uint16_t)sess->GetProxyVer(), sess->UIN, sess->GetGameId(), sess->logData.body.sessKey); //发送回应包 char ok[2] = {0x01, 0x00}; int ws = write(sock, ok, 2); if (ws <= 0 && errno != EAGAIN && errno != EWOULDBLOCK) { WRITE_ERR_LOG("发送用户验证通过消息的时候发生I/O错误。FD:%d", sock); return -1; } //进入新状态 if(thisSess->setStat(WatingCmd::instance()) != 0) { return -1; } return 0; }

实践证明优点:?代码结构容易理解,无需完整阅读即可上手修改,经过 4 个人维护 ?扩展性好,已在 socks5 协议上增加很多特性 ?内存管理简单,会话数据和状态执行代码完全分开 ?很好的支持了 epoll 的边缘触发

缺点:?状态之间的代码跳转,没有确定的机制和约束 ?会话数据的结构比较复杂,所有功能都可能要求会话数据结构的修改,没有针对不同状态仔细设计不同的状态数据结构 ?UDP 协议没有细分状态,代码明显复杂很多

?状态模式要求能抽象出比较稳定的方法接口,这点很像“策略模式” ?状态对象本身的内存管理是一个难题,全行为(静态)的状态对象都依赖处理的“上下文”(Context),可能导致这个上下文非常复杂。需要进一步设计优化“上下文”对象。

观察者模式 & 事件驱动& MVC

?灵活,但代价高昂。看似解耦,但代码难以阅读,只能运行时跟踪。 ? 观察者模式是实现“事件驱动”的一种面向对象方法 ? MVC 架构常常使用观察者模式实现,但重点是模块职责的划分,而非实现方法

设计目的1.实时处理大量操作或者行为 2.一个操作触发多个不同的处理(和命令模式的主要差别)

设计要点1.针对每种具体的操作,设计一个“观察者”的子类 2.被观察的对象具备一个列表,负责发起对所有观察者对象的调用 3.发起观察者调用所传入的参数,根据观察者类型匹配,因此不必要反射

例子

Unity 的UGUI 驱动

EventTrigger.Entry作为观察者基类,通过不同的 delegate 来实现具体操作,而不是扩展子类

C# public class ScriptControl : MonoBehaviour { void Start() { // 获得被观察者管理对象(Subject) var trigger = transform.gameObject.GetComponent<EventTrigger>(); trigger.delegates = new List<EventTrigger.Entry>(); // 注意这里是列表 List // 构造观察者对象(Observer) EventTrigger.Entry entry = new EventTrigger.Entry(); entry.eventID = EventTriggerType.PointerClick; entry.callback = new EventTrigger.TriggerEvent(); UnityAction<BaseEventData> callback = new UnityAction<BaseEventData>(OnScriptControll); entry.callback.AddListener(callback); // 添加观察者对象到观察列表中 trigger.delegates.Add(entry); } // 这里传入的参数是 Event 基类,但一般会是子类 public void OnScriptControll(BaseEventData arg0) { Debug.Log("Test Click"); } }

和“命令模式”的比较

相似?都有“注册”过程 ?都会自动触发,如通过 Update() 驱动 ? 具体的处理都是一个对象

不同?命令模式下一个“事件”只有一个对象处理;观察者模式一个“事件”触发多个对象处理 ?命令模式自带处理参数的数据结构;观察者模式每个处理函数的参数必须显式传入(也可以传入基类由开发者自己转型)

MVC:?View\Controllor 互动往往使用开发者自己注册的观察者 ?Model\View 互动往往是“绑定”的刷新事件处理

命令模式和观察者模式的重要缺点:代码之间的关系是运行时关联的,不利于代码阅读,需要代码维护者在代码以外通过“反射”规则或者配置文件进行理解,不应该让“事件”的触发过于复杂。

本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-11-28,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 韩大 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 MySQL
腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com