quanto complesso dovrebbe essere un costruttore


18

Sto discutendo con il mio collega su quanto lavoro può fare un costruttore. Ho una classe, B che richiede internamente un altro oggetto A. L'oggetto A è uno dei pochi membri di cui la classe B ha bisogno per fare il suo lavoro. Tutti i suoi metodi pubblici dipendono dall'oggetto interno A. Le informazioni sull'oggetto A sono memorizzate nel DB, quindi provo a convalidare e ottenerlo cercando nel DB nel costruttore. Il mio collega ha sottolineato che il costruttore non dovrebbe fare molto altro che catturare i parametri del costruttore. Dato che tutti i metodi pubblici fallirebbero comunque se l'oggetto A non fosse trovato usando gli input per il costruttore, ho sostenuto che invece di consentire la creazione di un'istanza e successivamente fallire, in realtà è meglio lanciare presto nel costruttore.

Cosa pensano gli altri? Sto usando C # se questo fa la differenza.

Lettura C'è mai una ragione per fare tutto il lavoro di un oggetto in un costruttore? mi chiedo di recuperare l'oggetto A andando su DB fa parte di "qualsiasi altra inizializzazione necessaria per rendere l'oggetto pronto per l'uso" perché se l'utente passasse valori errati al costruttore, non sarei in grado di usare nessuno dei suoi metodi pubblici.

I costruttori dovrebbero creare un'istanza dei campi di un oggetto ed eseguire qualsiasi altra inizializzazione necessaria per rendere l'oggetto pronto per l'uso. Questo in genere significa che i costruttori sono piccoli, ma ci sono scenari in cui ciò comporterebbe una notevole quantità di lavoro.


8
Non sono d'accordo. Dai un'occhiata al principio di inversione di dipendenza, sarebbe meglio se l'oggetto A fosse passato all'oggetto B in uno stato valido nel suo costruttore.
Jimmy Hoffa,

5
Stai chiedendo quanto si può fare? Quanto dovrebbe essere fatto? O quanta parte della tua situazione specifica dovrebbe essere fatta? Sono tre domande molto diverse.
Bobson,

1
Sono d'accordo con il suggerimento di Jimmy Hoffa di esaminare l'iniezione di dipendenza (o "inversione di dipendenza"). È stato il mio primo pensiero a leggere la descrizione, perché sembra che tu sia già a metà strada; hai già funzionalità separate nella classe A, che la classe B sta usando. Non posso parlare al tuo caso, ma generalmente trovo che il codice di refactoring per usare il modello di iniezione di dipendenza renda le classi più semplici / meno intrecciate.
Apprendista Dr. Wily,

1
Bobson, ho cambiato il titolo in "dovrebbe". Chiedo le migliori pratiche qui.
Taein Kim,

1
@JimmyHoffa: forse dovresti espandere il tuo commento in una risposta.
Kevin Cline,

Risposte:


22

La tua domanda è composta da due parti completamente separate:

Devo gettare un'eccezione dal costruttore o devo avere un metodo fallito?

Questa è chiaramente l'applicazione del principio Fail-fast . Far fallire il costruttore è molto più facile eseguire il debug rispetto a dover scoprire perché il metodo non sta funzionando. Ad esempio, è possibile che l'istanza sia già stata creata da un'altra parte del codice e si ottengano errori durante la chiamata dei metodi. È ovvio che l'oggetto è stato creato in modo errato? No.

Per quanto riguarda il problema "avvolgi la chiamata in try / catch". Le eccezioni sono pensate per essere eccezionali . Se sai che del codice genererà un'eccezione, non avvolgi il codice in try / catch, ma convalidi i parametri di quel codice prima di eseguire il codice che può generare un'eccezione. Le eccezioni sono intese solo come modo per garantire che il sistema non entri in uno stato non valido. Se sai che alcuni parametri di input possono portare a uno stato non valido, assicurati che tali parametri non si verifichino mai. In questo modo, devi solo provare / catturare in luoghi, dove puoi gestire logicamente l'eccezione, che di solito è al limite del sistema.

Posso accedere a "altre parti del sistema, come DB" dal costruttore.

Penso che ciò vada contro il principio del minimo stupore . Non molte persone si aspetterebbero che il costruttore acceda a un DB. Quindi no, non dovresti farlo.


2
La soluzione più appropriata a questo è di solito aggiungere un metodo di tipo "aperto" in cui la costruzione è libera, ma non è possibile fare alcun uso prima di "aprire" / "connettersi" / ecc., Che è dove si verifica tutta l'inizializzazione effettiva. I fallimenti dei costruttori possono causare ogni sorta di problemi quando in futuro si desidera eseguire la costruzione parziale di oggetti, che è un'abilità utile.
Jimmy Hoffa,

@JimmyHoffa Non vedo alcuna differenza tra il metodo del costruttore e il metodo specifico per la costruzione.
Euforico

4
Non puoi, ma molti lo fanno. Pensa quando crei un oggetto connessione, il costruttore lo connette? L'oggetto è utilizzabile se non è collegato? In entrambe le domande la risposta è no, perché è utile avere oggetti di connessione parzialmente costruiti in modo da poter modificare impostazioni e cose prima di aprire la connessione. Questo è un approccio comune a questo scenario. Continuo a pensare che l'iniezione del costruttore sia l'approccio giusto per gli scenari in cui l'oggetto A ha una dipendenza dalle risorse, ma un metodo aperto è ancora meglio che fare in modo che il costruttore faccia il lavoro pericoloso.
Jimmy Hoffa,

