常见设计模式介绍

2021-12-05 13:41:15 浏览数 (1)

策略模式 & 接口

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

设计目的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:•ViewControllor 互动往往使用开发者自己注册的观察者 •ModelView 互动往往是“绑定”的刷新事件处理

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

0 人点赞