2010-03-08 11 views
27

Eventuali duplicati:
How much work should be done in a constructor?Un costruttore C++ dovrebbe davvero funzionare?

sto strugging con qualche consiglio che ho in parte posteriore della mia mente, ma per il quale non riesco a ricordare il ragionamento.

Mi sembra di ricordare a un certo punto di leggere qualche consiglio (non ricordo la fonte) che i costruttori del C++ non dovrebbero fare il vero lavoro. Piuttosto, dovrebbero inizializzare solo le variabili. Il consiglio proseguì spiegando che il lavoro reale doveva essere fatto in una sorta di metodo init(), da chiamare separatamente dopo la creazione dell'istanza.

La situazione è che ho una classe che rappresenta un dispositivo hardware. È logico per me che il costruttore chiami le routine che interrogano il dispositivo per creare le variabili di istanza che descrivono il dispositivo. In altre parole, una volta che la nuova istanza l'oggetto, lo sviluppatore riceve un oggetto che è pronto per essere utilizzato, nessuna chiamata separata a object-> init() richiesta.

C'è una buona ragione per cui i costruttori non dovrebbero fare un vero lavoro? Ovviamente potrebbe rallentare il tempo di allocazione, ma non sarebbe diverso se si inviti un metodo separato subito dopo l'allocazione.

Solo cercando di capire quali trucchi non ho attualmente in considerazione che potrebbero aver portato a tale consiglio.

+3

Possibile candidato per la fusione. – dmckee

risposta

26

Ricordo che Scott Meyers in C++ più efficace raccomanda di non avere un costruttore di default superfluo. In quell'articolo, ha anche toccato l'uso di metodi come Init() per "creare" gli oggetti. Fondamentalmente, hai introdotto un ulteriore passaggio che pone la responsabilità sul cliente della classe.Inoltre, se si desidera creare una matrice di detti oggetti, ciascuno di essi dovrebbe chiamare manualmente Init(). Puoi avere una funzione Init che il costruttore può chiamare all'interno per mantenere il codice in ordine o per chiamare l'oggetto se si implementa un Reset(), ma dalle esperienze è meglio cancellare un oggetto e ricrearlo invece di provare a reimpostare i suoi valori di default, a meno che gli oggetti non vengano creati e distrutti molte volte in tempo reale (ad esempio, effetti particellari).

Inoltre, si noti che i costruttori possono eseguire elenchi di inizializzazione che le normali funzioni non potrebbero eseguire.

Uno dei motivi per cui si può essere cauti nell'usare i costruttori a fare un'allocazione pesante delle risorse è perché può essere difficile catturare le eccezioni nei costruttori. Tuttavia, ci sono modi per aggirarlo. Altrimenti, penso che i costruttori abbiano lo scopo di fare ciò che dovrebbero fare - preparare un oggetto per il suo stato iniziale di esecuzione (importante per la creazione dell'oggetto è l'allocazione delle risorse).

+24

Quello sarebbe Scott Meyers - Sid Meier è il tipo di Civilization :) –

+0

Ops. Ho fatto la modifica. – Extrakun

9

Dipende da cosa intendi per vero lavoro. Il costruttore deve mettere l'oggetto in uno stato utilizzabile, anche se tale stato è una bandiera che significa che non è ancora stato inizializzato :-)

L'unica spiegazione razionale che io abbia mai incontrato per non aver fatto il lavoro vero e proprio sarebbe il il fatto che l'unico modo in cui un costruttore può fallire è con un'eccezione (e il distruttore non verrà chiamato in quel caso). Non c'è alcuna possibilità di restituire un bel codice di errore.

La domanda che bisogna porsi è:

è l'oggetto utilizzabile senza chiamare il metodo init?

Se la risposta è "No", farei tutto questo lavoro nel costruttore. Altrimenti dovrai cogliere la situazione quando un utente ha istanziato ma non ancora inizializzato e restituire una sorta di errore.

Naturalmente, se si può ri-inizializzare il dispositivo, si dovrebbe fornire una sorta di init metodo, ma, in quel caso, avrei ancora chiamata che il metodo dal costruttore se la condizione di cui sopra è soddisfatta.

18

L'unica ragione per non "lavorare" nel costruttore è che se un'eccezione viene lanciata da lì, il distruttore di classe non verrà chiamato. Ma se usi i principi RAII e non ti affidi al distruttore per eseguire lavori di pulizia, allora ritengo sia meglio non introdurre un metodo che non è necessario.

+8

Avrei aggiunto che anche se un costruttore lancia ogni variabile membro completamente costruita avrà il suo derstructor chiamato (quindi se sei nel blocco di codice del costruttore tutti i membri verranno destrutturati correttamente). –

+0