@JimmyHoffa Ma questo è un requisito specifico della classe di connessione, non una regola generale di progettazione OOP. In realtà lo definirei un caso eccezionale, piuttosto che una regola. In pratica stai parlando del modello del costruttore.
Euforico

2
Non è un requisito, è stata una decisione di progettazione. Avrebbero potuto costringere il costruttore a prendere una stringa di connessione e connettersi immediatamente quando costruito. Hanno deciso non troppo perché è utile essere in grado di creare l'oggetto, quindi modificare la stringa di connessione o altri bit e bobbles e collegarlo in seguito ,
Jimmy Hoffa

19

Ugh.

Un costruttore dovrebbe fare il meno possibile - il problema è che il comportamento delle eccezioni nei costruttori è imbarazzante. Pochissimi programmatori sanno qual è il comportamento corretto (compresi gli scenari di ereditarietà) e costringere gli utenti a provare / catturare ogni singola istanza è ... nella migliore delle ipotesi doloroso.

Ci sono due parti in questo, nel costruttore e nell'uso del costruttore. È strano nel costruttore perché non si può fare molto lì quando si verifica un'eccezione. Non puoi restituire un tipo diverso. Non puoi restituire null. Fondamentalmente è possibile ridisegnare l'eccezione, restituire un oggetto rotto (errata costellazione) o (nel migliore dei casi) sostituire una parte interna con un valore predefinito sano. L'uso del costruttore è imbarazzante perché allora le tue istanze (e le istanze di tutti i tipi derivati!) Possono essere lanciate. Quindi non puoi usarli negli inizializzatori dei membri. Finisci per usare le eccezioni per la logica per vedere "la mia creazione è riuscita?", Oltre la leggibilità (e la fragilità) causata da try / catch ovunque.

Se il tuo costruttore sta facendo cose non banali, o cose che ci si può ragionevolmente aspettare che falliscano, considera invece un Createmetodo statico (con un costruttore non pubblico). È molto più utilizzabile, più facile da eseguire il debug e ti dà comunque un'istanza completamente costruita quando ha successo.


2
Gli scenari di costruzione complessi sono esattamente il motivo per cui esistono modelli di creazione come quelli che hai citato qui. Questo è un buon approccio a queste costruzioni problematiche. Mi viene da pensare a come in .NET un Connectionoggetto può fare CreateCommandper te in modo che tu sappia che il comando è opportunamente connesso.
Jimmy Hoffa,

2
A questo aggiungerei che un database è una dipendenza esterna pesante, supponiamo che tu voglia cambiare database in un certo punto in futuro (o deridere un oggetto per il test), spostando questo tipo di codice in una classe Factory (o qualcosa di simile a un IService fornitore) sembra un approccio più pulito
Jason Sperske

4
puoi approfondire "il comportamento eccezionale nei costruttori è imbarazzante"? Se dovessi usare Crea, non avresti bisogno di avvolgerlo in prova a catturare proprio come faresti con la chiamata al costruttore? Sento che Creare è solo un nome diverso per costruttore. Forse non sto ottenendo la tua risposta giusta?
Taein Kim,

1
Avendo dovuto provare a catturare le eccezioni ribollendo dai costruttori, +1 agli argomenti contrari. Avendo lavorato con persone che dicono "Nessun lavoro nel costruttore", questo può anche creare alcuni schemi strani. Ho visto codice in cui ogni oggetto ha non solo un costruttore, ma anche una forma di Initialize () dove, indovina un po '? L'oggetto non è realmente valido fino a quando non viene chiamato neanche. E poi hai due cose da chiamare: il ctor e Initialize (). Il problema di fare un lavoro per impostare un oggetto non scompare: C # può fornire soluzioni diverse rispetto a C ++. Le fabbriche sono buone!
J Trana,

1
@jtrana - sì, l'inizializzazione non va bene. Il costruttore si inizializza su un valore predefinito sano o su un metodo factory o create lo costruisce e restituisce un'istanza utilizzabile.
Telastyn,

1

Costruttore - l'equilibrio della complessità del metodo è davvero una questione di discussione sul buon stile. Class() {}Viene spesso usato un costruttore vuoto , con un metodo Init(..) {..}per cose più complicate. Tra gli argomenti da prendere in considerazione:

  • Possibilità di serializzazione di classe
  • Con il metodo Init di separazione dal costruttore, spesso è necessario ripetere Class x = new Class(); x.Init(..);più volte la stessa sequenza , in parte in Test unità. Non così buono, tuttavia, cristallino per la lettura. A volte, li inserisco in una riga di codice.
  • Quando l'obiettivo del test unitario è solo il test di inizializzazione della classe, meglio avere il proprio Init, eseguire azioni più complicate una per volta, con gli Assert intermedi.

il vantaggio, secondo me, del initmetodo è che la gestione degli errori è molto più semplice. In particolare, il valore di ritorno del initmetodo può indicare se l'inizializzazione ha esito positivo e il metodo può garantire che l'oggetto sia sempre in buono stato.
pqnet,
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.