C ++: la classe dovrebbe possedere o osservare le sue dipendenze?

15

Dire che ho una classe Foobar che utilizza (dipende dalla) classe Widget . Nei buoni vecchi giorni, Widget wolud essere dichiarato come campo in Foobar , o forse come puntatore intelligente se fosse necessario un comportamento polimorfico, e sarebbe inizializzato nel costruttore:

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)) {}
    (…)
};

E saremmo tutti pronti. Sfortunatamente, oggi, i bambini di Java ridono di noi una volta che li vedono, e giustamente, poiché accoppia Foobar e Widget insieme. La soluzione è apparentemente semplice: applica Dependency Injection per prendere la costruzione delle dipendenze dalla classe Foobar . Ma poi, il C ++ ci costringe a pensare alla proprietà delle dipendenze. Vengono in mente tre soluzioni:

Puntatore univoco

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

Foobar rivendica la proprietà esclusiva di Widget che gli viene passata. Questo ha i seguenti vantaggi:

  1. L'impatto sulle prestazioni è trascurabile.
  2. È sicuro, dato che Foobar controlla la durata del suo Widget , quindi si assicura che Widget non scompaia improvvisamente.
  3. È sicuro che Widget non perderà e verrà distrutto correttamente quando non sarà più necessario.

Tuttavia, ciò ha un costo:

  1. pone restrizioni su come utilizzare le istanze Widget , ad es. non è possibile utilizzare Widgets allocata allo stack, non è possibile condividere Widget .

Puntatore condiviso

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

Questo è probabilmente l'equivalente più vicino di Java e di altri linguaggi raccolti da garbage collection. Vantaggi:

  1. Più universale, in quanto consente di condividere le dipendenze.
  2. Mantiene la sicurezza (punti 2 e 3) della soluzione unique_ptr .

Svantaggi:

  1. Spreca risorse quando non è coinvolta alcuna condivisione.
  2. Richiede ancora l'allocazione dell'heap e non consente gli oggetti allocati nello stack.

Semplice ol 'puntatore osservatore

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

Posiziona il puntatore raw all'interno della classe e sposta l'onere della proprietà su qualcun altro. Pro:

  1. Il più semplice possibile.
  2. Universale, accetta solo qualsiasi Widget .

Contro:

  1. Non è più sicuro.
  2. Presenta un'altra entità responsabile della proprietà di Foobar e Widget .

Alcuni metaprogrammi del modello pazzo

L'unico vantaggio che posso pensare è che sarei in grado di leggere tutti quei libri per i quali non ho trovato il tempo mentre il mio software è in fase di produzione;)

Mi chiamo verso la terza soluzione, poiché è la più universale, qualcosa deve comunque gestire Foobars , quindi gestire Widgets è una semplice modifica. Tuttavia, l'uso di puntatori grezzi mi infastidisce, d'altra parte la soluzione di puntatore intelligente mi sembra sbagliata, poiché fanno in modo che i consumatori di dipendenza limitino il modo in cui viene creata tale dipendenza.

Mi manca qualcosa? O è solo la dipendenza dall'iniezione in C ++ non banale? La classe dovrebbe possedere le sue dipendenze o semplicemente osservarle?

    
posta el.pescado 07.09.2015 - 22:41
fonte

5 risposte

2

Volevo scrivere questo come commento, ma si è rivelato troppo lungo.

How I know that Foobar is the only owner? In the old case it's simple. But the problem with DI, as I see it, is as it decouples class from construction of its dependencies, it also decouples it from ownership of those dependencies (as ownership is tied to construction). In garbage collected environments such as Java, that's not a problem. In C++, this is.

Se dovresti usare std::unique_ptr<Widget> o std::shared_ptr<Widget> , dipende da te decidere e proviene dalla tua funzionalità.

Supponiamo che tu abbia un Utilities::Factory , che è responsabile della creazione dei tuoi blocchi, come Foobar . Seguendo il principio DI, avrai bisogno dell'istanza Widget , per iniettarla usando il costruttore di Foobar , cioè all'interno di uno dei metodi di Utilities::Factory , ad esempio createWidget(const std::vector<std::string>& params) , crei il Widget e lo inietti nell'oggetto Foobar .

Ora hai un metodo Utilities::Factory , che ha creato l'oggetto Widget . Significa che il metodo dovrebbe essere responsabile della sua cancellazione? Ovviamente no. È lì solo per farti l'istanza.

Immaginiamo, stai sviluppando un'applicazione, che avrà più finestre. Ogni finestra è rappresentata utilizzando la classe Foobar , quindi in effetti Foobar si comporta come un controller.

Il controller probabilmente utilizzerà alcuni dei tuoi Widget s e devi chiedertelo:

Se vado su questa finestra specifica nella mia applicazione, avrò bisogno di questi Widget. Questi widget sono condivisi tra le altre finestre dell'applicazione? Se è così, non dovrei probabilmente crearli di nuovo, perché saranno sempre uguali, perché sono condivisi.

std::shared_ptr<Widget> è la strada da percorrere.

Hai anche una finestra dell'applicazione, dove c'è un Widget specificamente legato a questa sola finestra, il che significa che non verrà visualizzato da nessun'altra parte. Quindi se chiudi la finestra, non hai più bisogno di Widget da nessuna parte nell'applicazione, o almeno della sua istanza.

Ecco dove std::unique_ptr<Widget> viene a rivendicare il suo trono.

Aggiornamento:

