Как сделать объекты в глубине легко настраиваемыми «сверху»?

Рассмотрим следующее соотношение между классами:

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). Теперь я хочу доставить эти значения нужным объектам.

Мой вопрос: Который является лучший / самый элегантный способ «сломать стену» между классами, чтобы я мог «Бросить» конфигурация и кому это нужно «Захватить» Это?


Initialy Я «открыл некоторые двери» и дал Window конструктор все, что было нужно Window, Layout а также Box, Затем, Window дал Layout все необходимое Layout а также Box, И так далее.

Я быстро понял, что это очень похоже на то, что Внедрение зависимости это о, но, как выясняется, это не относится непосредственно к моему делу.
Здесь я работаю с примитивы лайк bool а также int и на самом деле, если я принимаю их в качестве параметров конструктора, я получаю результат, описанный чуть выше — очень длинную цепочку похожих вызовов: Window(box_height, box_max_width, window_print_fps),

Что делать, если я хотел бы изменить тип Box::height в long? Мне нужно было бы гулять через каждую пару заголовок / источник из каждый класс в цепи, чтобы изменить его.

Если я хочу, чтобы мои занятия были изолированы (и я делаю) затем Окно не стоит беспокоиться о коробка а также главный не стоит беспокоиться о раскладка.


Затем, моя вторая идея придумал: создать JSON-подобную структуру, которая действует как объект конфигурации. Каждый получает (общий) указатель на него, и когда они хотят, они говорят this->config["box"]["height"] — все счастливы.

Это бы сработало, но здесь есть две проблемы: нет тип безопасности
а также тесная связь между классом (Config) и все кодовая база.


В основном я вижу два пути решения проблемы:

  1. «Вниз»: Out -> In
    Объекты на вершине (внешний) заботиться об объекте в глубине (внутренний). Oни толкать вниз явно то, что хотят внутренности.
  2. «Снизу вверх»: В <- Из («диаграмма» такая же, но, пожалуйста, подождите)
    Объекты на дне (внутренний) заботиться о себе самостоятельно. Они удовлетворяют свои потребности, обращаясь к некоторому контейнеру наверху (внешний) а также вытащить что они хотят.

Это либо вверх или же вниз — Я пытаюсь мыслить нестандартно (На самом деле это строка — просто ↑ или ↓) но я оказался только здесь.


Еще одна проблема Исходя из двух моих предыдущих идей, речь идет о том, как анализируется конфигурация:

  1. Если main.cpp (или парсер конфигурации) нужно дать int height в Box, тогда он должен знать о коробке, чтобы правильно проанализировать значение, верно? (тесная связь)
  2. Если, с другой стороны, main.cpp не знает о Box (в идеале), как он должен хранить значение в дружественном для box способе?

  3. Необязательные параметры не нужны в конструкторах => не должны нарушать работу приложения. То есть main должен принять отсутствие какого-либо параметра, но он также должен знать, что для требуемого объекта должен быть вызван установщик после того, как он был создан с требуемыми параметрами.


Вся идея это стремиться к этим трем принципам:

  1. Тип безопасности. Предусмотрено решением 1., но не 2.
  2. Слабая связь. Не предоставляется ни 1. (главное заботится о Box), ни 2. (всем нужен Config)
  3. Избегайте дублирования. Предоставляется 2, но не 1. (много идентичных параметров пересылаются, пока они не достигнут своей цели)

Я реализовал неоптимальное решение, которое я опубликую в качестве самостоятельного ответа, который пока работает хорошо, и это лучше, чем ничего, но я жду чего-то лучшего!

5

Решение

Я очень поддерживаю второй подход и собрал краткий план реализации. Пожалуйста, имейте в виду, что я не утверждаю, что это лучшее решение, однако, на мой взгляд, оно является надежным. Интересно услышать ваши комментарии и критику.

Во-первых, есть несколько крошечных объектов, представляющих настраиваемое значение.

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_;
};

Каждый объект хранит значение любого определенного типа (легко обрабатываемое с использованием одного шаблона класса), которое представляет параметр конфигурации, указанный его именем. Кроме того, он содержит необязательный обратный вызов, который должен вызываться при изменении соответствующей конфигурации. Все экземпляры имеют общий базовый класс, который регистрирует и отменяет регистрацию класса с заданным именем в центральном 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())) и обратные вызовы вызываются при изменении значений. Обратите внимание, как эти обратные вызовы напрямую отображаются на стандартные методы установки класса.

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");
}

Из-за наблюдателей и обновленных обратных вызовов вся конфигурация (или просто отдельные записи) может быть изменена в любое время.

Не стесняйтесь поиграть с вышеуказанным кодом Вот


