Powiedzmy, że mam klasę, Foobar
która używa (zależy od) klasy Widget
. W Widget
dawnych czasach wolud może być zadeklarowany jako pole Foobar
, a może jako inteligentny wskaźnik, jeśli zachodzi potrzeba zachowania polimorficznego, i zostanie zainicjowany w konstruktorze:
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)) {}
(…)
};
I wszystko byłoby gotowe. Niestety, dzisiaj Java dzieci będą śmiać się z nas, gdy widzą to, i słusznie, za nim par Foobar
i Widget
razem. Rozwiązanie jest z pozoru proste: zastosuj Dependency Injection, aby usunąć konstrukcję zależności z Foobar
klasy. Ale potem C ++ zmusza nas do myślenia o własności zależności. Przychodzą mi na myśl trzy rozwiązania:
Unikalny wskaźnik
class Foobar {
std::unique_ptr<Widget> widget;
public:
Foobar(std::unique_ptr<Widget> &&w) : widget(w) {}
(…)
}
Foobar
twierdzi, że wyłączna własność Widget
tej własności została mu przekazana. Ma to następujące zalety:
- Wpływ na wydajność jest znikomy.
- Jest bezpieczny, ponieważ
Foobar
kontroluje jego żywotnośćWidget
, dzięki czemu zapewnia, żeWidget
nie zniknie nagle. - Jest bezpieczny, że
Widget
nie wycieknie i zostanie odpowiednio zniszczony, gdy nie będzie już potrzebny.
Jest to jednak kosztem:
- Nakłada ograniczenia na sposób
Widget
użycia instancji, np. NieWidgets
można użyć alokacji stosu , nieWidget
można współdzielić.
Współdzielony wskaźnik
class Foobar {
std::shared_ptr<Widget> widget;
public:
Foobar(const std::shared_ptr<Widget> &w) : widget(w) {}
(…)
}
Jest to prawdopodobnie najbliższy odpowiednik języków Java i innych śmieci. Zalety:
- Bardziej uniwersalny, ponieważ umożliwia współdzielenie zależności.
- Utrzymuje bezpieczeństwo (punkty 2 i 3)
unique_ptr
roztworu.
Niedogodności:
- Marnuje zasoby, gdy nie dotyczy udostępniania.
- Nadal wymaga alokacji sterty i nie zezwala na obiekty alokowane na stosie.
Zwykły wskaźnik obserwujący
class Foobar {
Widget *widget;
public:
Foobar(Widget *w) : widget(w) {}
(…)
}
Umieść surowy wskaźnik wewnątrz klasy i przenieś ciężar własności na kogoś innego. Plusy:
- Tak proste, jak to tylko możliwe.
- Uniwersalny, akceptuje tylko każdy
Widget
.
Cons:
- Nie jest już bezpieczny.
- Wprowadza inny podmiot, który jest odpowiedzialny za własność zarówno
Foobar
iWidget
.
Niektóre szalone metaprogramowanie szablonu
Jedyną zaletą, jaką mogę wymyślić, jest możliwość przeczytania wszystkich książek, na które nie znalazłem czasu, gdy moje oprogramowanie się buduje;)
Skłaniam się ku trzeciemu rozwiązaniu, ponieważ jest najbardziej uniwersalne, czymś i tak trzeba zarządzać Foobars
, więc zarządzanie Widgets
jest prostą zmianą. Jednak używanie surowych wskaźników przeszkadza mi, z drugiej strony inteligentne rozwiązanie wskaźnika wydaje mi się złe, ponieważ ograniczają sposób tworzenia zależności.
Czy coś brakuje? A może po prostu wstrzykiwanie zależności w C ++ nie jest trywialne? Czy klasa powinna posiadać swoje zależności, czy tylko je obserwować?
źródło
std::unique_ptr
jest to właściwa droga. Możesz użyć,std::move()
aby przenieść własność zasobu z górnego zakresu do klasy.Foobar
to jedyny właściciel? W starym przypadku jest to proste. Ale problem z DI, jak widzę, polega na tym, że oddziela klasę od konstrukcji swoich zależności, a także od własności tych zależności (ponieważ własność jest związana z budową). W środowiskach śmieciowych, takich jak Java, nie stanowi to problemu. W C ++ to jest.Odpowiedzi:
Chciałem napisać to jako komentarz, ale okazało się, że jest za długie.
Czy można stosować
std::unique_ptr<Widget>
albostd::shared_ptr<Widget>
, że to do ciebie, aby zdecydować, i pochodzi z funkcjonalnością.Załóżmy, że masz plik
Utilities::Factory
, który jest odpowiedzialny za tworzenie twoich bloków, takich jakFoobar
. Zgodnie z zasadą DI będziesz potrzebowaćWidget
instancji, aby wstrzyknąć ją za pomocąFoobar
konstruktora, co oznacza, że w jednej zUtilities::Factory
metod, na przykładcreateWidget(const std::vector<std::string>& params)
, utworzysz widget i wstrzykujesz go doFoobar
obiektu.Teraz masz
Utilities::Factory
metodę, która utworzyłaWidget
obiekt. Czy to znaczy, że metoda powinna być odpowiedzialna za jej usunięcie? Oczywiście nie. To jest tylko po to, aby uczynić cię instancją.Wyobraźmy sobie, że tworzysz aplikację, która będzie miała wiele okien. Każde okno jest reprezentowane za pomocą
Foobar
klasy, więc w rzeczywistościFoobar
działa jak kontroler.Kontroler prawdopodobnie skorzysta z niektórych twoich
Widget
i musisz zadać sobie pytanie:Jeśli przejdę do tego konkretnego okna w mojej aplikacji, będę potrzebować tych widżetów. Czy te widżety są współużytkowane przez inne okna aplikacji? Jeśli tak, prawdopodobnie nie powinienem ich od nowa tworzyć, ponieważ zawsze będą wyglądać tak samo, ponieważ są udostępniane.
std::shared_ptr<Widget>
jest droga.Masz także okno aplikacji, w którym jest
Widget
powiązane tylko z tym jednym oknem, co oznacza, że nie będzie wyświetlane nigdzie indziej. Więc jeśli zamkniesz okno, nie będziesz już potrzebowaćWidget
nigdzie aplikacji, a przynajmniej jej instancji.To właśnie po to,
std::unique_ptr<Widget>
aby zdobyć swój tron.Aktualizacja:
Naprawdę nie zgadzam się z @DominicMcDonnell , jeśli chodzi o problem dotyczący życia. Dzwoniąc
std::move
nastd::unique_ptr
całkowicie przenosi własność, więc nawet jeśli utworzyćobject A
w metodzie i przekazać je do innejobject B
postaci zależności,object B
będzie teraz odpowiedzialny za zasobuobject A
i poprawnie go usunąć, gdyobject B
wychodzi z zakresu.źródło
unique_ptr
testowaniem jednostkowym (DI jest reklamowany jako przyjazny dla testowania): chcę testowaćFoobar
, więc tworzęWidget
, przekazuję goFoobar
, ćwiczę,Foobar
a następnie chcę sprawdzićWidget
, ale chyba, żeFoobar
jakoś to ujawni, mogę t, ponieważ twierdził o tymFoobar
.Foobar
jest właścicielem zasobu, nie oznacza, że nikt inny nie powinien go używać. Możesz zaimplementowaćFoobar
metodę taką jakWidget* getWidget() const { return this->_widget.get(); }
, która zwróci ci surowy wskaźnik, z którym możesz pracować. Następnie możesz użyć tej metody jako danych wejściowych do testów jednostkowych, gdy chcesz przetestowaćWidget
klasę.Użyłbym wskaźnika obserwacyjnego w formie referencji. Daje o wiele lepszą składnię, gdy go używasz, i ma tę przewagę semantyczną, że nie implikuje własności, co może zrobić zwykły wskaźnik.
Największym problemem związanym z tym podejściem są okresy życia. Musisz upewnić się, że zależność jest konstruowana przed i zniszczona po twojej klasie zależnej. To nie jest prosty problem. Korzystanie ze wskaźników wspólnych (jako magazynu zależności i we wszystkich klasach od niego zależnych, opcja 2 powyżej) może usunąć ten problem, ale wprowadza również problem zależności cyklicznych, który również nie jest trywialny, a moim zdaniem mniej oczywisty, a zatem trudniejszy do wykryć, zanim spowoduje problemy. Dlatego wolę nie robić tego automatycznie i ręcznie zarządzać czasem życia i zleceniem budowy. Widziałem także systemy, które wykorzystują podejście oparte na lekkim szablonie, które konstruowało listę obiektów w kolejności, w jakiej zostały utworzone, i niszczyły je w odwrotnej kolejności, nie było to niezawodne, ale znacznie upraszczało sprawę.
Aktualizacja
Odpowiedź Davida Packera skłoniła mnie do zastanowienia się nad pytaniem. Oryginalna odpowiedź jest prawdziwa dla współdzielonych zależności z mojego doświadczenia, co jest jedną z zalet wstrzykiwania zależności, można mieć wiele wystąpień przy użyciu jednego wystąpienia zależności. Jeśli jednak twoja klasa potrzebuje własnej instancji określonej zależności,
std::unique_ptr
to poprawna odpowiedź.źródło
Po pierwsze - jest to C ++, a nie Java - i tutaj wiele rzeczy idzie inaczej. Ludzie Java nie mają takich problemów z własnością, ponieważ istnieje automatyczne usuwanie śmieci, które je rozwiązuje.
Po drugie: nie ma ogólnej odpowiedzi na to pytanie - zależy to od wymagań!
Na czym polega problem ze sprzężeniem FooBar i Widżetu? FooBar chce używać Widżetu, a jeśli każda instancja FooBar zawsze będzie miała swoją własną i tę samą Widżet, pozostaw ją połączoną ...
W C ++ możesz nawet robić „dziwne” rzeczy, które po prostu nie istnieją w Javie, np. Mieć variadic konstruktora szablonów (no cóż, istnieje ... notacja w Javie, która może być również używana w konstruktorach, ale to jest po prostu cukier składniowy, aby ukryć tablicę obiektów, która tak naprawdę nie ma nic wspólnego z prawdziwymi szablonami variadic!) - kategoria „Niektóre szalone metaprogramowanie szablonów”:
Lubię to?
Oczywiście, są powody, kiedy chcesz lub potrzeba oddzielenia obu klas - np jeśli trzeba tworzyć widgety naraz daleko przed każda instancja FooBar istnieje, jeśli chcesz lub musisz ponownie użyć widżety, ..., lub po prostu ponieważ w przypadku obecnego problemu jest to bardziej odpowiednie (np. jeśli Widżet jest elementem GUI, a FooBar powinien / nie może / nie może być nim).
Następnie wracamy do drugiego punktu: ogólnej odpowiedzi. Musisz zdecydować, co - w przypadku rzeczywistego problemu - jest bardziej odpowiednim rozwiązaniem. Podoba mi się podejście referencyjne DominicMcDonnell, ale można je zastosować tylko wtedy, gdy FooBar nie przejmie prawa własności (no cóż, faktycznie możesz, ale to oznacza bardzo, bardzo brudny kod ...). Poza tym dołączam do odpowiedzi Davida Packera (tej, która miała być napisana jako komentarz - ale i tak dobra odpowiedź).
źródło
class
es tylko z metodami statycznymi, nawet jeśli jest to tylko fabryka jak w twoim przykładzie. To narusza zasady OO. Zamiast tego używaj przestrzeni nazw i grupuj za pomocą nich swoje funkcje.Brakuje Ci co najmniej dwóch dodatkowych opcji, które są dostępne w C ++:
Jednym z nich jest zastrzyk zależności „statyczny”, w którym zależności są parametrami szablonu. Daje to opcję utrzymywania zależności według wartości, jednocześnie umożliwiając wstrzyknięcie zależności kompilacji w czasie. Kontenery STL używają tego podejścia na przykład do alokatorów oraz funkcji porównania i funkcji skrótu.
Innym jest pobranie wartości polimorficznych obiektów za pomocą głębokiej kopii. Tradycyjnym sposobem na to jest użycie metody wirtualnego klonowania, inną popularną opcją jest wymazywanie typów, aby typy wartości zachowywały się polimorficznie.
To, która opcja jest najbardziej odpowiednia, tak naprawdę zależy od przypadku użycia, trudno jest udzielić ogólnej odpowiedzi. Jeśli potrzebujesz tylko statycznego polimorfizmu, choć powiedziałbym, że szablony są najbardziej dostępne w C ++.
źródło
Zignorowałeś czwartą możliwą odpowiedź, która łączy pierwszy opublikowany kod („dobre stare dni” przechowywania elementu według wartości) z wstrzyknięciem zależności:
Kod klienta może następnie napisać:
Reprezentowanie sprzężenia między obiektami nie powinno odbywać się (wyłącznie) na podanych przez ciebie kryteriach (to znaczy nie opiera się wyłącznie na „co jest lepsze dla bezpiecznego zarządzania czasem życia”).
Możesz także zastosować kryteria koncepcyjne („samochód ma cztery koła” vs. „samochód ma cztery koła, które kierowca musi zabrać ze sobą”).
Możesz mieć kryteria narzucone przez inne interfejsy API (jeśli to, co otrzymujesz z interfejsu API, to niestandardowe opakowanie lub na przykład std :: unique_ptr, opcje w kodzie klienta również są ograniczone).
źródło