Управление параметрами в приложении ООП

15

Я пишу ООП приложения среднего размера на C ++ как способ применения принципов ООП.

У меня есть несколько классов в моем проекте, и некоторые из них должны иметь доступ к параметрам конфигурации во время выполнения. Эти параметры считываются из нескольких источников при запуске приложения. Некоторые читаются из файла конфигурации в домашнем каталоге пользователя, некоторые - аргументы командной строки (argv).

Итак, я создал класс ConfigBlock. Этот класс читает все источники параметров и сохраняет их в соответствующей структуре данных. Примерами являются пути и имена файлов, которые могут быть изменены пользователем в файле конфигурации, или флаг CLI --verbose. Затем можно позвонить ConfigBlock.GetVerboseLevel(), чтобы прочитать этот конкретный параметр.

Мой вопрос: это хорошая практика, чтобы собрать все такие данные конфигурации времени выполнения в одном классе?

Затем моим классам нужен доступ ко всем этим параметрам. Я могу придумать несколько способов добиться этого, но я не уверен, какой из них выбрать. Конструктор класса может быть заданной ссылкой на мой ConfigBlock, например

public:
    MyGreatClass(ConfigBlock &config);

Или они просто включают заголовок «CodingBlock.h», который содержит определение моего CodingBlock:

extern CodingBlock MyCodingBlock;

Затем только файл .cpp классов должен включать и использовать содержимое ConfigBlock.
Файл .h не представляет этот интерфейс пользователю класса. Тем не менее, интерфейс к ConfigBlock все еще существует, однако он скрыт от файла .h.

Это хорошо, чтобы скрыть это так?

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

lugge86
источник

Ответы:

10

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

explicit MyGreatClass(const ConfigBlock& config);

... более подходящий интерфейс может быть таким:

MyGreatClass(int foo, float bar, const string& baz);

... вместо того, чтобы просто собирать эти foo/bar/bazполя из массива ConfigBlock.

Ленивый дизайн интерфейса

С другой стороны, этот вид дизайна позволяет легко создавать стабильный интерфейс для вашего конструктора, например, поскольку, если вам в конечном итоге понадобится что-то новое, вы можете просто загрузить это в ConfigBlock(возможно, без каких-либо изменений кода), а затем cherry- выбрать любой новый материал, который вам нужен, без какого-либо изменения интерфейса, только изменение реализации MyGreatClass.

Так что это как за, так и против, что это освобождает вас от разработки более тщательно продуманного интерфейса, который принимает только те входные данные, которые ему действительно необходимы. Он применяет мышление: «Просто дайте мне этот большой массив данных, я выберу из него то, что мне нужно», а не что-то вроде «Эти точные параметры - это то, что этот интерфейс должен работать».

Так что, безусловно, здесь есть некоторые плюсы, но они могут сильно перевесить минусы.

Связь

В этом сценарии все такие классы, создаваемые из ConfigBlockэкземпляра, в конечном итоге имеют зависимости:

введите описание изображения здесь

Это может стать PITA, например, если вы хотите провести модульное тестирование Class2в этой диаграмме изолированно. Возможно, вам придется поверхностно смоделировать различные ConfigBlockвходные данные, содержащие соответствующие поля Class2, чтобы иметь возможность протестировать его в различных условиях.

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

Повторное использование / готовность к развертыванию / Тестируемость

Вместо этого, если вы спроектируете эти интерфейсы надлежащим образом, мы можем отделить их ConfigBlockи получить что-то вроде этого:

введите описание изображения здесь

Если вы заметили на этой диаграмме выше, все классы становятся независимыми (их афферентные / исходящие связи уменьшаются на 1).

Это приводит к гораздо большему количеству независимых классов (по крайней мере, независимых ConfigBlock), которые намного проще (повторно) использовать / тестировать в новых сценариях / проектах.

Теперь этот Clientкод оказывается тем, который должен зависеть от всего и собирать все это вместе. Бремя в конечном итоге переносится в этот клиентский код для чтения соответствующих полей из ConfigBlockи передачи их в соответствующие классы в качестве параметров. Тем не менее, такой клиентский код, как правило, узко разработан для конкретного контекста, и его потенциал для повторного использования, как правило, будет в любом случае нулевым или закрытым (это может быть mainфункция точки входа вашего приложения или что-то в этом роде).

