C ++: Чи повинен клас володіти або спостерігати його залежності?


16

Скажіть, у мене є клас, Foobarякий використовує (залежить від) клас Widget. У добрі дні Widgetwolud буде оголошений як поле в Foobar, або, можливо, як розумний вказівник, якщо потрібна поліморфна поведінка, і вона буде ініціалізована в конструкторі:

class Foobar {
    Widget widget;
    public:
    Foobar() : widget(blah blah blah) {}
    // or
    std::unique_ptr<Widget> widget;
    public:
    Foobar() : widget(std::make_unique<Widget>(blah blah blah)) {}
    (…)
};

І ми б все налаштували і зробили. На жаль, сьогодні діти Java посміятимуться з нас, як тільки вони це побачать, і це справедливо, як це пара Foobarі Widgetразом. Рішення здається простим: застосуйте Dependency Injection, щоб вивести побудову залежностей з Foobarкласу. Але тоді C ++ змушує задуматися про власність залежностей. На думку приходять три рішення:

Унікальний покажчик

class Foobar {
    std::unique_ptr<Widget> widget;
    public:
    Foobar(std::unique_ptr<Widget> &&w) : widget(w) {}
    (…)
}

Foobarвимагає одноосібної власності на Widgetце, передане йому. Це має такі переваги:

  1. Вплив на продуктивність незначний.
  2. Це безпечно, оскільки Foobarконтролює термін його експлуатаціїWidget , тому переконайтеся, що Widgetвін раптово не зникне.
  3. Це безпечно, що Widgetне просочиться і буде зруйновано належним чином, коли більше не потрібно.

Однак це коштує:

  1. Він встановлює обмеження щодо використання Widgetекземплярів, наприклад, не Widgetsможна використовувати виділений стек , не Widgetможна ділитися.

Спільний покажчик

class Foobar {
    std::shared_ptr<Widget> widget;
    public:
    Foobar(const std::shared_ptr<Widget> &w) : widget(w) {}
    (…)
}

Це, мабуть, найближчий еквівалент Java та інших зібраних сміттям мов. Переваги:

  1. Більш універсальний, оскільки він дозволяє обмінюватися залежностями.
  2. Підтримує безпеку (точки 2 і 3) unique_ptrрозчину.

Недоліки:

  1. Витрачає ресурси, коли не використовується спільний доступ.
  2. Все ще вимагає розподілу купи та забороняє виділяти стеки об'єкти.

Звичайний оглядовий вказівник

class Foobar {
    Widget *widget;
    public:
    Foobar(Widget *w) : widget(w) {}
    (…)
}

Помістіть сирий покажчик всередину класу і перекладіть тягар власності на когось іншого. Плюси:

  1. Настільки просто, як це може вийти.
  2. Універсальний, приймає будь-яку Widget.

Мінуси:

  1. Більше не безпечно.
  2. Представляє інше підприємство, яке відповідає за право власності на Foobarі Widget.

Деякі шалені метапрограмування шаблону

Єдина перевага, яку я можу придумати, - це те, що я зможу прочитати всі ті книги, на які я не знаходив часу, поки моє програмне забезпечення буває;)

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

Я щось пропускаю? Або просто ін'єкція залежності в C ++ не є тривіальною? Чи повинен клас володіти своїми залежностями або просто спостерігати за ними?


1
З самого початку, якщо ви можете компілювати під C ++ 11 та / або ніколи, використання простого вказівника як способу обробки ресурсів у класі вже не є рекомендованим способом. Якщо клас Foobar є єдиним власником ресурсу і ресурс повинен бути звільнений, коли Foobar виходить із сфери застосування, std::unique_ptrце шлях. Ви можете використовувати std::move()для передачі права власності на ресурс з верхнього діапазону до класу.
Енді

