gpt4 book ai didi

c++ - 如何使深度中的对象易于配置 "from the top"?

转载 作者:塔克拉玛干 更新时间:2023-11-03 00:46:43 25 4
gpt4 key购买 nike

考虑以下类之间的关系:

int main(int, char**) { | class Window {      | class Layout { | class Box {
/* Use argc/argv */ | Layout layout; | Box box; | int height,
Window window; | | | max_width;
} | bool print_fps; | public: |
| | Layout(); | public:
| public: | }; | Box (int,int);
| Window (); | | };
| }; | |

为了简单起见,我编写了这个结构,实际上还有更多的类。
main()我获取了一些应用程序参数(通过配置文件、数据库、CLI 参数)。现在我想将这些值传递给所需的对象。

我的问题: 其中是在类之间“打破壁垒”的最佳/最优雅的方式,以便我可以“抛出”配置,而谁需要它来“捕获”它?

初始 我“打开了一些门”并给了 Window构造函数 Window 所需的一切, LayoutBox .然后, WindowLayout Layout 所需的一切和 Box .等等。

我很快意识到这与 非常相似。依赖注入(inject) 关于它,但事实证明,它并不直接适用于我的情况。
在这里,我使用像 bool 这样的原语。和 int事实上,如果我接受它们作为构造函数参数,我会得到上面描述的结果 - 一个很长的类似调用链: Window(box_height, box_max_width, window_print_fps) .

如果我想更改 Box::height 的类型怎么办至 long ?我需要遍历链中每个类的每一对头/源来更改它。

如果我希望我的类被隔离(我确实这样做了),那么 Window 不应该担心 Box 并且 main 不应该担心 Layout。

然后, 我的第二个想法 提出:创建一些类似 JSON 的结构,充当配置对象。每个人都会得到一个(共享的)指向它的指针,只要他们想要,他们就会说 this->config["box"]["height"] - 大家都很开心。

这有点工作,但这里有两个问题:没有类型安全
以及类( Config )和整个代码库之间的紧密耦合。