Таким образом, с точки зрения повторного использования и тестирования, это может помочь сделать эти классы более независимыми. С точки зрения интерфейса для тех, кто использует ваши классы, это также может помочь явно указать, какие параметры им нужны, а не только один массив, ConfigBlockкоторый моделирует весь набор полей данных, необходимых для всего.

Вывод

В общем, этот вид класс-ориентированного дизайна, который зависит от монолита, в котором есть все необходимое, как правило, имеет такие характеристики. Их применимость, возможность развертывания, возможность повторного использования, тестируемость и т. Д. Могут значительно ухудшиться. Тем не менее, они могут отчасти упростить дизайн интерфейса, если мы попытаемся сделать это положительно. Вы должны оценить эти плюсы и минусы и решить, стоят ли компромиссы того. Как правило, гораздо безопаснее ошибиться против такого дизайна, когда вы выбираете вишню из монолита в классах, которые обычно предназначены для моделирования более общего и широко применимого дизайна.

Последний, но тем не менее важный:

extern CodingBlock MyCodingBlock;

... это потенциально даже хуже (более искажено?) с точки зрения характеристик, описанных выше, чем подход с внедрением зависимостей, так как в итоге он связывает ваши классы не только с конкретным экземпляромConfigBlocks , но и непосредственно с ним. Это дополнительно ухудшает применимость / разворачиваемость / тестируемость.

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

marstato
источник
1

Обычно конфигурация приложения используется в основном объектами фабрики. Любой объект, зависящий от конфигурации, должен быть сгенерирован из одного из этих заводских объектов. Вы можете использовать абстрактный шаблон фабрики для реализации одного класса, который принимает весь ConfigBlockобъект. Этот класс предоставит открытые методы для возврата других объектов фабрики и передаст только часть, ConfigBlockотносящуюся к этому конкретному объекту фабрики. Таким образом, параметры конфигурации «просачиваются» от ConfigBlockобъекта к его членам и с фабрики фабрики на фабрики.

Я буду использовать C #, так как знаю язык лучше, но это должно быть легко перенесено на C ++.

public class ConfigBlock
{
    public ConfigBlock()
    {
        // Load config data and
        // connectionSettings = new ConnectionConfig();
        // connectionSettings...
    }

    private ConnectionConfig connectionSettings;

    public ConnectionConfig GetConnectionSettings()
    {
        return connectionSettings;
    }
}

public class FactoryProvider
{
    public FactoryProvider(ConfigBlock config)
    {
        this.config = config;
    }

    private ConfigBlock config;

    public ConnectionFactory GetConnectionFactory()
    {
        ConnectionConfig connectionSettings = config.GetConnectionSettings();

        return new ConnectionFactory(connectionSettings);
    }
}

public class ConnectionFactory
{
    public ConnectionFactory(ConnectionConfig settings)
    {
        this.settings = settings;
    }

    private ConnectionConfig settings;

    public Connection GetConnection()
    {
        return new Connection(settings.Hostname, settings.Port, settings.Username, settings.Password);
    }
}

После этого вам нужен некоторый класс, который действует как «приложение», которое создается в вашей основной процедуре:

// Your main procedure (yeah I'm bending the rules of C# a tad here,
// but you get the point).
int Main(string[] args)
{
    Application app = new Application();

    app.Run();
}

public class Application
{
    public Application()
    {
        config = new ConfigBlock();
        factoryProvider = new FactoryProvider(config);
    }

    private ConfigBlock config;
    private FactoryProvider factoryProvider;

    public void Run()
    {
        ConnectionFactory connections = factoryProvider.GetConnectionFactory();
        Connection connection = connections.GetConnection();

        connection.Connect();

        // Enter into your main loop and do what this program is meant to do
    }
}

Как последнее замечание, это известно как «объект провайдера» в .NET. Похоже, что объекты провайдера в .NET объединяют данные конфигурации с объектами фабрики, что, по сути, и здесь вы хотите сделать.

См. Также шаблон провайдера для начинающих . Опять же, это ориентировано на разработку .NET, но поскольку C # и C ++ оба являются объектно-ориентированными языками, шаблон должен быть в основном переносимым между ними.