1
Як я знаю, що Foobarце єдиний власник? У старому випадку це просто. Але проблема з DI, як я бачу, полягає в тому, що він відриває клас від побудови його залежностей, він також відокремлює його від власності на ці залежності (оскільки право власності пов'язане з будівництвом). У сміттєзбірних середовищах, таких як Java, це не проблема. У C ++ це так.
el.pescado

1
@ el.pescado Існує та сама проблема в Java, пам'ять - не єдиний ресурс. Також є файли та з'єднання, наприклад. Звичайний спосіб вирішити це в Java - мати контейнер, який керує життєвим циклом усіх його об'єктів, що містяться. Тому вам не доведеться турбуватися про це в об'єктах, що містяться в контейнері DI. Це також може працювати для додатків c ++, якщо є можливість, щоб контейнер DI міг знати, коли перейти на фазу життєвого циклу.
SpaceTrucker

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

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

Відповіді:


2

Я мав намір написати це як коментар, але це виявилося занадто довго.

Як я знаю, що Foobarце єдиний власник? У старому випадку це просто. Але проблема з DI, як я бачу, полягає в тому, що він відриває клас від побудови його залежностей, він також відокремлює його від власності на ці залежності (оскільки право власності пов'язане з будівництвом). У сміттєзбірних середовищах, таких як Java, це не проблема. У C ++ це так.

Якщо ви повинні використовувати std::unique_ptr<Widget>або std::shared_ptr<Widget>, що до вас , щоб вирішити , і йде від функціональності.

Припустимо, у вас є Utilities::Factory, який відповідає за створення ваших блоків, таких як Foobar. Дотримуючись принципу DI, вам знадобиться Widgetекземпляр, щоб ввести його за допомогою Foobarконструктора 's, тобто всередині одного з Utilities::Factoryметодів', наприклад createWidget(const std::vector<std::string>& params), ви створите віджет і введіть його в Foobarоб'єкт.

Тепер у вас є Utilities::Factoryметод, який створив Widgetоб'єкт. Чи означає це, метод повинен відповідати за його видалення? Звичайно, ні. Це лише зробити вас екземпляром.


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

Контролер, ймовірно, використовує деякі ваші WidgetS, і вам доведеться запитати себе:

Якщо я перейду до цього конкретного вікна у своїй програмі, мені знадобляться ці Віджети. Чи поділяються ці віджети серед інших вікон додатків? Якщо так, я, мабуть, не повинен їх створювати заново, тому що вони завжди будуть виглядати однаково, тому що вони є спільними.

std::shared_ptr<Widget> це шлях.

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

Ось тут і std::unique_ptr<Widget>потрібно претендувати на трон.


Оновлення:

Я не дуже погоджуюся з @DominicMcDonnell , щодо життєвого питання. Виклик std::moveна std::unique_ptrповністю передає у власність, так що навіть якщо ви створюєте object Aв методі і передати його іншому , object Bяк залежність, то object Bтепер буде нести відповідальність за ресурс object Aі правильно видалити його, коли object Bвиходить з області видимості.


Загальна ідея власності та розумних покажчиків мені зрозуміла. Мені просто не здається добре грати з ідеєю DI. На мене введення залежності залежить від роз'єднання класу від його залежностей, але в цьому випадку клас все ще впливає на те, як створюються його залежності.
el.pescado

Наприклад, проблема, з якою я маю unique_ptrтестування в одиницях (DI рекламується як тестування): я хочу протестувати Foobar, тому я створюю Widget, передаю його Foobar, здійснюю, Foobarа потім хочу перевірити Widget, але, якщо Foobarякимось чином не піддаю це, я можу з тих пір, як на це заявили Foobar.
el.pescado

@ el.pescado Ви змішуєте дві речі разом. Ви змішуєте доступність та право власності. Тільки тому, що Foobarволодіти ресурсом не означає, ніхто більше не повинен ним користуватися. Ви можете реалізувати такий Foobarметод, як Widget* getWidget() const { return this->_widget.get(); }, який поверне вам необроблений покажчик, з яким ви можете працювати. Потім ви можете використовувати цей метод в якості вхідного даних для ваших одиничних тестів, коли ви хочете перевірити Widgetклас.
Енді

Хороший момент, це має сенс.
el.pescado

1
Якщо ви перетворили shared_ptr в унікальний_ptr, як би ви хотіли гарантувати унікальність ???
Аконкагуа

2

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

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

Оновлення

Відповідь Девіда Пакера змусила мене трохи більше подумати над питанням. Оригінальна відповідь справедлива для спільних залежностей у моєму досвіді, що є однією з переваг введення залежності, ви можете мати кілька примірників, використовуючи один екземпляр залежності. Якщо ваш клас повинен мати власний екземпляр певної залежності, то std::unique_ptrце правильна відповідь.


1

Перш за все - це C ++, а не Java - і тут багато речей йдуть по-різному. У людей Java немає тих проблем щодо власності, оскільки існує автоматичне збирання сміття, яке вирішує їх.

По-друге: На це питання немає загальної відповіді - це залежить від того, які вимоги є!

У чому полягає проблема з’єднання FooBar і Widget? FooBar хоче використовувати віджет, і якщо кожен екземпляр FooBar завжди матиме свій і той самий віджет, залиште його в парі ...

У C ++ ви навіть можете робити "дивні" речі, яких просто не існує на Java, наприклад, має конструктор варіативного шаблону (ну, у Java існує позначення ..., яке також можна використовувати в конструкторах, але це просто синтаксичний цукор для приховування масиву об'єктів, який насправді не має нічого спільного з реальними шаблонами варіантів!) - категорія "Деякі шалені метапрограмування шаблону":

namespace WidgetFactory
{
    Widget* create(int, int)
    {
        return 0;
    }
    Widget* create(int, bool, long)
    {
        return 0;
    }
}
class FooBar
{
public:
    template < typename ...Arguments >
    FooBar(Arguments... arguments)
        : mWidget(WidgetFactory::create(arguments...))
    {
    }
    ~FooBar()
    {
        delete mWidget;
    }
private:
    Widget* mWidget;
};

FooBar foobar1(10, 12);
FooBar foobar2(51, true, 54L);

Люблю це?

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

Потім ми повертаємось до другого пункту: Немає загальної відповіді. Вам потрібно вирішити, що - для реальної проблеми - є більш підходящим рішенням. Мені подобається довідковий підхід DominicMcDonnell, але він може бути застосований лише в тому випадку, якщо право власності не візьме FooBar (ну, насправді, ви могли, але це означає дуже, дуже брудний код ...). Окрім цього, я приєднуюся до відповіді Девіда Пакера (той мав бути написаний як коментар - але все-таки хороша відповідь).


1
У C ++ не використовуйте classes, окрім статичних методів, навіть якщо це лише фабрика, як у вашому прикладі. Це порушує принципи ОО. Замість цього використовуйте простори імен і групуйте свої функції, використовуючи їх.
Енді

@DavidPacker Хм, звичайно, ти маєш рацію - я мовчки вважав, що у класу є приватні інтернати, але це ні видно, ні згаданих інших місць ...
Aconcagua,

1

Вам не вистачає щонайменше ще двох варіантів, які доступні вам на C ++:

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

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

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


0

Ви проігнорували четверту можливу відповідь, яка поєднує перший код, який ви опублікували ("добрі дні" зберігання члена за значенням) із введенням залежності:

class Foobar {
    Widget widget;
public:
    Foobar(Widget w) // pass w by value
     : widget{ std::move(w) } {}
};

Потім код клієнта може написати:

Widget w;
Foobar f1{ w }; // default: copy w into f1
Foobar f2{ std::move(w) }; // move w into f2

Представлення з'єднання між об'єктами не повинно здійснюватися (чисто) за переліченими вами критеріями (тобто не базуватися виключно на тому, "що краще для управління безпечним життям").

Ви також можете використовувати концептуальні критерії ("автомобіль має чотири колеса" проти "автомобіль використовує чотири колеса, які водій повинен взяти з собою").

Ви можете мати критерії, накладені іншими API (якщо ви отримуєте від API - це спеціальна обгортка або, наприклад, std :: unique_ptr, варіанти вашого коду клієнта також обмежені).


1
Це виключає поліморфну ​​поведінку, що сильно обмежує додану вартість.
el.pescado

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