In realtà non sono d'accordo con @DominicMcDonnell , sul problema della durata. Chiamando std::move su std::unique_ptr trasferisce completamente la proprietà, quindi anche se crei un object A in un metodo e lo passi a un altro object B come dipendenza, il object B sarà ora responsabile della risorsa object A e lo eliminerà correttamente, quando object B esce dall'ambito.

    
risposta data 08.09.2015 - 08:46
fonte
2

Vorrei usare un puntatore osservatore sotto forma di riferimento. Fornisce una sintassi molto migliore quando lo usi e ha il vantaggio semantico che non implica la proprietà, che può essere un puntatore semplice.

Il più grande problema con questo approccio è la vita. Devi assicurarti che la dipendenza sia costruita prima e distrutta dopo la tua classe dipendente. Non è un problema semplice. L'utilizzo di puntatori condivisi (come l'archiviazione delle dipendenze e in tutte le classi che dipendono da esso, l'opzione 2 sopra) può rimuovere questo problema, ma introduce anche il problema delle dipendenze circolari, anch'esso non banale, e secondo me meno ovvio e quindi più difficile rilevare prima che causi problemi. È per questo che preferisco non farlo automaticamente e gestire manualmente le vite e l'ordine di costruzione. Ho anche visto sistemi che utilizzano un approccio di template leggero che ha costruito un elenco di oggetti nell'ordine in cui sono stati creati e li ha distrutti in ordine inverso, non è infallibile, ma ha reso le cose molto più semplici.

Aggiornamento

La risposta di David Packer mi ha fatto riflettere un po 'di più sulla domanda. La risposta originale è vera per le dipendenze condivise nella mia esperienza, che è uno dei vantaggi di Injection dipendenza, è possibile avere più istanze che utilizzano l'istanza di una dipendenza. Se tuttavia la tua classe ha bisogno di avere la propria istanza di una dipendenza particolare, std::unique_ptr è la risposta corretta.

    
risposta data 08.09.2015 - 01:14
fonte
1

Prima di tutto - questo è C ++, not Java - e qui molte cose vanno diversamente. Gli utenti Java non hanno problemi di proprietà in quanto esiste una garbage collection automatica che risolve quelli per loro.

Secondo: non esiste una risposta generale a questa domanda - dipende da quali sono i requisiti!

Qual è il problema dell'accoppiamento di FooBar e Widget? FooBar vuole usare un Widget, e se ogni istanza FooBar avrà sempre il suo e lo stesso Widget comunque, lascialo accoppiato ...

In C ++, puoi persino fare cose "strane" che semplicemente non esistono in Java, e. g. avere un costruttore di template variadico (beh, esiste una notazione in Java, che può essere usata anche nei costruttori, ma è solo uno zucchero sintattico per nascondere un array di oggetti, che in realtà non ha nulla a che fare con i modelli variadici reali! ) - categoria 'Some crazy metaprogramming template':

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

Ti piace?

Ovviamente, ci sono sono ragioni quando vuoi o devi separare entrambe le classi - e. g. se hai bisogno di creare i widget alla volta molto prima che esista un'istanza FooBar, se vuoi o devi riutilizzare i widget, ..., o semplicemente perché per il problema corrente è più appropriato (es. se il Widget è un elemento della GUI e FooBar deve / can / non deve essere uno).

Quindi, torniamo al secondo punto: nessuna risposta generale. Devi decidere, cosa - per il problema effettivo - è la soluzione più appropriata. Mi piace l'approccio di riferimento di DominicMcDonnell, ma può essere applicato solo se la proprietà non è presa da FooBar (beh, in realtà, si potrebbe, ma questo implica codice molto, molto sporco ...). A parte questo, mi unisco alla risposta di David Packer (quella che doveva essere scritta come commento - ma una buona risposta, comunque).

    
risposta data 10.09.2015 - 03:09
fonte
1

Mancano almeno altre due opzioni che sono disponibili in C ++:

Uno consiste nell'utilizzare l'iniezione di dipendenza "statica" in cui le dipendenze sono parametri del modello. Questo ti dà la possibilità di mantenere le tue dipendenze in base al valore pur consentendo l'iniezione della dipendenza in tempo compilato. I contenitori STL utilizzano questo approccio per gli allocatori e le funzioni di confronto e hash, ad esempio.

Un altro è prendere oggetti polimorfici in base al valore usando una copia profonda. Il modo tradizionale per farlo è utilizzare un metodo clone virtuale, un'altra opzione che è diventata popolare è usare la cancellazione dei tipi per rendere i tipi di valore che si comportano in modo polimorfico.

Quale opzione è più appropriata dipende davvero dal tuo caso d'uso, è difficile dare una risposta generale. Se hai solo bisogno di polimorfismo statico, direi che i modelli sono la via più C ++.

    
risposta data 10.09.2015 - 23:32
fonte
0

Hai ignorato una quarta risposta possibile, che combina il primo codice che hai postato (i "buoni vecchi" giorni di archiviazione di un membro per valore) con l'iniezione di dipendenza:

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

Il codice cliente può quindi scrivere:

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

Rappresentare l'accoppiamento tra gli oggetti non dovrebbe essere fatto (puramente) sui criteri che hai elencato (cioè, non basato esclusivamente su "che è meglio per una gestione sicura della vita").

Puoi anche utilizzare criteri concettuali ("un'auto ha quattro ruote" contro "un'auto utilizza quattro ruote che il conducente deve portare con sé").

Puoi avere criteri imposti da altre API (se ciò che ottieni da un'API è un wrapper personalizzato o uno std :: unique_ptr ad esempio, anche le opzioni nel codice client sono limitate).

    
risposta data 08.09.2015 - 11:00
fonte

Leggi altre domande sui tag