C++工厂模式

本文代码demo开源:https://github.com/Ericsii/factory_pattern

最近设计RoboMaster机器人的通讯协议中,为了模块化协议开发模块打算使用工厂模式自动匹配处理不同数据帧的代码。

工厂模式

工厂模式个人理解,一个工厂对象(通常为单例)用于生成一系列不同的对象,这些生成的对象往往实现不同的业务。对应到我们的通讯协议的实现中,不同的帧解析对象去解析不同的数据帧。每新增一个自定义解析直接新增一个类而不需要对已有代码有侵入性修改;而且对于上层的使用者而言是感知不到不同的对象是如何被创建的。

具体实现

以下代码来自与即将开源的机器人算法框架

帧处理类设计

根据我们的需求,消息帧处理基类可以写成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// process_interface.hpp

class ProcessInterface
{

public:
using Packet = std::variant<rmoss_base::FixedPacket16, rmoss_base::FixedPacket32,
rmoss_base::FixedPacket64>;

public:
ProcessInterface(rclcpp::Node * node)
: node_(node)
{
}

virtual bool process_packet(const Packet & packet) = 0;

protected:
rclcpp::Node * node_;
};

构造函数部分 ProcessInterface(rclcpp::Node * node) : node_(node) 是为了能够获得 ROS 的节点句柄,用于创建该处理对象和 ROS 交互的工具。 process_packet 函数为处理具体一帧数据的方法,它接受一个数据帧 Packet 并在其中进行解析之后与ROS进行交互。例如将解析的结果发布为 topic。

例如从中派生的一个比赛结果数据帧处理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// game_status_processor.cpp

class GameResultProcessor : public ProcessInterface
{
public:
explicit GameResultProcessor(rclcpp::Node * node)
: ProcessInterface(node)
{
auto topic_name = node_->declare_parameter("game_result_topic", "game_result");
RCLCPP_INFO(node_->get_logger(), "game_result_topic: %s", topic_name.c_str());
pub_ = node_->create_publisher<rm_interfaces::msg::GameResult>(topic_name, 10);
}

bool process_packet(const Packet & packet)
{
if (std::holds_alternative<rmoss_base::FixedPacket64>(packet)) {
auto packet_recv = std::get<rmoss_base::FixedPacket64>(packet);
rm_interfaces::msg::GameResult::UniquePtr msg(new rm_interfaces::msg::GameResult());

ext_game_result_t data;
packet_recv.unload_data(data, 2);
msg->result = data.winner;

pub_->publish(std::move(msg));
return true;
} else {
RCLCPP_WARN(node_->get_logger(), "Invalid length of data frame for GameResult processor.");
return false;
}
}

private:
typedef struct
{
uint8_t winner;
} ext_game_result_t;
rclcpp::Publisher<rm_interfaces::msg::GameResult>::SharedPtr pub_;
};

GameResultProcessor 对象构造中使用ROS节点句柄创建与ROS交互的publisher工具。GameResultProcessor::process_packet 则实现了一个解析串口64字节数据发送到ROS topic的过程。

工厂类设计

定义工厂类的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// process_factory.hpp

#include <process_interface.hpp>

class ProcessFactory
{
public:
using CreateFunction = std::function<ProcessInterface::SharedPtr(rclcpp::Node * node)>;

template<typename T>
static bool register_class(const std::string & key)
{
if (auto it = get_creator_map().find(key); it == get_creator_map().end()) {
get_creator_map()[key] = [](rclcpp::Node * node) {return std::make_shared<T>(node);};
return true;
}
return false;
}

static ProcessInterface::SharedPtr create(const std::string & key, rclcpp::Node * node);

private:
static std::unordered_map<std::string, CreateFunction> & get_creator_map();
};

ProcessFactory::register_class: 模板函数用于将新的类注册进入工厂类的构造函数表中

1
2
3
4
5
if (auto it = get_creator_map().find(key); it == get_creator_map().end()) {
get_creator_map()[key] = [](rclcpp::Node * node) {return std::make_shared<T>(node);};
return true;
}
return false;

这行代码中判断当前注册的类名称是否已经包含在creator_map这个表中,如果不存在则创建一个新的lambda函数来调用这个新类的构造函数。[](rclcpp::Node * node) {return std::make_shared<T>(node);} 这个lambda函数接收一个node指针句柄作为参数,调用 T 的构造函数创建一个新对象。

ProcessFactory::create 函数接受一个key和node句柄来自动创建key对应的帧处理对象。

ProcessFactory::get_creator_map 函数用来获取一个map变量存储注册在工厂的各个对象构造函数

工厂实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// process_factory.cpp

#include <process_factory.hpp>

ProcessInterface::SharedPtr ProcessFactory::create(const std::string & key, rclcpp::Node * node)
{
auto creator = get_creator_map().find(key);
if (creator != get_creator_map().end()) {
return get_processor_map()[key] = creator->second(node);
}
// 防止未注册的类创建时出现空悬指针
return nullptr;
}

std::unordered_map<std::string, ProcessFactory::CreateFunction> & ProcessFactory::get_creator_map()
{
// 此处的 static 是 C++11 的局部静态对象机制,能够实现一个线程安全的单例 creator_map
static std::unordered_map<std::string, CreateFunction> creator_map;
return creator_map;
}

向工厂注册新的类型

以上我们创建了一个工厂类,但是还是有疑问如何将 GameResultProcessor 类注册进这个工厂,让工厂创建对象时能够调用它的构造函数。答案已经在ProcessFactory::register_class这个模板函数中了,只需要调用此模板函数,就能在工厂中注册我们的新对象:

1
2
3
4
5
6
7
#include <process_factory.hpp>

int main()
{
bool registered = ProcessFactory::register_class<MyProcessor>("MyProcessorA");
return 0;
}

如果这样实现工厂模式,对于上层的使用者而言依旧需要在初始化整个工厂之前依次注册对象,这与我们在最开始说的使用者应该感受不到工厂创建对象的过程这个思想有所违背。需要一种更为“优雅”的方式来实现类型的注册。

我们可以利用一个C/C++中的机制——全局变量会在主函数执行之前进行初始化,来实现类型的自动注册。具体可以写成:

1
static bool game_result_processorregistered = ProcessFactory::register_class<GameResultProcessor>("GameResultProcessor");

将每一个派生类的实现末尾都加入以上这样一行代码,例如GameResultProcessor中。

1
2
3
4
5
6
7
8
// game_status_processor.cpp

class GameResultProcessor : public ProcessInterface
{
... // 实现
};

static bool game_result_processorregistered = ProcessFactory::register_class<GameResultProcessor>("GameResultProcessor");

这样就在用户感知不到GameResultProcessor的情况下向工厂注册了。

更进一步可以将这段注册代码写成C/C++的宏,这样更便于注册新类:

1
2
3
4
5
6
7
// register_macro.hpp

#include <rm_base/buffer_processor_factory.hpp>

#define REGISTER_PROCESSOR_CLASS(class_name, key) \
static bool class_name ## _registered = \
rm_base::ProcessFactory::register_class<class_name>(key);

可能会出现的问题

  • 纯头文件实现的工厂模式,在动态链接库中使用可能会出现在链接的目标程序中工厂找不到库中实现的类的问题
    • 解决方式:将工厂的实现写入库的cpp源文件中,避免在链接目标程序中对工厂类的get_creator_map方法重复编译。