Come si effettua la pulizia nel costruttore una volta generata un'eccezione? (Supponiamo che tu abbia assegnato un 'FILE *' per esempio) ** non fare affidamento sul tuo distruttore ** ma tutto quello che puoi fare è lasciare che lo stack si srotoli ... –

+0

Uno dei membri della tua classe sarebbe un oggetto di tipo RAII che chiuderà il file * nel suo distruttore. Qualcosa come la classe FileCloser da questo: http://tomdalling.com/blog/software-design/resource-acquisition-is-initialisation-raii-explained/ – Warpin

3

L'unica vera ragione è Testability. Se i costruttori sono pieni di "lavoro reale", questo di solito significa che gli oggetti possono essere istanziati solo all'interno di un'applicazione in esecuzione completamente inizializzata. È un segno che l'oggetto/classe ha bisogno di ulteriori scomposizioni.

+0

L'argomento di testabilità è buono. Ma probabilmente può essere gestito da oggetti finti nella maggior parte dei casi – neuro

+1

Se il costruttore crea i propri oggetti, non è possibile configurare l'oggetto con oggetti mock a meno che non si scriva un nuovo costruttore. Separando la responsabilità di rendere un oggetto complesso dall'impostazione delle sue variabili membro si ottiene la cucitura richiesta. – mabraham

6

Oltre agli altri suggerimenti relativi alla gestione delle eccezioni, una cosa da considerare quando ci si connette a un dispositivo hardware è come la classe gestirà la situazione in cui un dispositivo non è presente o la comunicazione non riesce.

Nella situazione in cui non è possibile comunicare con il dispositivo, potrebbe essere necessario fornire alcuni metodi sulla classe per eseguire comunque l'inizializzazione successiva. In tal caso, può essere più sensato istanziare l'oggetto e quindi eseguire una chiamata di inizializzazione. Se l'inizializzazione fallisce, puoi semplicemente mantenere l'oggetto e provare a inizializzare nuovamente la comunicazione in un secondo momento. Oppure potrebbe essere necessario gestire la situazione in cui la comunicazione viene persa dopo l'inizializzazione. In entrambi i casi, probabilmente vorrai pensare a come progetterai la classe per gestire i problemi di comunicazione in generale e ciò potrebbe aiutarti a decidere cosa vuoi fare nel costruttore rispetto a un metodo di inizializzazione.

Quando ho implementato classi che comunicano con l'hardware esterno, ho trovato più semplice creare un'istanza di un oggetto "disconnesso" e fornire metodi per la connessione e l'impostazione dello stato iniziale. Ciò in genere fornisce maggiore flessibilità di connessione/disconnessione/riconnessione con il dispositivo.

+0

Sì! La maggior parte delle persone non sembra ottenerlo: mettere il lavoro in un costruttore lega implicitamente il successo alla durata dell'oggetto, il che non funziona bene per oggetti disconnessi (come un client TCP "persistente"). – Tom

+0

Sono d'accordo. Io uso un'interfaccia/paradigma open/close/isOpen per contrassegnare/gestire questo tipo di oggetti – neuro

3

Quando si utilizza un costruttore e un metodo Init() si ha una fonte di errore. Nella mia esperienza, incontrerai situazioni in cui qualcuno dimentica di chiamarlo e potresti avere un insetto sottile nelle tue mani. Direi che non dovresti fare molto lavoro nel tuo costruttore, ma se è necessario un metodo init, allora hai uno scenario di costruzione non banale, ed è ora di guardare i pattern creativi. Una funzione di costruttore o una fabbrica è consigliabile per dare un'occhiata. Con un costruttore privato assicurandoti che nessuno, tranne la tua fabbrica o la tua funzione builder, costruisca effettivamente gli oggetti, puoi essere certo che sia sempre costruito correttamente.

Se il progetto consente errori nell'implementazione, qualcuno farà quegli errori. Il mio amico Murphy me l'ha detto;)

Nel mio campo lavoriamo con un sacco di simili situazioni legate all'hardware. Le fabbriche ci forniscono testabilità, sicurezza e migliori modi di fallire nella costruzione.

2

Vale la pena prendere in considerazione problemi di durata e collegamento/riconnessione, come fa notare Neal S.

Se non si riesce a connettersi a un dispositivo all'altra estremità di un collegamento, è spesso il caso che il "dispositivo" alla propria estremità sia utilizzabile e lo sarà in un secondo momento se l'altra estremità agisce insieme. Esempi di connessioni di rete, ecc.

