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/

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏编程直播室

折腾git pages+hexo+NexT初识hexo开始本地试运行准备服务器准备上传工具先告一段落发表文章主题

2076
来自专栏Debian社区

为什么 Django 能持续统治 Python 开发世界

对于 Python 开发者来说,web 开发框架真可谓玲琅满目。然而 Django , 毋庸置疑的成为最受青睐的 web 框架。通过本篇博客,我来为大家讲解下为...

873
来自专栏编程坑太多

『高级篇』docker之了解kubernetes(31)

PS:(梳理概念)pod里面包括N个容器,service里面包括pod,Deployment可能包括service或者是pod。

1284
来自专栏FreeBuf

反取证技术:内核模式下的进程隐蔽

介绍 本文是介绍恶意软件的持久性及传播性技术这一系列的第一次迭代,这些技术中大部分是研究人员几年前发现并披露的,在此介绍的目的是建立这些技术和取证方面的知识框架...

3118
来自专栏老九学堂

进程与线程的区别?

进程是什么? 程序并不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是...

35911
来自专栏高爽的专栏

Eclipse安装插件的几种方式

       前段时间Google转向了IDEA,貌似有些动摇了Eclipse作为Java领域IDE龙头老大的位置,为此引起了Eclipse粉丝和IDEA粉丝的...

1790
来自专栏IT笔记

SVN自动化部署全流程之架构之美

公司一直没有一个完善的部署流程,基本都是通过上线打包以后SSH手动拖拽部署项目。 当然网上也有现成的持续集成工具,比如jenkins。Jenkins是一个开源软...

3667
来自专栏一名合格java开发的自我修养

kafka0.8--0.11各个版本特性预览介绍

kafka-0.8.2 新特性 producer不再区分同步(sync)和异步方式(async),所有的请求以异步方式发送,这样提升了客户端效率。produc...

712
来自专栏mySoul

Linux基础知识

软件运行时输入单元输入内容,进入内存,CPU由控制单元和算术逻辑单元组成,控制单元控制算术逻辑单元从内存中读取数据,内存和外部存储设备进行交互,运算完毕以后输出...

1824
来自专栏Java技术

记一次解决业务系统生产环境宕机问题!

Zabbix告警生产环境应用shutdown,通过堡垒机登入生产环境,查看应用容器进程,并发现没有该业务应用的相应进程,第一感觉进程在某些条件下被系统杀死了,然...

721

扫码关注云+社区