Управління параметрами в додатку OOP


15

Я пишу заяву OOP середнього розміру в C ++ як спосіб практикувати принципи OOP.

У моєму проекті є кілька класів, і деякі з них потребують доступу до параметрів конфігурації під час виконання. Ці параметри зчитуються з декількох джерел під час запуску програми. Деякі читаються з конфігураційного файлу в домашньому режимі користувачів, деякі - аргументи командного рядка (argv).

Тому я створив клас ConfigBlock. Цей клас зчитує всі джерела параметрів і зберігає їх у відповідній структурі даних. Прикладами є шляхи та назви файлів, які користувач може змінити у конфігураційному файлі, або прапор CLI --verbose. Потім можна зателефонувати ConfigBlock.GetVerboseLevel(), щоб прочитати цей конкретний параметр.

Моє запитання: Чи правильно збирати такі конфігураційні дані виконання в одному класі?

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

public:
    MyGreatClass(ConfigBlock &config);

Або вони просто включають заголовок "CodingBlock.h", який містить визначення мого CodingBlock:

extern CodingBlock MyCodingBlock;

Тоді лише файли .cpp класу повинні включати та використовувати речі ConfigBlock.
Файл .h не вводить цей інтерфейс користувачеві класу. Однак інтерфейс до ConfigBlock все ще існує, однак він прихований від файлу .h.

Чи добре це приховати таким чином?

Я хочу, щоб інтерфейс був якомога меншим, але, врешті-решт, я думаю, що кожен клас, який потребує параметрів конфігурації, повинен мати підключення до мого ConfigBlock. Але, як повинен виглядати цей зв’язок?

Відповіді:


10

Я цілком прагматик, але моя головна турбота тут полягає в тому, що ви можете дозволити цьому ConfigBlockдомінувати над вашими конструкціями інтерфейсів, можливо, погано. Коли у вас є щось подібне:

explicit MyGreatClass(const ConfigBlock& config);

... Більш відповідний інтерфейс може бути таким:

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

... на відміну від того, щоб просто збирати вишні з цих foo/bar/bazмасивів ConfigBlock.

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

З іншого боку, такий дизайн спрощує створення стабільного інтерфейсу для вашого конструктора, наприклад, оскільки якщо вам потрібне щось нове, ви можете просто завантажити його в ConfigBlock(можливо, без змін коду), а потім вибирайте будь-які нові речі, які вам потрібні, без будь-яких змін інтерфейсу, лише зміни в реалізації MyGreatClass.

Так що це одночасно і професіонал, і це, що це звільняє вас від створення більш ретельно продуманого інтерфейсу, який приймає лише ті дані, які йому фактично потрібні. Він застосовує мислення: "Просто дайте мені цей масивний обсяг даних, я виберу, що мені потрібно від нього" , на відміну від чогось більш схожого, "Ці точні параметри - це те, що цей інтерфейс повинен працювати".

Тож тут безумовно є деякі плюси, але вони можуть сильно перевершити мінуси.

Зчеплення

У цьому сценарії всі такі класи, що будуються з ConfigBlockекземпляра, мають залежність таким чином:

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

Це може стати PITA, наприклад, якщо ви хочете Class2ізолювати тест на цій діаграмі ізольовано. Можливо, вам доведеться поверхово моделювати різні ConfigBlockвхідні дані, що містять відповідні поля Class2, які зацікавлені, щоб мати змогу протестувати його в різних умовах.

У будь-якому новому контексті (будь то тестовий блок або весь новий проект), будь-які подібні класи можуть стати тягарем для (повторного) використання, оскільки ми змушені завжди брати ConfigBlockз собою їзди та налаштовувати його. відповідно.

Можливість повторної використання / розгортання / завіреність

Натомість, якщо ви правильно розробите ці інтерфейси, ми можемо роз’єднати їх ConfigBlockі закінчити щось подібне:

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

Якщо ви помітили на цій вище діаграмі, всі класи стають незалежними (їх аферентні / вихідні сполучення зменшуються на 1).

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

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

Тож з точки зору повторного використання та тестування це може допомогти зробити ці класи більш незалежними. З точки зору інтерфейсу для тих, хто використовує ваші класи, це також може допомогти чітко вказати, які параметри їм потрібні, а не лише один масивний, ConfigBlockякий моделює весь всесвіт полів даних, необхідних для всього.

Висновок

Взагалі, такий клас, орієнтований на клас дизайну, який залежить від моноліту, який має все необхідне, має властивості такого роду. Їх застосовність, розгортаність, повторне використання, тестабельність тощо можуть в результаті значно погіршитись. Однак вони можуть настільки спростити дизайн інтерфейсу, якщо ми спробуємо на ньому позитивно повернутись. Ви вирішуєте оцінювати ці плюси і мінуси і вирішувати, чи варті цього компроміси. Зазвичай набагато безпечніше помилятися проти подібного дизайну, коли ви вибираєте вишню з моноліту в класах, які, як правило, призначені для моделювання більш загального та широко застосовуваного дизайну.

Останнє, але не менш важливе:

extern CodingBlock MyCodingBlock;

... це потенційно ще гірше (більш асиметричним?) З точки зору характеристик , описаних вище , ніж ін'єкції підходу залежно, як вона закінчується з'єднанням класи не тільки ConfigBlocks, але і безпосередньо до конкретного екземпляру цього. Це ще більше погіршує застосовність / розгорнутість / доказовість.

Моя загальна порада буде помилятися на стороні проектування інтерфейсів, які не залежать від таких видів монолітів, щоб вказати їх параметри, принаймні для найбільш загальноприйнятих класів, які ви розробляєте. І уникайте глобального підходу без введення залежності, якщо можете, якщо у вас справді немає дуже сильних і впевнених причин не уникати цього.


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 ++ обидва є об'єктно-орієнтованими мовами, модель повинна здебільшого передаватися між двома.

Ще одне добре прочитання, пов’язане з цією схемою: Модель постачальника .

Нарешті, критика цього шаблону: Постачальник - це не зразок


Все добре, крім посилань на моделі постачальника. Рефлексія не підтримується 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приймаєте рядок і п'ять цілих чисел, вам буде краще пропустити цей рядок і цілі числа, ніж ви будете передавати в a CodingBlock.


Я думаю, що це гарна ідея від’єднати фабрики від конфігу. Незадовільно, що реалізація конфігурації повинна знати, як інстанціювати компоненти, оскільки це обов'язково призводить до двосторонньої залежності, де раніше існувала лише однобічна залежність. Це має великі наслідки для розширення коду, особливо при використанні спільних бібліотек, де інтерфейси дійсно мають значення
Джоел Корнетт

0

Коротка відповідь:

Вам не потрібні всі налаштування для кожного з модулів / класів у вашому коді. Якщо ви це робите, то у вашому об’єктно-орієнтованому дизайні щось не так. Особливо у випадку тестування одиниць налаштування всіх змінних, які вам не потрібні, і передача цього об'єкта не допоможе зчитуванням або підтримкою.


Таким чином я можу зібрати код аналізатора (командний рядок розбору та конфігураційні файли) в одному центральному місці. Тоді кожен клас може вибрати звідти свої відповідні параметри. Який на ваш погляд хороший дизайн?
lugge86

Можливо, я просто написав це неправильно - я маю на увазі, що у вас є (і це хороша практика) загальна абстракція з усіма параметрами, отриманими від змінних файлів конфігурації / середовища - які можуть бути вашим ConfigBlockкласом. Сенс тут полягає в тому, щоб не надати в даному випадку всього контексту стану системи, а лише конкретних, необхідних для цього значень.
Давід Пура
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.