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


16

Supponiamo che io abbia una classe Foobarche usa (dipende da) classe Widget. Ai Widgetvecchi tempi, il lupo veniva dichiarato come un campo Foobaro forse come un puntatore intelligente se fosse necessario un comportamento polimorfico e sarebbe stato 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 pronti e pronti. Sfortunatamente, oggi, i bambini Java rideranno di noi quando lo vedranno, e giustamente, mentre si accoppiano Foobare Widgetinsieme. La soluzione è apparentemente semplice: applicare Dependency Injection per eliminare la costruzione di dipendenze Foobar. Ma poi, C ++ ci costringe a pensare alla proprietà delle dipendenze. Mi vengono in mente tre soluzioni:

Puntatore unico

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

Foobarrivendica la proprietà esclusiva di Widgetciò che gli viene passato. Ciò presenta i seguenti vantaggi:

  1. L'impatto sulle prestazioni è trascurabile.
  2. È sicuro, poiché Foobarcontrolla la durata della vita Widget, quindi si assicura che Widgetnon scompaia improvvisamente.
  3. È sicuro che Widgetnon perderà e verrà distrutto correttamente quando non sarà più necessario.

Tuttavia, questo ha un costo:

  1. Pone delle restrizioni su come utilizzare le Widgetistanze, ad es. Non è Widgetspossibile utilizzare alcuna allocazione di stack , non è Widgetpossibile condividere.

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 ad altre lingue raccolte di immondizia e Java. vantaggi:

  1. Più universale, in quanto consente la condivisione delle dipendenze.
  2. Mantiene la sicurezza (punti 2 e 3) della unique_ptrsoluzione.

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 puntatore osservatore

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

Posiziona il puntatore non elaborato all'interno della classe e sposta l'onere della proprietà su qualcun altro. Professionisti:

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

Contro:

  1. Non più al sicuro.
  2. Introduce un'altra entità che è responsabile della proprietà di entrambi Foobare Widget.

Qualche metaprogrammazione di modelli pazzi

L'unico vantaggio che mi viene in mente è che sarei in grado di leggere tutti quei libri per i quali non ho trovato il tempo mentre il mio software sta funzionando;)

Mi rivolgo alla terza soluzione, poiché è la più universale, qualcosa deve essere gestito Foobarscomunque, quindi gestire Widgetsè un semplice cambiamento. Tuttavia, l'utilizzo di puntatori non elaborati mi disturba, d'altra parte la soluzione di puntatore intelligente mi sembra sbagliata, poiché fanno sì che il consumatore di dipendenza limiti il ​​modo in cui tale dipendenza viene creata.

Mi sto perdendo qualcosa? O semplicemente l'iniezione di dipendenza in C ++ non è banale? La classe dovrebbe possedere le sue dipendenze o semplicemente osservarle?


1
Fin dall'inizio, se puoi compilare in C ++ 11 e / o mai, avere un semplice puntatore come un modo per gestire le risorse in una classe non è più il modo raccomandato. Se la classe Foobar è l'unico proprietario della risorsa e la risorsa deve essere liberata, quando Foobar esce dal campo di applicazione, std::unique_ptrè la strada da percorrere. È possibile utilizzare std::move()per trasferire la proprietà di una risorsa dall'ambito superiore alla classe.
Andy,

1
Come faccio a sapere che Foobarè l'unico proprietario? Nel vecchio caso è semplice. Ma il problema con DI, come la vedo io, è che separa la classe dalla costruzione delle sue dipendenze, la separa anche dalla proprietà di quelle dipendenze (poiché la proprietà è legata alla costruzione). In ambienti garbage collection come Java, questo non è un problema. In C ++, questo è.
el.pescado,

1
@ el.pescado C'è anche lo stesso problema in Java, la memoria non è l'unica risorsa. Ci sono anche file e connessioni per esempio. Il modo abituale di risolverlo in Java è disporre di un contenitore che gestisca il ciclo di vita di tutti i suoi oggetti contenuti. Pertanto non devi preoccuparti di ciò negli oggetti contenuti in un contenitore DI. Questo potrebbe funzionare anche per le applicazioni c ++ se esiste un modo per il contenitore DI di sapere quando passare a quale fase del ciclo di vita.
SpaceTrucker,

