There may be better ways, but we (at my company) tend to gravitate towards passing around the rclcpp::Node object and parameter namespaces. Then anything that needs to have some configuration can use the parameter namespace and node object to just declare it's own parameters and register callbacks for those parameter changes.
In cases where these classes are specifically ros-agnostic, we'll pass in a shared_ptr configuration struct into the non-ros classes. Then from the ros node, we can still declare parameters, and callbacks that change values in that shared configuration struct.
Added Examples:
Configuration Struct:
struct ClassNConfiguration
{
int64_t field_a;
std::string field_b;
}
ClassN:
class ClassN
{
public:
ClassN(std::shared_ptr<ClassNConfiguration> configuration) :
configuration_(configuration)
{
...
}
protected:
std::shared_ptr<ClassNConfiguration> configuration_;
}
Node Class:
class MyNode : rclcpp::Node
{
public:
MyNode() :
rclcpp::Node("my_node"),
class_n_config_(std::make_shared<ClassNConfiguration>())
{
class_n_config_.field_a = declare_parameter("class_n.field_a", 42);
class_n_config_.field_b = declare_parameter("class_n.field_b", "hello ros2");
class_n_ = std::make_shared<ClassN>(class_n_config_);
}
protected:
std::shared_ptr<ClassNConfiguration> class_n_config_;
std::shared_ptr<ClassN> class_n_;
}