基本上,我看到 解决问题的两种方法 :
  • “向下”:出 -> 入
    顶部的对象( 外部的 )关心深层的对象( 内部的 )。他们明确地推倒了内部人员想要的东西。
  • “向上”:输入 <- 输出 (“图”是一样的,但请稍等)
    底部的对象( 内部 )自己关心自己。他们通过接触顶部的某个容器( )来满足他们的需求并拉出他们想要的东西。

  • 它要么向上要么向下 - 我试图开箱即用(实际上,它是一条线 - 只是 ↑ 或 ↓)但我只在这里结束。

    另一个问题 我之前的两个想法是关于解析配置的方式:
  • 如果 main.cpp(或配置解析器)需要给出 int heightBox ,那么它需要知道 box 才能正确解析值,对吗? (紧耦合)
  • 另一方面,如果 main.cpp 不知道 Box(理想情况下),它应该如何以对 box 友好的方式存储值?
  • 构造函数中不应需要可选参数 => 不应破坏应用程序。也就是说, main 应该接受某些参数的缺失,但它也需要知道在使用所需的参数构造所需的对象后,必须为所需的对象调用 setter。


  • 整个想法就是要努力做到这三个原则:
  • 类型安全。由解决方案 1 提供。但不是 2。
  • 松耦合。既不是由 1.(主要关心 Box)也不是由 2.(每个人都需要 Config)提供
  • 避免重复。由 2 但不是 1 提供。(转发许多相同的参数,直到它们达到目标)

  • 我实现了一个次优解决方案,我将作为自我回答发布,目前效果很好,总比没有好,但我期待更好的解决方案!

    最佳答案

    我非常赞成第二种方法,并编写了一个实现的快速概述。请记住,我并不是说这是最好的解决方案,但是,在我看来这是一个可靠的解决方案。有兴趣听取您的意见和批评。

    首先,有一些代表可配置值的小对象。

    class ConfigurationParameterBase
    {
    public:
    ConfigurationParameterBase(ConfigurationService* service,
    std::string name)
    : service_(service), name_(std::move(name)) {
    assert(service_);
    service_->registerParameter(this);
    }
    protected:
    ~ConfigurationParameterBase() {
    service_->unregisterParameter(this);
    }

    public:
    std::string name() const { return name_; }
    virtual bool trySet(const std::string& s) = 0;

    private:
    ConfigurationService* service_;
    std::string name_;
    };

    template<typename T>
    class ConfigurationParameter : public ConfigurationParameterBase
    {
    public:
    ConfigurationParameter(ConfigurationService* service,
    std::string name, std::function<void(T)> updateCallback = {})
    : ConfigurationParameterBase(service, std::move(name))
    , value_(boost::none)
    , updateCallback_(std::move(updateCallback))
    { }

    bool isSet() const { return !!value_; }
    T get() const { return *value_; }
    T get(const T& _default) const { return isSet() ? get() : _default; }

    bool trySet(const std::string& s) override {
    if(!fromString<T>(s, value_))
    return false;
    if(updateCallback_)
    updateCallback_(*value_);
    return true;
    }

    private:
    boost::optional<T> value_;
    std::function<void(T)> updateCallback_;
    };

    每个对象存储任何特定类型的值(使用单个类模板很容易处理),它表示由其名称指定的配置参数。此外,它拥有一个可选的回调,应在修改相应配置时调用。所有实例共享一个公共(public)基类,该基类在中心 ConfigurationService 注册和注销具有给定名称的类。目的。除此之外,它强制派生类实现 trySet ,它将配置值(字符串)读入参数预期的类型。如果成功,则存储该值并调用回调(如果有)。

    接下来,我们有 ConfigurationService .它负责跟踪当前配置和所有观察者。可以使用 setConfigurationParameter 设置各个选项.在这里,我们可以添加函数来从文件、数据库、网络或其他任何地方读取整个配置。
    class ConfigurationService
    {
    public:
    void registerParameter(ConfigurationParameterBase* param) {
    // keep track of this observer
    params_.insert(param);
    // set current configuration value (if one exists)
    auto v = values_.find(param->name());
    if(v != values_.end())
    param->trySet(v->second);
    }

    void unregisterParameter(ConfigurationParameterBase* param) {
    params_.erase(param);
    }

    void setConfigurationParameter(const std::string& name,
    const std::string& value) {
    // store setting
    values_[name] = value;
    // update all 'observers'
    for(auto& p : params_) {
    if(p->name() == name) {
    if(!p->trySet(value))
    reportInvalidParameter(name, value);
    }
    }
    }

    void readConfigurationFromFile(const std::string& filename) {
    // read your file ...
    // and for each entry (n,v) do
    // setConfigurationParameter(n, v);
    }

    protected:
    void reportInvalidParameter(const std::string& name,
    const std::string& value) {
    // report whatever ...
    }

    private:
    std::set<ConfigurationParameterBase*> params_;
    std::map<std::string, std::string> values_;
    };

    然后我们可以最终定义我们的应用程序的类。每个可配置的类成员(类型为 T )都被替换为成员 ConfigurationParameter<T>并在构造函数中使用相应的配置名称进行初始化,以及 - 可选 - 更新回调。然后类可以像使用普通类成员一样使用这些值(例如 fillRect(backgroundColor_.get()) ),并且只要值发生变化就会调用回调。注意这些回调是如何直接映射到类的标准 setter 方法的。
    class Button
    {
    public:
    Button(ConfigurationService* service)
    : fontSize_(service, "app.fontSize",
    [this](int v) { setFontSize(v); })
    , buttonText_(service, "app.button.text") {
    // ...
    }

    void setFontSize(int size) { /* ... */ }

    private:
    ConfigurationParameter<int> fontSize_;
    ConfigurationParameter<std::string> buttonText_;
    };

    class Window
    {
    public:
    Window(ConfigurationService* service)
    : backgroundColor_(service, "app.mainWindow.bgColor",
    [this](Color c){ setBackgroundColor(c); })
    , fontSize_(service, "app.fontSize") {
    // ...
    button_ = std::make_unique<Button>(service);
    }

    void setBackgroundColor(Color color) { /* ... */ }

    private:
    ConfigurationParameter<Color> backgroundColor_;
    ConfigurationParameter<int> fontSize_;
    std::unique_ptr<Button> button_;
    };

    最后,我们将所有内容粘在一起(例如在 main 中)。创建 ConfigurationService 的实例然后创建可以访问它的所有内容。对于上述实现,重要的是 service比所有观察者的生命周期都长,但是这可以很容易地改变。
    int main()
    {
    ConfigurationService service;
    auto win = std::make_unique<Window>(&service);

    service.readConfigurationFromFile("config.ini");

    // go into main loop
    // change configuration(s) whenever you need
    service.setConfigurationParameter("app.fontSize", "12");
    }

    由于观察者和更新回调,可以随时更改整个配置(或仅单个条目)。

    随意使用上面的代码 here

    让我快速总结一下:
  • 没有相互交织的类(class)。 Window不需要了解Button的配置。 .
  • 确实,所有类都需要访问 ConfigurationService ,但是可以通过接口(interface)轻松实现这一点,从而使实际实现可互换。
  • 实现工作是平庸的。您唯一需要扩展以支持更多配置类型的是 fromString模板,但如果您想从文本文件中解析配置,无论如何都需要这样做。
  • 配置可以是每个类(如示例中所示)或每个对象(只需将配置键或前缀传递给类构造函数)。
  • 不同的类/对象可以挂接到同一个配置条目上。
  • 可以从任意来源提供配置。只需将另一个函数(例如 loadConfigurationFromDatabase )添加到 ConfigurationService ,这样做。
  • 可以检测到未知的配置条目和/或意外类型并将其报告给用户、日志文件或其他地方。
  • 如有必要,可以通过编程方式更改配置。为ConfigurationService添加相应的方法并且可以在程序退出时将以编程方式更改的配置写回文件或数据库。
  • 可以在执行期间(不仅在启动时)更改配置。
  • 可以舒适地使用可配置的值(ConfigurationParameter 成员)。这可以通过提供相应的强制转换运算符 ( operator T() const ) 来进一步改进。
  • ConfigurationService实例必须传递给所有需要注册的类。这可以使用全局或静态实例(例如作为单例)来规避,尽管我不确定这是否更好。
  • 在性能和内存消耗方面有很多改进是可能的(例如,每个参数只转换一次)。将上面的内容视为一个草图。
  • 关于c++ - 如何使深度中的对象易于配置 "from the top"?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45998746/

    25 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com