3
Certo, potresti farlo alla vecchia maniera senza tutta la complessità e avere il codice che funziona ed è molto più facile da mantenere. (Nota che i ragazzi Java possono ridere, ma sono sempre
impegnati nel

3
Inoltre avere altre persone che ridono di te non è un motivo per essere come loro. Ascolta i loro argomenti (diversi da "perché ci è stato detto di") e implementa DI se porta benefici tangibili al tuo progetto. In questo caso, pensa al modello come una regola molto generale, che devi applicare in un modo specifico per il progetto: tutti e tre gli approcci (e tutti gli altri potenziali approcci che non hai ancora pensato) sono validi apportano un vantaggio complessivo (ovvero hanno più vantaggi che svantaggi).

Risposte:


2

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

Come faccio a sapere che Foobarè l'unico proprietario? Nel vecchio caso è semplice. Ma il problema con DI, come la vedo io, è che separa la classe dalla costruzione delle sue dipendenze, la separa anche dalla proprietà di quelle dipendenze (poiché la proprietà è legata alla costruzione). In ambienti garbage collection come Java, questo non è un problema. In C ++, questo è.

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 Widgetdell'istanza per iniettarla usandoFoobar il costruttore, cioè all'interno di uno dei Utilities::Factorymetodi, per esempio createWidget(const std::vector<std::string>& params), crei il Widget e lo iniettiamo Foobarnell'oggetto.

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


Immaginiamo che stai sviluppando un'applicazione, che avrà più finestre. Ogni finestra è rappresentata usando la Foobarclasse, quindi in effetti si Foobarcomporta come un controller.

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

Se vado su questa finestra specifica nella mia applicazione, avrò bisogno di questi widget. Questi widget sono condivisi tra le altre finestre dell'applicazione? In tal caso, probabilmente non dovrei crearli di nuovo da capo, perché avranno sempre lo stesso aspetto, perché sono condivisi.

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

Hai anche una finestra dell'applicazione, in cui esiste Widgetuna finestra specificamente legata a questa finestra, il che significa che non verrà visualizzata in nessun altro luogo. Quindi, se chiudi la finestra, non ti serveWidget nessuna parte della tua applicazione, o almeno della sua istanza.

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


Aggiornare:

Non sono davvero d'accordo con @DominicMcDonnell sul problema della vita. Chiamata std::movesul std::unique_ptrtrasferisce completamente alla proprietà, quindi, anche se si crea un object Ain un metodo e passare ad un altro object Bcome una dipendenza, la object Bsarà ora responsabile della risorsa object Ae sarà correttamente eliminarlo, quando object Bva fuori portata.


L'idea generale di proprietà e suggerimenti intelligenti mi è chiara. Semplicemente non mi sembra di giocare bene con l'idea di DI. Injection Dependency, per me, si tratta di disaccoppiare la classe dalle sue dipendenze, ma in questo caso la classe influenza ancora il modo in cui vengono create le sue dipendenze.
el.pescado,

Ad esempio, il problema che ho con unique_ptrè il test unitario (DI è pubblicizzato come test-friendly): voglio testare Foobar, quindi lo creo Widget, lo passo Foobar, esercizio Foobare quindi voglio ispezionare Widget, ma a meno che in Foobarqualche modo non lo esponga, posso da quando è stato rivendicato da Foobar.
el.pescado,

@ el.pescado Stai mescolando due cose insieme. Stai mescolando accessibilità e proprietà. Solo perché Foobarpossiede la risorsa non significa che nessun altro dovrebbe usarla. Potresti implementare un Foobarmetodo come Widget* getWidget() const { return this->_widget.get(); }, che ti restituirà il puntatore non elaborato con cui puoi lavorare. È quindi possibile utilizzare questo metodo come input per i test unitari, quando si desidera testare la Widgetclasse.
Andy,

Buon punto, ha senso.
el.pescado,

1
Se hai convertito shared_ptr in unique_ptr, come vorresti garantire unique_ness ???
Aconcagua,

2

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

Il problema più grande con questo approccio è la vita. Devi assicurarti che la dipendenza sia stata costruita prima e distrutta dopo la tua classe dipendente. Non è un problema semplice. L'uso di puntatori condivisi (come archivio delle dipendenze e in tutte le classi che dipendono da esso, opzione 2 sopra) può rimuovere questo problema, ma introduce anche il problema delle dipendenze circolari che è anche non banale, e secondo me meno ovvio e quindi più difficile da rilevare prima che causi problemi. Ecco perché preferisco non farlo automaticamente e gestire manualmente la durata e l'ordine di costruzione. Ho anche visto sistemi che usano 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 opposto, non era infallibile, ma ha reso le cose molto più semplici.

Aggiornare

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 dell'iniezione di dipendenze, puoi avere più istanze usando l'istanza di una dipendenza. Se tuttavia la tua classe deve avere la propria istanza di una particolare dipendenza, allora std::unique_ptrè la risposta corretta.


1

Prima di tutto - questo è C ++, non Java - e qui molte cose vanno diversamente. Le persone Java non hanno questi 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 di FooBar avrà sempre il proprio e lo stesso Widget, lascialo accoppiato ...

In C ++, puoi persino fare cose "strane" che semplicemente non esistono in Java, ad esempio hanno un costruttore di template variadico (beh, esiste una ... notazione in Java, che può essere usata anche nei costruttori, ma questo è solo zucchero sintattico per nascondere un array di oggetti, che in realtà non ha nulla a che fare con modelli variadici reali!) - categoria 'Alcuni pazzi metaprogrammi di 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);

Mi piace?

Naturalmente, ci sono ragioni per cui si desidera o è necessario separare entrambe le classi, ad esempio se è necessario creare i widget in un momento molto precedente all'esistenza di qualsiasi istanza di FooBar, se si desidera o è necessario riutilizzare i widget, ... o semplicemente perché per il problema attuale è più appropriato (ad es. se Widget è un elemento GUI e FooBar deve / può / non deve esserlo).

Quindi, torniamo al secondo punto: nessuna risposta generale. È necessario decidere quale, per il problema reale , è la soluzione più appropriata. Mi piace l'approccio di riferimento di DominicMcDonnell, ma può essere applicato solo se FooBar non deve assumere la proprietà (beh, in realtà, potresti, ma questo implica un codice molto, molto sporco ...). A parte questo, mi unisco alla risposta di David Packer (quella pensata per essere scritta come commento - ma comunque una buona risposta).


1
In C ++, non usare classes con nient'altro che metodi statici, anche se è solo una fabbrica come nel tuo esempio. Ciò viola i principi OO. Usa invece gli spazi dei nomi e raggruppa le tue funzioni utilizzandole.
Andy,

@DavidPacker Hmm, certo, hai ragione - ho silenziosamente supposto che la classe avesse degli interni privati, ma questo non è né visibile né menzionato altrove ...
Aconcagua

1

Ti mancano almeno altre due opzioni disponibili in C ++:

Uno è usare 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 delle dipendenze durante la compilazione. I contenitori STL utilizzano questo approccio per allocatori e funzioni di confronto e hash, ad esempio.

Un altro è prendere oggetti polimorfici per valore usando una copia profonda. Il modo tradizionale per farlo è utilizzare un metodo di clonazione virtuale, un'altra opzione che è diventata popolare è quella di utilizzare la cancellazione del tipo per creare tipi di valore che si comportano polimorficamente.

L'opzione più appropriata dipende dal tuo caso d'uso, è difficile dare una risposta generale. Se hai solo bisogno di un polimorfismo statico, direi che i modelli sono la strada da percorrere più in C ++.


0

Hai ignorato una quarta possibile risposta, che combina il primo codice che hai pubblicato (i "bei giorni" della memorizzazione di un membro in base al valore) con l'iniezione di dipendenza:

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

Il codice client può quindi scrivere:

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

La rappresentazione dell'accoppiamento tra oggetti non deve essere eseguita (puramente) in base ai criteri elencati (ovvero, non basati esclusivamente su "che è meglio per una gestione sicura della vita").

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

Puoi avere dei 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 tuo codice client sono limitate).


1
Ciò preclude il comportamento polimorfico, che limita fortemente il valore aggiunto.
el.pescado,

Vero. Ho solo pensato di menzionarlo poiché è la forma che finisco per usare di più (uso solo l'ereditarietà nella mia codifica per il comportamento polimorfico - non il riutilizzo del codice, quindi non ho molti casi in cui ho bisogno di un comportamento polimorfico) ..
utnapistim
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.