Скажем, у меня есть класс, 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
это передана ему. Это имеет следующие преимущества:
- Влияние на производительность незначительно.
- Это безопасно, так как
Foobar
контролирует время жизниWidget
, поэтому оноWidget
не пропадает внезапно. - Это безопасно, что
Widget
не будет течь и будет должным образом разрушено, когда больше не нужно.
Тем не менее, это происходит за счет:
- Он накладывает ограничения на то, как
Widget
могут использоваться экземпляры, например, нельзя использовать ни один выделенный стекWidgets
, ниWidget
общий.
Общий указатель
class Foobar {
std::shared_ptr<Widget> widget;
public:
Foobar(const std::shared_ptr<Widget> &w) : widget(w) {}
(…)
}
Это, вероятно, самый близкий эквивалент Java и других языков для сборки мусора. Преимущества:
- Более универсальный, так как позволяет обмениваться зависимостями.
- Поддерживает безопасность (пункты 2 и 3)
unique_ptr
решения.
Недостатки:
- Тратит ресурсы, когда не происходит обмена.
- По-прежнему требует выделения кучи и запрещает объекты, выделенные стеком.
Обычный наблюдатель
class Foobar {
Widget *widget;
public:
Foobar(Widget *w) : widget(w) {}
(…)
}
Поместите необработанный указатель в класс и перенесите бремя владения на другого. Плюсы:
- Так просто, как только можно.
- Универсальный, принимает только любой
Widget
.
Минусы:
- Больше не безопасно.
- Представляет другое юридическое лицо, которое отвечает за владение обоими
Foobar
иWidget
.
Какой-то сумасшедший шаблон метапрограммирования
Единственное преимущество, которое я могу придумать, - это то, что я смогу читать все те книги, на которые у меня не было времени, пока я строил свое программное обеспечение;)
Я склоняюсь к третьему решению, так как оно является наиболее универсальным, в Foobars
любом случае чем-то нужно управлять , поэтому управление Widgets
- это простое изменение. Тем не менее, использование необработанных указателей беспокоит меня, с другой стороны, решение для интеллектуальных указателей кажется мне неправильным, поскольку они заставляют потребителя зависимости ограничивать создание этой зависимости.
Я что-то пропустил? Или просто внедрение зависимостей в C ++ не тривиально? Должен ли класс владеть своими зависимостями или просто наблюдать за ними?
источник
std::unique_ptr
это путь. Вы можете использоватьstd::move()
для переноса владения ресурсом из верхней области в класс.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
выходит из области видимости.источник
unique_ptr
которой я сталкиваюсь, заключается в модульном тестировании (DI объявлен как дружественный к тестированию): я хочу протестироватьFoobar
, поэтому я создаюWidget
, передаю егоFoobar
, выполняю,Foobar
а затем хочу проверитьWidget
, но, еслиFoobar
каким-то образом это не разоблачить, я могу с тех пор, как это было заявленоFoobar
.Foobar
владеет ресурсом, не означает, что никто другой не должен его использовать. Вы можете реализовать такойFoobar
метод, какWidget* getWidget() const { return this->_widget.get(); }
, который вернет вам необработанный указатель, с которым вы можете работать. Затем вы можете использовать этот метод в качестве входных данных для ваших модульных тестов, когда вы хотите протестироватьWidget
класс.Я бы использовал указатель наблюдения в виде ссылки. Он дает гораздо лучший синтаксис, когда вы его используете, и имеет семантическое преимущество, заключающееся в том, что он не подразумевает владение, что может сделать простой указатель.
Самая большая проблема с этим подходом - это время жизни. Вы должны убедиться, что зависимость построена до и уничтожена после вашего зависимого класса. Это не простая проблема. Использование общих указателей (в качестве хранилища зависимостей и во всех классах, которые зависят от него, вариант 2 выше) может устранить эту проблему, но также представляет проблему циклических зависимостей, которая также является нетривиальной, и, на мой взгляд, менее очевидной и, следовательно, труднее обнаружить, прежде чем это вызовет проблемы. Именно поэтому я предпочитаю не делать это автоматически, а вручную управлять временем жизни и строительным заказом. Я также видел системы, использующие легкий шаблонный подход, который создавал список объектов в том порядке, в котором они были созданы, и уничтожал их в обратном порядке, это не было надежно, но это делало вещи намного проще.
Обновить
Ответ Дэвида Пэкера заставил меня задуматься над этим вопросом. Исходный ответ верен для общих зависимостей в моем опыте, что является одним из преимуществ внедрения зависимостей, вы можете иметь несколько экземпляров, используя один экземпляр зависимости. Однако, если вашему классу нужен собственный экземпляр определенной зависимости, тогда
std::unique_ptr
правильный ответ.источник
Прежде всего - это C ++, а не Java - и здесь многое происходит иначе. У людей Java нет таких проблем с владением, поскольку существует автоматическая сборка мусора, которая решает их за них.
Второе: на этот вопрос нет общего ответа - все зависит от требований!
В чем проблема соединения FooBar и Widget? FooBar хочет использовать виджет, и если каждый экземпляр FooBar всегда будет иметь свой собственный и один и тот же виджет, оставьте его связанным ...
В C ++ вы можете даже делать «странные» вещи, которые просто не существуют в Java, например, иметь конструктор шаблонов с переменным числом аргументов (ну, в Java существует нотация ..., которая также может использоваться в конструкторах, но это просто синтаксический сахар, чтобы скрыть массив объектов, который на самом деле не имеет ничего общего с реальными шаблонами с переменными числами!) - категория «Некоторое сумасшедшее метапрограммирование шаблонов»:
Нравится?
Конечно, есть есть причины , когда вы хотите или должны разъединить оба класса - например , если вам нужно создать виджеты в то время , задолго до того, существует какой - либо экземпляр FooBar, если вы хотите , или необходимости повторного использования виджетов, ..., или просто потому что для текущей проблемы это более уместно (например, если Widget является элементом GUI и FooBar должен / может / не должен быть одним).
Затем мы вернемся ко второму пункту: нет общего ответа. Вы должны решить, что - для актуальной проблемы - является более подходящим решением. Мне нравится эталонный подход DominicMcDonnell, но он может быть применен только в том случае, если FooBar не переходит во владение (ну, на самом деле, вы можете, но это подразумевает очень, очень грязный код ...). Кроме того, я присоединяюсь к ответу Дэвида Пэкера (тот, который должен быть написан как комментарий, но в любом случае хороший ответ).
источник
class
es только со статическими методами, даже если это просто фабрика, как в вашем примере. Это нарушает принципы ОО. Вместо этого используйте пространства имен и группируйте свои функции, используя их.Вам не хватает как минимум еще двух параметров, доступных вам в C ++:
Одним из них является использование «статического» внедрения зависимостей, где ваши зависимости являются параметрами шаблона. Это дает вам возможность удерживать ваши зависимости по значению, в то же время позволяя скомпилировать внедрение зависимости от времени. Контейнеры STL используют этот подход, например, для распределителей и функций сравнения и хеширования.
Другой способ - взять полиморфные объекты по значению, используя глубокую копию. Традиционный способ сделать это - использовать метод виртуального клона, другой популярной опцией является использование стирания типов для создания типов значений, которые ведут себя полиморфно.
Какой вариант является наиболее подходящим, зависит от вашего варианта использования, трудно дать общий ответ. Если вам нужен только статический полиморфизм, хотя я бы сказал, что шаблоны - это наиболее подходящий путь для C ++.
источник
Вы проигнорировали четвертый возможный ответ, который объединяет первый опубликованный вами код («хорошие старые дни» хранения члена по значению) с внедрением зависимости:
Код клиента может тогда написать:
Представление связи между объектами не должно осуществляться (чисто) по критериям, которые вы перечислили (то есть не основано исключительно на «что лучше для безопасного управления жизненным циклом»).
Вы также можете использовать концептуальные критерии («автомобиль имеет четыре колеса» против «автомобиль использует четыре колеса, которые должен иметь водитель»).
У вас могут быть критерии, налагаемые другими API (если то, что вы получаете от API - это, например, пользовательская оболочка или std :: unique_ptr, опции в вашем клиентском коде также ограничены).
источник