Еще одно хорошее прочтение, связанное с этим шаблоном: Модель провайдера .

Наконец, критика этого паттерна: провайдер не является паттерном

Грег Бургардт
источник
Все хорошо, кроме ссылок на модели провайдера. Отражение не поддерживается с ++, и это не будет работать.
BЈовић
@ BЈовић: Верно. Отражение класса не существует, однако вы можете встроить ручной обходной путь, который в основном сводится к switchутверждению или ifпроверке утверждения по значению, считанному из файлов конфигурации.
Грег Бургхардт
0

Первый вопрос: это хорошая практика, чтобы собрать все такие данные конфигурации времени выполнения в одном классе?

Да. Лучше централизовать константы и значения времени выполнения и код для их чтения.

Конструктор класса может быть ссылкой на мой ConfigBlock

Это плохо: большинству ваших конструкторов не понадобится большинство значений. Вместо этого создайте интерфейсы для всего, что нетривиально построить:

старый код (ваше предложение):

MyGreatClass(ConfigBlock &config);

новый код:

struct GreatClassData {/*...*/}; // initialization data for MyGreatClass
GreatClassData ConfigBlock::great_class_values();

создать экземпляр MyGreatClass:

auto x = MyGreatClass{ current_config_block.great_class_values() };

Вот current_config_blockэкземпляр вашего ConfigBlockкласса (тот, который содержит все ваши значения), и MyGreatClassкласс получает GreatClassDataэкземпляр. Другими словами, передавайте конструкторам только те данные, которые им нужны, и добавляйте в них средства ConfigBlockдля создания этих данных.

Или они просто включают заголовок «CodingBlock.h», который содержит определение моего CodingBlock:

 extern CodingBlock MyCodingBlock;

Затем только файл .cpp классов должен включать и использовать содержимое ConfigBlock. Файл .h не представляет этот интерфейс пользователю класса. Тем не менее, интерфейс к ConfigBlock все еще существует, однако он скрыт от файла .h. Это хорошо, чтобы скрыть это так?

Этот код предполагает, что у вас будет глобальный экземпляр CodingBlock. Не делайте этого: обычно вы должны иметь экземпляр, объявленный глобально, в любой точке входа, которую ваше приложение использует (основная функция, DllMain и т. Д.), И передавать ее как аргумент везде, где вам нужно (но, как объяснено выше, вы не должны передавать весь класс вокруг, просто выставить интерфейсы вокруг данных и передать их).

Кроме того, не привязывайте ваши клиентские классы (ваши MyGreatClass) к типу CodingBlock; Это означает, что если вы MyGreatClassберете строку и пять целых чисел, вам будет лучше передать эту строку и целые числа, чем вы будете передавать в CodingBlock.

utnapistim
источник
Я думаю, что это хорошая идея, чтобы отделить фабрики от конфига. Неудовлетворительно, чтобы реализация config знала, как создавать экземпляры компонентов, поскольку это обязательно приводит к двухсторонней зависимости, где ранее существовала только односторонняя зависимость. Это имеет большое значение при расширении вашего кода, особенно при использовании разделяемых библиотек, где интерфейсы действительно имеют значение
Джоэль Корнетт
0

Короткий ответ:

Вам не нужны все настройки для каждого из модулей / классов в вашем коде. Если вы это сделаете, значит, что-то не так с вашим объектно-ориентированным дизайном. Особенно в случае модульного тестирования установка всех переменных, которые вам не нужны, и передача этого объекта не поможет с чтением или обслуживанием.

Давид Пура
источник
Таким образом, я могу собрать код синтаксического анализатора (синтаксический анализ командной строки и файлы конфигурации) в одном центральном месте. Затем каждый класс может выбрать нужные параметры оттуда. Какой дизайн на ваш взгляд хорош?
lugge86 14.12.15
Возможно, я просто написал это неправильно - я имею в виду, что у вас есть (и это хорошая практика) общая абстракция со всеми настройками, полученными из конфигурационного файла / переменных среды - что может быть вашим ConfigBlockклассом. Смысл здесь в том, чтобы не предоставлять весь, в данном случае, контекст состояния системы, а только конкретные, необходимые значения для этого.
Давид Пура