GOF设计模式有23个,很难一一都记住,因此很多时候在用的时候会想不到。所以全面掌握模式的分类和特性非常重要。 我按照中国象棋来总结了一下设计模式的分类,如下图
帅:唯一的棋子,符合单例的特征
兵:冲在最前面的棋子,而“构造对象”类的模式往往需要在程序开始时使用。
炮:威力巨大,但使用条件有点复杂,要“炮台”;命令和观察者模式常用作于核心驱动模式,是最常见的模式,但运行时状态比较复杂,较难维持代码的可读性。
马:灵活机动,但攻击范围较小。策略、状态模式常用于比较复杂的流程逻辑,非常灵活,缺点是如果流程不是那么复杂,则不需要用。
車:威力最大,但难以启动;职责链、模版方法都是整体型的模式,应用广泛。缺点是过于重型,需要写一堆“框架”型代码来使用。
士:作用关键,但范围很小。适配器和代理常用于局部功能,但是确实是非常关键的模式。
象:范围很大,但威力其实很小。外观和装饰器模式几乎可以用于任何系统,但真正的用处只在于简化接口,实际上对程序结构影响有限。
另外常见的还有:迭代器、享元等模式,因为都是比较容易记住或者很少用到,所以暂时没有列入。
明确术语
学习经典
理解内涵
希望能对“具体”的实现进行替换、升级、并存
不断积累各种“具体”的实现方案
把要完成的功能以“接口”定义
切换不同实现类的对象,实现不同的处理细节
///@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;
};
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;
}
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){
....
}
}
}
命令模式,是实现数据驱动的一种面向对象的方法
反射是实现命令模式的最常用手段
不同的数据,以不同的方式处理,
希望入口模块保持简洁,避免大段的 if/else 和 switch/case
不同的行为具有不同的数据格式,不希望耦合复杂的数据处理
把所有要处理的数据,都抽象为“命令”
每个“命令”对象,具备各自特有的数据格式,以及配套的数据处理方法,避免了大量的处理逻辑判断各自的数据格式正确性
和“策略模式”的关系:根据不同的数据结构,自动使用不同的“策略”
class Handler {
public:
virtual std::string GetName() = 0;
virtual int Process(const MsgObj &request, MsgObj *response,
Server *server) = 0;
};
由于没有反射,采用模板类进行静态绑定,收到数据之后,根据命令本身的类型参数,进行类型转换
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;
// 调用请求转码成为对象
if (is__same<QT, Q>()) {
req_ptr =
(QT *)(const_cast<StrMsgObjCast<Q> *>(request_obj)->object_ptr());
} else {
rt = UnpackRequest(request_obj->object(), &req_obj);
if (rt)
return ERR_OCC(REQUEST_UNPACK_ERR, "Request object unpack error!");
}
if (is__same<ST, S>()) {
res_ptr = (ST *)(response_obj->object_ptr());
}
// 发起处理逻辑
rt = Handle(*req_ptr, res_ptr, request.fd(), request_obj->session_id(),
server);
// 调用响应对象转码
if (!is__same<ST, S>()) {
int pack_rt = PackResponse(res_obj, response_obj->object_ptr());
if (pack_rt) ERROR_LOG("Response object pack error(%d)!", pack_rt);
}
if (rt) ERROR_LOG("Handle error! return : %d", rt);
return rt;
}
/**
* @brief 具体处理逻辑
*/
virtual int Handle(const QT &req_obj, ST *res_obj, int fd, int sess_id,
Server *server) = 0;
......
};
// 删除房间命令,命令数据为 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);
};
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;
}
如果语言具备反射功能,可以把命令数据直接反序列化为一个命令对象,命令对象根据自己身上的属性进行操作,而不是通过“处理方法”的参数获取对象属性
class Command
{
public:
virtual ~Command() {}
virtual void execute() = 0;
virtual void undo() = 0;
};
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_;
};
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_;
};
带状态的程序,根据状态有不同的处理逻辑
希望状态及其处理逻辑是可扩展的
所有要处理的行为抽象成固定的方法
把程序的“状态”抽象为类
具体的“状态”对象完成具体行为
防止空中连续跳跃,防止跳跃中卧倒,但可以跳跃中攻击
class HeroineState
{
public:
virtual ~HeroineState() {}
virtual void handleInput(Heroine& heroine, Input input) {}
virtual void update(Heroine& heroine) {}
};
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_;
};
class Heroine
{
public:
virtual void handleInput(Input input)
{
state_->handleInput(*this, input);
}
virtual void update()
{
state_->update(*this);
}
// 其他方法……
private:
HeroineState* state_;
};
// 根据每次输入的行为结果判断是否切换状态
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;
}
// 每一帧都判断是否需要切换状态
void Heroine::update() {
state_->update(*this);
state_ = st_mc_->next(state_);
}
HeroineState* StateMechine::next(HeroineState* state) {
// 根据有限状态机来统一的切换状态
......
}
代理服务器作为管道,需要处理握手过程,双向、读写堵塞的 4 种状态
定义一个代理管理管道的状态基类,核心需要处理的方法是:onRead()/onWrite(),就是收网络包和发网络包,这两个方法会被 epoll 事件驱动所触发。
///会话的状态类,不同类会用它来实现不同状态下的行为
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);
};
双向堵塞
代理管道连接完成
单向堵塞
双向畅通
UDP 管道完成
握手:等待鉴权
握手:等待鉴权结果
握手:等待路由命令
握手:等待第二层代理连接
握手:等待第二层路由命令选择
握手:等待命令选择
握手:等待连接方法
///接收用户名密码信息,写入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;
}
状态之间的代码跳转,没有确定的机制和约束
会话数据的结构比较复杂,所有功能都可能要求会话数据结构的修改,没有针对不同状态仔细设计不同的状态数据结构
UDP 协议没有细分状态,代码明显复杂很多
状态模式要求能抽象出比较稳定的方法接口,这点很像“策略模式”
状态对象本身的内存管理是一个难题,全行为(静态)的状态对象都依赖处理的“上下文”(Context),可能导致这个上下文非常复杂。需要进一步设计优化“上下文”对象。
实时处理大量操作或者行为
一个操作触发多个不同的处理(和命令模式的主要差别)
针对每种具体的操作,设计一个“观察者”的子类
被观察的对象具备一个列表,负责发起对所有观察者对象的调用
发起观察者调用所传入的参数,根据观察者类型匹配,因此不必要反射
EventTrigger.Entry 作为观察者基类,通过不同的 delegate 来实现具体操作,而不是扩展子类
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() 驱动
具体的处理都是一个对象
命令模式和观察者模式的重要缺点:代码之间的关系是运行时关联的,不利于代码阅读,需要代码维护者在代码以外通过“反射”规则或者配置文件进行理解,不应该让“事件”的触发过于复杂。
最后推荐一个电子书:https://refactoringguru.cn/design-patterns,有很多画的很棒的设计模式的图。
评论区
共 条评论热门最新