D'altra parte se si tenta di accedere ad un dispositivo hardware locale che non esiste e non esisterà mai nell'ambito del programma (ad esempio una scheda grafica non presente), penso che questo sia un caso in cui vuoi sapere questo nel costruttore, in modo che il costruttore possa lanciare e l'oggetto non possa esistere. Se non lo fai, potresti finire con un oggetto che non è valido e lo sarà sempre. Lanciare nel costruttore significa che l'oggetto non esisterà, e quindi le funzioni non possono essere chiamate su quell'oggetto. Ovviamente è necessario essere consapevoli dei problemi di pulizia se si lancia in un costruttore, ma se non lo si fa in casi come questo, in genere si finisce con controlli di convalida in tutte le funzioni che possono essere chiamate.

Quindi penso che dovresti fare abbastanza nel costruttore per assicurarti di avere un oggetto valido, utilizzabile, creato.

1

Mi piacerebbe aggiungere la mia esperienza lì.

Non dirò molto del dibattito tradizionale Costruttore/Iniz ... ad esempio le linee guida Google sconsigliano qualsiasi cosa sia nel Costruttore ma questo perché sconsigliano le Eccezioni e le 2 lavorano insieme.

Posso parlare di una classe Connection che uso però.

Quando viene creata la classe Connection, verrà effettuato il tentativo per connettersi (almeno, se non predefinito, costruito). Se lo Connection fallisce ... l'oggetto è ancora costruito e non lo sai.

Quando si tenta di utilizzare la classe Connection siete quindi in uno dei 3 casi:

  • nessun parametro è stato mai precisati> eccezione o il codice di errore
  • l'oggetto sia effettivamente collegato> bene
  • l'oggetto non è collegato, tenterà di collegarsi> questo riesce, bene, questo non riesce, si ottiene un'eccezione o un codice di errore

Penso che sia molto utile per Hanno entrambi. Tuttavia, significa che in ogni singolo metodo che utilizza effettivamente la connessione, è necessario verificare se funziona o meno.

Ne vale la pena anche a causa di eventi di disconnessione. Quando sei connesso, potresti perdere la connessione senza che l'oggetto ne sia a conoscenza. Incapsulando l'autocontrollo della connessione in un metodo reconnect chiamato internamente da tutti i metodi che necessitano di una connessione funzionante, si isolano davvero gli sviluppatori dall'affrontare i problemi ... o almeno il più possibile poiché quando tutto fallisce si ha nessuna altra soluzione che li faccia sapere :)

0

Fare il "vero lavoro" in un costruttore è meglio evitare.

Se configuro le connessioni del database, apro file ecc all'interno di un costruttore e se così facendo uno di essi genera un'eccezione, si verificherà una perdita di memoria. Ciò comprometterà l'applicazione exception safety.

Un altro motivo per evitare di lavorare in un costruttore è che renderebbe l'applicazione meno controllabile. Supponiamo che stiate scrivendo un elaboratore di pagamenti con carta di credito. Se si dice nel costruttore della classe CreditCardProcessor si fa tutto il lavoro di connessione a un gateway di pagamento, si autentica e si fattura la carta di credito come faccio a scrivere mai test unitari per la classe CreditCardProcessor?

Venendo allo scenario, se le routine che eseguono query sul dispositivo non generano eccezioni e non testerete la classe in isolamento, probabilmente è preferibile eseguire il lavoro nel costruttore ed evitare chiamate a tale extra Metodo init.

0

ci sono un paio di ragioni avrei usato costruttore separato/init():

  • pigro/inizializzazione ritardata. Ciò consente di creare l'oggetto in modo rapido, una rapida risposta dell'utente e di ritardare un'inizializzazione più lunga per l'elaborazione successiva o in background. Questo fa anche parte di uno o più schemi di progettazione riguardanti i pool di oggetti riutilizzabili per evitare allocazioni costose.
  • Non sicuro se questo ha un nome proprio, ma forse quando l'oggetto è creato, le informazioni di inizializzazione non sono disponibili o non sono comprese da chi sta creando l'oggetto (ad esempio, la creazione di oggetti generici in blocco). Un'altra sezione di codice ha il know-how per inizializzarlo, ma non crearlo.
  • Come motivo personale, il distruttore dovrebbe essere in grado di annullare tutto ciò che ha fatto il costruttore. Se ciò comporta l'uso di init/deinit() interno, nessun problema, a condizione che siano immagini speculari l'una dell'altra.
+0

Questi obiettivi sono raggiunti in modo più sicuro creando un oggetto di un tipo la cui responsabilità è quella di creare il tipo reale più tardi. La cosa importante è che quando hai un oggetto del tipo reale, sai che è in uno stato sicuro da usare. Invece di "construct a Thing", chiama il suo metodo init(), quindi verifica sempre che tu abbia chiamato init() "tu hai" costruisci il ThingBuilder, chiama il suo metodo createThing(), poi usa la tua cosa " – mabraham

Problemi correlati