Позвольте мне быстро подвести итог:

  • Здесь нет переплетения классов. Window не нужно ничего знать о конфигурации Button,
  • Это правда, что все классы должны иметь доступ к ConfigurationService, но это легко сделать с помощью интерфейса, что делает фактическую реализацию взаимозаменяемой.
  • Усилия по реализации посредственны. Единственное, что вам нужно расширить для поддержки большего количества типов конфигурации, это fromString шаблон, но это необходимо в любом случае, если вы хотите анализировать конфигурации из текстовых файлов.
  • Конфигурации могут быть для каждого класса (как в примере) или для объекта (просто передать ключ конфигурации или просто префикс в конструктор класса).
  • Различные классы / объекты могут подключаться к одной и той же записи конфигурации.
  • Конфигурации могут быть предоставлены из произвольных источников. Просто добавьте еще одну функцию (например, loadConfigurationFromDatabase) к ConfigurationService, чтобы сделать это.
  • Неизвестные записи конфигурации и / или неожиданные типы могут быть обнаружены и сообщены пользователю, в файле журнала или где-то еще.
  • Конфигурация может быть изменена программно, если это необходимо. Добавьте соответствующий метод к ConfigurationService и программно измененная конфигурация может быть записана обратно в файл или базу данных при выходе из программы.
  • Конфигурации могут быть изменены во время выполнения (не только при запуске).
  • Настраиваемые значения (ConfigurationParameter члены) могут быть использованы с комфортом. Это может быть улучшено дополнительно путем предоставления соответствующего оператора приведения (operator T() const).
  • ConfigurationService Экземпляр должен быть передан всем классам, которые должны что-то зарегистрировать. Это можно обойти, используя глобальный или статический экземпляр (например, как Singleton), хотя я не уверен, что это лучше.
  • Возможны многие улучшения в отношении производительности и потребления памяти (например, преобразование каждого параметра только один раз). Смотрите выше, как простой набросок.
1

Другие решения

Я решил что Я предпочитаю первый подход более того, из-за безопасности типов, мне также не очень понравилась идея каждый каждый объект, содержащий указатель на некоторую конфигурацию.

Я также понял, что мое восприятие DI было не совсем правильным, или, по крайней мере, что есть лучший способ реализовать его в моем случае.
Итак, вместо принятия все параметры все объекты вниз по дереву, конструктор только получает прямой & необходимые зависимости. Это полностью решает проблему дублирования 1.

Это оставляет нас только с проблемой сильной связи — главное знать практически обо всех. Чтобы знать, как создать Окно, нужно создать Макет, Бокс и всех остальных.

Во-первых, я хотел взять эту проблему где-то еще, поэтому я создал класс для создания объектов для меня. Как фабрика, но используется только для того, чтобы скрывать грязные вещи от основного.
В ConfigFactory теперь я каким-то образом сохраняю все параметры и передаю их, когда мне нужно построить объект. Не имеет значения, как хранится конфигурация, я решил иметь простой struct Data и использовал PIMPL, чтобы не перекомпилировать какие-либо ConfigFactory.h-зависимый файл необходим при изменении логики параметров (это должно происходить довольно часто)

Далее я увидел, как этот парень использовал шаблоны, чтобы сделать Фабрику более общей: https://stackoverflow.com/a/26950454, но вместо работы с базовыми и производными классами, указателями и динамическим размещением, Я определил функцию-член, которая возвращает объект, выделенный из стека (в идеале):

template <typename T> T produce() const { return {}; }

Если нет специализации produce для желаемого типа, T создается по умолчанию — таким образом, он более гибок при добавлении / удалении специализаций.

я держу ConfigFactory.h только этот маленький и свободный от любого #includes, чтобы я не связывал ненужные зависимости с теми, кто его включает.

Теперь, если мне нужно что-то produced()Я включаю ConfigFactory.h и объявить специализацию для него. Я поместил его определение в некоторый исходный файл (ConfigFactory.cpp) а также produce() объект, использующий параметры в this->data:

main.cpp:

#include "ConfigFactory.h"#include "Window.h"
template <> Window ConfigFactory::process() const;

int main (int argc, const char** argv) {
ConfigFactory factory{argc, argv};

Window window = factory.produce<Window>();
}

ConfigFactory.cpp, Window.cpp или кто знает, как сделать окно:

template <> Layout ConfigFactory::produce() const;

template <> Window ConfigFactory::produce() const
{
Window window{produce<Layout>()}; // required dependencies
window.setPrintFps(data->print_fps); // optional ones
return window;
}
  • Если я поставлю produce() Определения в соответствующих исходных файлах, у меня есть некоторые плохие предчувствия по этому поводу — потому что я определяю вещи, не объявленные в заголовке.
  • Но если я использую ConfigFactory.h для определений, он становится довольно длинным и тяжелым, поскольку он должен знать о каждый produce()способный класс.

В настоящее время я использую оба, и в целом это работает, но это не идеально.

0

По вопросам рекламы ammmcru@yandex.ru
Adblock
detector