FFLIb Demo && CQRS
使用FFLIB 构建了一个demo,该demo模拟了一个常见的游戏后台架构,该demo主要有一下亮点:
- FFLIB 实现进程间通信非常方便
- 基于CQRS 思想构建LogicServer
- 使用Event Publish/Subscribe, 实现各个模块的解耦合
- 基于Event 实现实体对象的单元测试,在你gtest中,利用event做mock,同时利用event 做验证,单元测试就是一个Given(event,先提供条件), When(Command,触发操作), Expect(Event,期望结果是否发生)。
模拟后台进程的通信
由于本demo 只在于演示fflib,demo中的细节没有做过多处理,主要通讯流程就是client –》 gatewayBroker –》 LogicServer。
GatewayBroker 转发消息
Gatewaybroker 扮演的角色为接受连接,转发消息。示例代码如下:
int gateway_service_t::handle_common_logic(gate_msg_tool_t& msg_, socket_ptr_t sock_)
{
struct lambda_t
{
static void callback(common_msg_t::out_t& msg_, long uid_)
{
//! send to client, add to gateway user map
//! msg_sender_t::send_to_client(sock_, msg_);
}
};
long uid = sock_->get_data<client_session_t>()->uid;
common_msg_t::in_t dest_msg;
dest_msg.uid = uid;
dest_msg.content = msg_.packet_body;
singleton_t<msg_bus_t>::instance().get_service_group("logic")
->get_service(0)
->async_call(dest_msg, binder_t::callback(&lambda_t::callback, uid));
return 0;
}
LogicServer 各个逻辑模块处理请求
LogicServer 接收到消息后,将消息交由特定的逻辑模块处理,所有的逻辑模块接口都专门处理一种cmd,并且这些接口都已经注册到BUS中了。故LogicServer 将消息publish到BUS中即可:
int logic_service_t::common_msg(common_msg_t::in_t& msg_, rpc_callcack_t<common_msg_t::out_t>& cb_)
{
common_msg_t::out_t ret;
cb_(ret);
uint32_t* len = (uint32_t*)(msg_.content.c_str());
string name(msg_.content.c_str()+4, *len);
BUS.publish(name, msg_.content);
return 0;
}
BUS 的细节
Service 中定义的接口,需要注册到BUS中,订阅相关的CMD,示例代码:
int task_service_t::start()
{
subscriber_t subscriber;
subscriber.reg<accept_task_cmd_t>(this)
.reg<complete_task_cmd_t>(this);
BUS.subscribe(subscriber);
return 0;
}
void task_service_t::handle(const accept_task_cmd_t& cmd_)
{
USER_MGR.get_user(cmd_.uid).get_tasks().accet_task(cmd_.tid);
}
void task_service_t::handle(const complete_task_cmd_t& cmd_)
{
USER_MGR.get_user(cmd_.uid).get_tasks().complete_task(cmd_.tid);
}
将特定的消息投递给特定接口只是BUS的功能之一,它也负责发布event, event和cmd的区别是cmd是用户的操作,它会触发特定的实体逻辑,逻辑检查ok,将会创建某个或某些event,这些event会触发某些实体对象的数据改变。所有cmd和event都继承于type_i:
class type_i
{
public:
virtual ~ type_i(){}
virtual int get_type_id() const { return -1; }
virtual const string& get_type_name() const {static string foo; return foo; }
virtual void decode(const string& data_) {}
virtual string encode() { return "";}
};
其中typeid和typename都不需要使用者自己定义,有一个类event_t 会自动为其生成。示例代码如下:
class task_accepted_t: public event_t<task_accepted_t>
{
public:
task_accepted_t(int task_id_ =0, int dest_value_ = 0):
task_id(task_id_),
dest_value(dest_value_)
{}
int task_id;
int dest_value;
};
BUS 有event被发布时,所有的订阅者都会被调用:
virtual int publish(const event_i& event_)
{
return call(event_.get_type_id(), event_);
}
virtual int publish(const command_i& cmd_)
{
return call(cmd_.get_type_id(), cmd_);
}
int call(int type_id_, const type_i& obj_)
{
int num = 0;
pair<subscriber_t::callback_multimap_t::iterator, subscriber_t::callback_multimap_t::iterator> ret;
ret = m_callbacks.equal_range(type_id_);
for (subscriber_t::callback_multimap_t::iterator it = ret.first; it != ret.second; ++it)
{
try
{
++num;
it->second->callback(&obj_);
}
catch(exception& e)
{
cout <<"bus exception:" << e.what() <<"n";
continue;
}
return 0;
}
return num;
}
Logicserver 的细节
LogicServer的设计
LogicServer 是后台程序中最复杂的部分,应尽量保证其可扩展性。在本demo中,遵循如下原则:
- 实体对象封装所有的业务逻辑,如Usertasks 封装用户所有的任务相关操作
- 实体对象内部分成两部分,一部分为借口,如accept,用于验证用户操作是否有效,若无效抛出异常,若有效,创建evnet。另一部分专门处理event,当有event触发,修改对象内部数据,同时event也会被publish到BUS 中,这样其他逻辑模块也可以进行其他处理。示例代码:
void user_tasks_t::accet_task(int task_id_)
{
if (m_tasks.find(task_id_) != m_tasks.end()) throw task_exception_t("tid exist");
apply_change(task_accepted_t(task_id_, 100));
}
void user_tasks_t::apply(const task_accepted_t& event_)
{
task_ino_t task_info(event_.task_id, event_.dest_value, TASK_ACCEPTED);
m_tasks.insert(make_pair(event_.task_id, task_info));
}
void apply_change(const T& event_, bool new_change_ = true)
{
apply(event_);
if (new_change_)
{
BUS.publish(event_);
}
}
- Service 负责处理cmd,根据不同的cmd,调用实体对象的接口
- 使用Event做单元测试
单元测试流程
Given:
在测试实体对象特定的接口时,需要mock操作,由于实体对象的所有修改都是由Event 触发的,mock操作只是按照顺序提供给实体对象event即可:
//! 先 mock出数据 只需给对象提供相应的event即可
task_accepted_t e;
e.task_id = 100;
e.dest_value = 200;
user_task.apply_change(e, false);
When
Event given完毕后,触发实体的接口,并且测试接口是否按照预定的逻辑操作,如验证失败是否抛出异常。
//! test interface
EXPECT_THROW(user_task.accet_task(100), user_tasks_t::task_exception_t);
Expect:
当调用实体对象时,若逻辑争取,会触发一些event产生,由于实体对象的数据不能被直接验证是否修改争取,但是可以通过验证event是否按照预想的顺序触发来达到目的。
class task_event_counter_t
{
public:
task_event_counter_t():task_accepted_counter(0){}
void handle(const task_accepted_t& e_)
{
task_accepted_counter ++;
}
int task_accepted_counter;
};
#define EVENT_COUNTER (singleton_t<task_event_counter_t>::instance())
//! task_accepted_t will be trigger
user_task.accet_task(200);
EXPECT_TRUE(EVENT_COUNTER.task_accepted_counter == 1);
如上代码所示, .accet_task()成功会触发, task_accepted_t 事件,通过验证此事件是否被触发,即可验证实体对象是否操作正常。
备注
示例代码地址:http://ffown.googlecode.com/svn/trunk/example/game_framework/
- ASP.NET MVC以ModelValidator为核心的Model验证体系: ModelValidatorProviders
- 我所理解的RESTful Web API [设计篇]
- 黑箱难题阻碍了深度学习的普及与发展
- iOS 转场动画探究(一)
- XCode中如何使用事务
- 如何部署编译NDIS驱动的环境(内部资料)
- 深度学习的入门级装机配置推荐
- Self Host模式下的ASP. NET Web API是如何进行请求的监听与处理的?
- GridView绑定小技
- XCode读取Excel数据(适用于任何数据库)
- ObjectDataSource选择业务对象列表为空的探讨
- ASP.NET Web API自身对CORS的支持: CORS授权检验的实施
- 模版引擎XTemplate与代码生成器XCoder(源码)
- 深度学习让人脸识别准确率不断提升
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 基础知识 | R语言绘图保存的pdf图片无法显示中文怎么办?
- 基础知识 | R语言绘图基础之柱形图
- 高维数据 | R语言绘图基础之主成分分析
- 高维数据 |R语言数据可视化之t-SNE
- 基础知识 | R语言数据分析之控制流
- 基础知识 | R语言数据处理之日期值的转换
- 云开发 CloudBase CMS 内容管理系统正式开源啦!
- 高维数据 | R语言数据可视化之热力图
- 高维数据 | R语言数据可视化之日历图
- 打卡群刷题总结0804——二叉树的中序遍历
- 基础知识 | R语言数据管理之SQL语句
- 文本挖掘| 某作者文章的词频统计排序
- spring注解配置之@Configuration
- TS核心知识点总结及项目实战案例分析
- 用Python开发 写个消消乐小游戏