Чи один об'єкт конфігурації - це погана ідея?


43

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

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

  • Надайте об’єкту конфігурації сетер, який використовується ТІЛЬКИ для тестування, щоб ви могли пройти в різних налаштуваннях.
  • Продовжуйте використовувати один конфігураційний об'єкт, але змініть його з однотонного на екземпляр, який ви обходите всюди, де це потрібно. Тоді ви можете сконструювати це один раз у вашій програмі та один раз у ваших тестах, з різними налаштуваннями.

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

Я починаю робити висновок, що такий тип конфігураційного об’єкта - це погана ідея. Що ти думаєш? Які існують альтернативи? І як почати рефакторинг програми, яка всюди використовує конфігурацію?


Конфігурація отримує свої налаштування, читаючи файл на диску, правда? То чому б просто не мати файл "test.config", який він може прочитати?
Анон.

Це вирішує першу проблему, але не другу.
JW01

@ JW01: Тоді всі ваші тести потребують помітно різної конфігурації? Вам доведеться налаштувати цю конфігурацію десь під час тесту, чи не так?
Анон.

Правда. Але якщо я продовжую використовувати єдиний пул налаштувань (з іншим пулом для тестування), то всі мої тести закінчуються з використанням однакових налаштувань. А оскільки я, можливо, хочу перевірити поведінку з різними налаштуваннями, це не ідеально.
JW01

"Отже, у тесті вам потрібно встановити конфігурацію для тестуваного класу, а також ВСІ його залежності. Це може зробити ваш код тестування некрасивим." Маєте приклад? У мене таке відчуття, що не структура вашого цілого додатка, що підключається до класу config, а, скоріше, структура самого класу config, яка робить речі "потворними". Чи не слід, якщо налаштування конфігурації класу автоматично налаштовує його залежності?
AndrewKS

Відповіді:


35

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

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

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

Якщо ви хочете вивчити ці ідеї далі, рекомендую ознайомитись з принципами SOLID , зокрема принципом поділу інтерфейсу та принципом інверсії залежності .


7

Я відокремлюю групи пов'язаних налаштувань за допомогою інтерфейсів.

Щось на зразок:

public interface INotificationEmailSettings {
   public string To { get; set; }
}

public interface IMediaFileSettings {
    public string BasePath { get; set; }
}

тощо.

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

Але розділення за інтерфейсом робить фактичні залежності набагато більш чіткими і чітко вираженими, і це очевидне поліпшення для встановлення.

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

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

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

public static class AllSettings {
    public INotificationEmailSettings NotificationEmailSettings {
        get {
            return ServiceLocator.Get<INotificationEmailSettings>();
        }
    }
}

Я вважаю цю суміш найкращою з усіх світів.


3

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

(Обов’язкове посилання: Ефективна робота зі застарілим кодом . Він містить цей та багато інших безцінних прийомів для застарілого коду тестування.)

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

Це вже наближається до справжнього рішення щодо введення залежності, як і Spring для Java. Якщо ви зможете перейти на такі рамки, добре. Але в реальному житті, особливо в роботі зі застарілим додатком, найчастіше компромісний компроміс - це повільний і ретельний рефакторинг до DI.


Мені дуже сподобався ваш підхід, і я не хотів би припускати, що ідею сетера слід зберігати в таємниці або вважати «швидким злом» з цього питання. Якщо ви сприймаєте позицію, що одиничні тести є важливим користувачем / споживачем / частиною вашої програми, то наявність test_атрибутів і методів уже не така вже й погана справа, правда? Я погоджуюся, що справжнє рішення проблеми полягає у використанні рамки DI, але у таких прикладах, як ваш, коли ви працюєте зі застарілим кодом чи іншими простими випадками, не відчужуючий код тесту застосовується чисто.
Філіп Дупанович

1
@kRON, це надзвичайно корисні та практичні хитрощі для того, щоб зробити застарілий блок коду перевіряючим, і я їх також широко використовую. Однак в ідеалі виробничий код не повинен містити будь-яких особливостей, введених виключно для тестування. Зрештою, саме до цього я спрямовуюсь.
Péter Török

2

На мій досвід, у світі Java саме для цього призначена Весна. Він створює та керує об'єктами (бобами) на основі певних властивостей, які встановлюються під час виконання, і в іншому випадку є прозорими для вашої програми. Ви можете перейти з файлом test.config, який Anon. згадує або ви можете включити певну логіку у конфігураційний файл, який Spring буде обробляти для встановлення властивостей на основі якогось іншого ключа (наприклад, ім'я хоста або середовища).

Що стосується вашої другої проблеми, ви можете її обійти деякою архітектурою, але це не так серйозно, як здається. Це означає у вашому випадку те, що ви, наприклад, не мали б глобального об’єкта Config, який використовують різні інші класи. Ви просто налаштуєте ці інші класи через Spring, а потім не матимете жодного об’єкта config, оскільки все, що потрібно для конфігурації, отримало через Spring, щоб ви могли використовувати ці класи безпосередньо у своєму коді.


2

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

Шаблон Singleton вважається шкідливим. Це означає, що це не завжди неправильна річ, але це зазвичай . Якщо ви коли-небудь плануєте використовувати одинарний візерунок для чого-небудь , перестаньте розглядати:

  • Вам коли-небудь знадобиться підкласирувати його?
  • Вам коли-небудь знадобиться запрограмувати інтерфейс?
  • Чи потрібно запускати одиничні тести проти нього?
  • Вам потрібно буде часто змінювати його?
  • Чи ваша програма призначена для підтримки декількох виробничих платформ / середовищ?
  • Ви навіть трохи стурбовані використанням пам'яті?

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

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


0

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

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

Якщо ви завантажуєте з конфігураційних файлів, у вас може бути ієрархія файлів. Параметри в одному файлі можуть визначати, який з інших файлів потрібно завантажити. Я використовував цей механізм з сервісами C # Windows, які мають кілька необов’язкових компонентів, кожен з яких має власні настройки конфігурації. Шаблони файлових файлів були однакові у файлах, але завантажувались або не ґрунтувалися на основних параметрах файлу.


0

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

Крім того, ви використовуєте Singleton з причини, яка не підходить. Однотонні слід використовувати лише тоді, коли існування декількох об'єктів створить поганий стан. Використання Singleton у цьому випадку недоцільно, оскільки наявність декількох зчитувачів конфігурації не повинно негайно викликати стан помилок. Якщо у вас є більше одного, ви, ймовірно, робите це неправильно, але це не зовсім обов'язково, щоб у вас був лише один зчитувач конфігурації.

Нарешті, створення такого глобального стану - це порушення інкапсуляції, оскільки ви дозволяєте отримувати більше класів, ніж потрібно знати доступ безпосередньо до даних.


0

Я думаю, що якщо налаштувань багато, з "згустками" пов'язаних з ними, є сенс розділити їх на окремі інтерфейси з відповідними реалізаціями - тоді введіть ті інтерфейси, де це потрібно з DI. Ви отримуєте провідність, менший зв'язок і SRP.

Мене надихнуло це питання Джошуа Фланаган з Los Techies; він написав статтю з цього питання деякий час тому.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.