C ++: должен ли класс владеть или соблюдать свои зависимости?

16

Скажем, у меня есть класс, Foobarкоторый использует (зависит от) класса Widget. В Widgetстарые добрые времена wolud объявлялся как поле в 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 ++ не тривиально? Должен ли класс владеть своими зависимостями или просто наблюдать за ними?

el.pescado
источник
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конструктора, то есть внутри одного из Utilities::Factoryметодов, например createWidget(const std::vector<std::string>& params), вы создаете виджет и внедряете его в Foobarобъект.

Теперь у вас есть Utilities::Factoryметод, который создал Widgetобъект. Означает ли это, что метод должен отвечать за его удаление? Конечно, нет. Это только для того, чтобы сделать вас примером.


Представим, что вы разрабатываете приложение, в котором будет несколько окон. Каждое окно представлено с использованием Foobarкласса, поэтому фактически Foobarдействует как контроллер.

Контроллер, вероятно, будет использовать некоторые из ваших, Widgetи вы должны спросить себя:

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

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 в unique_ptr, как бы вы хотели гарантировать unique_ness ???
Аконкагуа,
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, если вы хотите , или необходимости повторного использования виджетов, ..., или просто потому что для текущей проблемы это более уместно (например, если Widget является элементом GUI и FooBar должен / может / не должен быть одним).

Затем мы вернемся ко второму пункту: нет общего ответа. Вы должны решить, что - для актуальной проблемы - является более подходящим решением. Мне нравится эталонный подход DominicMcDonnell, но он может быть применен только в том случае, если FooBar не переходит во владение (ну, на самом деле, вы можете, но это подразумевает очень, очень грязный код ...). Кроме того, я присоединяюсь к ответу Дэвида Пэкера (тот, который должен быть написан как комментарий, но в любом случае хороший ответ).

Аконкагуа
источник
1
В C ++ не используйте classes только со статическими методами, даже если это просто фабрика, как в вашем примере. Это нарушает принципы ОО. Вместо этого используйте пространства имен и группируйте свои функции, используя их.
Энди
@DavidPacker Хм, конечно, вы правы - я молча предположил, что у класса есть какие-то частные внутренние органы, но это нигде не видно и не упоминается ...
Аконкагуа
1

Вам не хватает как минимум еще двух параметров, доступных вам в C ++:

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

Другой способ - взять полиморфные объекты по значению, используя глубокую копию. Традиционный способ сделать это - использовать метод виртуального клона, другой популярной опцией является использование стирания типов для создания типов значений, которые ведут себя полиморфно.

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

mattnewport
источник
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, опции в вашем клиентском коде также ограничены).

utnapistim
источник
1
Это исключает полиморфное поведение, которое серьезно ограничивает добавленную стоимость.
el.pescado
Правда. Я просто подумал упомянуть об этом, поскольку именно эту форму я использую чаще всего (я использую наследование в своем кодировании для полиморфного поведения - не повторное использование кода, поэтому у меня не так много случаев, когда мне нужно полиморфное поведение) ..
utnapistim