C ++: Czy klasa powinna posiadać lub obserwować swoje zależności?

16

Powiedzmy, że mam klasę, Foobarktóra używa (zależy od) klasy Widget. W Widgetdawnych 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 Foobari Widgetrazem. Rozwiązanie jest z pozoru proste: zastosuj Dependency Injection, aby usunąć konstrukcję zależności z Foobarklasy. 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) {}
    (…)
}

Foobartwierdzi, że wyłączna własność Widgettej własności została mu przekazana. Ma to następujące zalety:

  1. Wpływ na wydajność jest znikomy.
  2. Jest bezpieczny, ponieważ Foobarkontroluje jego żywotność Widget, dzięki czemu zapewnia, że Widgetnie zniknie nagle.
  3. Jest bezpieczny, że Widgetnie wycieknie i zostanie odpowiednio zniszczony, gdy nie będzie już potrzebny.

Jest to jednak kosztem:

  1. Nakłada ograniczenia na sposób Widgetużycia instancji, np. Nie Widgetsmożna użyć alokacji stosu , nie Widgetmoż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:

  1. Bardziej uniwersalny, ponieważ umożliwia współdzielenie zależności.
  2. Utrzymuje bezpieczeństwo (punkty 2 i 3) unique_ptrroztworu.

Niedogodności:

  1. Marnuje zasoby, gdy nie dotyczy udostępniania.
  2. 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:

  1. Tak proste, jak to tylko możliwe.
  2. Uniwersalny, akceptuje tylko każdy Widget.

Cons:

  1. Nie jest już bezpieczny.
  2. Wprowadza inny podmiot, który jest odpowiedzialny za własność zarówno Foobari Widget.

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 Widgetsjest 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ć?

el.pescado
źródło
1
Od samego początku, jeśli możesz kompilować w C ++ 11 i / lub nigdy, posiadanie zwykłego wskaźnika jako sposobu obsługi zasobów w klasie nie jest już zalecanym sposobem. Jeśli klasa Foobar jest jedynym właścicielem zasobu, a zasób powinien zostać uwolniony, gdy Foobar wykracza poza zakres, std::unique_ptrjest to właściwa droga. Możesz użyć, std::move()aby przenieść własność zasobu z górnego zakresu do klasy.
Andy,
1
Skąd mam wiedzieć, że Foobarto 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.
el.pescado
1
@ el.pescado Istnieje również ten sam problem w Javie, pamięć nie jest jedynym zasobem. Są też na przykład pliki i połączenia. Zwykłym sposobem rozwiązania tego w Javie jest posiadanie kontenera, który zarządza cyklem życia wszystkich zawartych w nim obiektów. Dlatego nie musisz się o to martwić w obiektach zawartych w kontenerze DI. Może to również działać w aplikacjach c ++, jeśli istnieje sposób, aby kontener DI wiedział, kiedy przełączyć się na którą fazę cyklu życia.
SpaceTrucker
3
Oczywiście możesz to zrobić w staromodny sposób, bez całej złożoności i mieć kod, który działa i jest łatwiejszy w utrzymaniu. (Uwaga, że chłopcy Java może się śmiać, ale są one zawsze chodzi o refactoring, więc nie jest dokładnie odsprzęganie pomagając im dużo)
gbjbaanb
3
Plus, że inni śmieją się z ciebie, nie jest powodem do bycia takim jak oni. Wysłuchaj ich argumentów (innych niż „bo tak nam powiedziano”) i zastosuj DI, jeśli przyniesie ono wymierne korzyści Twojemu projektowi. W takim przypadku pomyśl o schemacie jako bardzo ogólnej zasadzie, którą musisz zastosować w sposób specyficzny dla projektu - wszystkie trzy podejścia (i wszystkie inne potencjalne podejścia, o których jeszcze nie pomyślałeś) są prawidłowe, biorąc pod uwagę przynoszą ogólne korzyści (tj. mają więcej zalet niż wad).

Odpowiedzi:

2

Chciałem napisać to jako komentarz, ale okazało się, że jest za długie.

Skąd mam wiedzieć, że Foobarto 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.

Czy można stosować std::unique_ptr<Widget>albo std::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 jak Foobar. Zgodnie z zasadą DI będziesz potrzebować Widgetinstancji, aby wstrzyknąć ją za pomocą Foobarkonstruktora, co oznacza, że ​​w jednej z Utilities::Factorymetod, na przykład createWidget(const std::vector<std::string>& params), utworzysz widget i wstrzykujesz go do Foobarobiektu.

Teraz masz Utilities::Factorymetodę, która utworzyła Widgetobiekt. 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ą Foobarklasy, więc w rzeczywistości Foobardziała jak kontroler.

Kontroler prawdopodobnie skorzysta z niektórych twoich Widgeti 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 Widgetpowią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ć Widgetnigdzie 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::movena std::unique_ptrcałkowicie przenosi własność, więc nawet jeśli utworzyć object Aw metodzie i przekazać je do innej object Bpostaci zależności, object Bbędzie teraz odpowiedzialny za zasobu object Ai poprawnie go usunąć, gdy object Bwychodzi z zakresu.

Andy
źródło
Ogólna idea własności i inteligentnych wskaźników jest dla mnie jasna. Po prostu wydaje mi się, że nie gra się dobrze z ideą DI. Wstrzykiwanie zależności polega na oddzieleniu klasy od jej zależności, ale w tym przypadku klasa nadal wpływa na sposób tworzenia jej zależności.
el.pescado
Na przykład mam problem z unique_ptrtestowaniem jednostkowym (DI jest reklamowany jako przyjazny dla testowania): chcę testować Foobar, więc tworzę Widget, przekazuję go Foobar, ćwiczę, Foobara następnie chcę sprawdzić Widget, ale chyba, że Foobarjakoś to ujawni, mogę t, ponieważ twierdził o tym Foobar.
el.pescado
@ el.pescado Mieszacie dwie rzeczy razem. Mieszacie dostępność i własność. To, że Foobarjest właścicielem zasobu, nie oznacza, że ​​nikt inny nie powinien go używać. Możesz zaimplementować Foobarmetodę taką jak Widget* 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ć Widgetklasę.
Andy,
Dobra uwaga, to ma sens.
el.pescado
1
Jeśli przekonwertowałeś plik shared_ptr na unikalny_ptr, jak chcesz zagwarantować unikalny_ ??
Aconcagua,
2

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_ptrto poprawna odpowiedź.

Dominique McDonnell
źródło
1

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”:

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);

Lubię to?

Oczywiście, 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ź).

Aconcagua
źródło
1
W C ++ nie używaj classes 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.
Andy,
@DavidPacker Hmm, jasne, masz rację - po cichu przyjąłem, że klasa ma jakieś prywatne elementy wewnętrzne, ale nie jest to ani widoczne, ani wspomniane gdzie indziej ...
Aconcagua
1

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 ++.

mattnewport
źródło
0

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:

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

Kod klienta może następnie napisać:

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

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).

utnapistim
źródło
1
Wyklucza to zachowanie polimorficzne, które poważnie ogranicza wartość dodaną.
el.pescado,
Prawdziwe. Właśnie pomyślałem o tym, ponieważ jest to forma, którą najczęściej używam (używam tylko dziedziczenia w kodowaniu dla zachowania polimorficznego - nie ponownego użycia kodu, więc nie mam wielu przypadków wymagających zachowania polimorficznego) ..
utnapistim