cqrs java demo_FFLIb Demo && CQRS

本文介绍了一个使用FFLIB构建的Java游戏后台架构Demo,该Demo基于CQRS思想,实现了进程间通信、解耦合的Event Publish/Subscribe机制,并通过Event进行单元测试。详细阐述了GatewayBroker的转发消息功能、LogicServer的逻辑处理以及BUS的事件发布和订阅机制。
摘要由CSDN通过智能技术生成

使用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。

ad0714567fe56ff2a467d58cd402cadd.png

GatewayBroker转发消息

Gatewaybroker扮演的角色为接受连接,转发消息。示例代码如下:

int gateway_service_t::handle_common_logic(gate_msg_tool_t&msg_, socket_ptr_t sock_)

{structlambda_t

{static void callback(common_msg_t::out_t& msg_, longuid_)

{//! send to client, add to gateway user map//! msg_sender_t::send_to_client(sock_, msg_);

}

};long uid = sock_->get_data()->uid;

common_msg_t::in_t dest_msg;

dest_msg.uid=uid;

dest_msg.content=msg_.packet_body;

singleton_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<: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,示例代码:

inttask_service_t::start()

{

subscriber_t subscriber;

subscriber.reg(this)

.reg(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:

classtype_i

{public:virtual ~type_i(){}virtual int get_type_id() const { return -1; }virtual const string& get_type_name() const {static string foo; returnfoo; }virtual void decode(const string&data_) {}virtual string encode() { return "";}

};

其中typeid和typename都不需要使用者自己定义,有一个类event_t会自动为其生成。示例代码如下:

class task_accepted_t: public event_t{public:

task_accepted_t(int task_id_ =0, int dest_value_ = 0):

task_id(task_id_),

dest_value(dest_value_)

{}inttask_id;intdest_value;

};

BUS有event被发布时,所有的订阅者都会被调用:

virtual int publish(const event_i&event_)

{returncall(event_.get_type_id(), event_);

}virtual int publish(const command_i&cmd_)

{returncall(cmd_.get_type_id(), cmd_);

}int call(int type_id_, const type_i&obj_)

{int num = 0;

pair<: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<

}return 0;

}returnnum;

}

Logicserver的细节

LogicServer的设计

LogicServer是后台程序中最复杂的部分,应尽量保证其可扩展性。在本demo中,遵循如下原则:

实体对象封装所有的业务逻辑,如Usertasks封装用户所有的任务相关操作

实体对象内部分成两部分,一部分为借口,如accept,用于验证用户操作是否有效,若无效抛出异常,若有效,创建evnet。另一部分专门处理event,当有event触发,修改对象内部数据,同时event也会被publish到BUS中,这样其他逻辑模块也可以进行其他处理。示例代码:

void user_tasks_t::accet_task(inttask_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是否按照预想的顺序触发来达到目的。

classtask_event_counter_t

{public:

task_event_counter_t():task_accepted_counter(0){}void handle(const task_accepted_t&e_)

{

task_accepted_counter++;

}inttask_accepted_counter;

};#define EVENT_COUNTER (singleton_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/

CQRS(Command Query Responsibility Segration)架构,大家应该不会陌生了。简单的说,就是一个系统,从架构上把它拆分为两部分:命令处理(写请求)+查询处理(读请求)。然后读写两边可以用不同的架构实现,以实现CQ两端(即Command Side,简称C端;Query Side,简称Q端)的分别优化。CQRS作为一个读写分离思想的架构,在数据存储方面,没有做过多的约束。所以,我觉得CQRS可以有不同层次的实现,比如: 1.CQ两端数据库共享,CQ两端只是在上层代码上分离;这种做法,带来的好处是可以让我们的代码读写分离,更好维护,且没有CQ两端的数据一致性问题,因为是共享一个数据库的。我个人认为,这种架构很实用,既兼顾了数据的强一致性,又能让代码好维护。 2.CQ两端数据库和上层代码都分离,然后Q的数据由C端同步过来,一般是通过Domain Event进行同步。同步方式有两种,同步或异步,如果需要CQ两端的强一致性,则需要用同步;如果能接受CQ两端数据的最终一致性,则可以使用异步。采用这种方式的架构,个人觉得,C端应该采用Event Sourcing(简称ES)模式才有意义,否则就是自己给自己找麻烦。因为这样做你会发现会出现冗余数据,同样的数据,在C端的db中有,而在Q端的db中也有。和上面第一种做法相比,我想不到什么好处。而采用ES,则所有C端的最新数据全部用Domain Event表达即可;而要查询显示用的数据,则从Q端的ReadDB(关系型数据库)查询即可。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值