L'uso del "nuovo" nel costruttore è sempre negativo?


37

Ho letto che usare "nuovo" in un costruttore (per qualsiasi altro oggetto oltre a quelli di semplice valore) è una cattiva pratica in quanto rende impossibile il test unitario (poiché quindi anche quei collaboratori devono essere creati e non possono essere derisi). Dato che non ho molta esperienza nei test unitari, sto cercando di raccogliere alcune regole che imparerò per prime. Inoltre, questa è una regola generalmente valida, indipendentemente dalla lingua utilizzata?


9
Non rende impossibile il test. Tuttavia, rende più difficile la manutenzione e il test del codice. Per esempio, leggi una nuova colla .
David Arno,

38
"sempre" è sempre errato. :) Esistono buone pratiche, ma abbondano le eccezioni.
Paul,

63
Che lingua è questa? newsignifica cose diverse in lingue diverse.
user2357112 supporta Monica l'

13
Questa domanda dipende un po 'dalla lingua, no? Non tutte le lingue hanno nemmeno una newparola chiave. Di quali lingue stai chiedendo?
Bryan Oakley,

7
Regola sciocca. Non riuscire a usare l'iniezione di dipendenza quando si dovrebbe avere ben poco a che fare con la "nuova" parola chiave. Invece di dire che "usare new è un problema se lo usi per interrompere l'inversione di dipendenza", dì semplicemente "non interrompere l'inversione di dipendenza".
Matt Timmermans,

Risposte:


36

Ci sono sempre delle eccezioni, e metto in discussione il "sempre" nel titolo, ma sì, questa linea guida è generalmente valida e si applica anche al di fuori del costruttore.

L'uso di new in un costruttore viola la D in SOLID (principio di inversione di dipendenza). Rende difficile testare il tuo codice perché i test unitari riguardano l'isolamento; è difficile isolare la classe se ha riferimenti concreti.

Non si tratta solo di test unitari. Cosa succede se desidero indirizzare un repository a due database diversi contemporaneamente? La possibilità di passare nel mio contesto mi consente di creare un'istanza di due diversi repository che puntano a posizioni diverse.

Non usare new nel costruttore rende il tuo codice più flessibile. Questo vale anche per i linguaggi che possono usare costrutti diversi da quelli newper l'inizializzazione degli oggetti.

Tuttavia, chiaramente, è necessario usare il buon senso. Ci sono molte volte in cui va bene usare new, o dove sarebbe meglio non farlo, ma non avrai conseguenze negative. Ad un certo punto da qualche parte, newdeve essere chiamato. Fai solo attenzione a chiamare newall'interno di una classe da cui dipendono molte altre classi.

Fare qualcosa come inizializzare una collezione privata vuota nel tuo costruttore va bene, e iniettarla sarebbe assurdo.

Più riferimenti ha una classe, più attenta dovresti non chiamarla newal suo interno.


12
Non vedo davvero alcun valore in questa regola. Esistono regole e linee guida su come evitare l'accoppiamento stretto nel codice. Questa regola sembra gestire esattamente lo stesso problema, tranne per il fatto che è incredibilmente troppo restrittiva senza darci alcun valore aggiuntivo. Quindi qual è il punto? Perché creare un oggetto header fittizio per la mia implementazione di LinkedList invece di occuparsi di tutti quei casi speciali che derivano dall'aver head = nullin qualche modo migliorare il codice? Perché lasciare le raccolte incluse come nulle e crearle su richiesta sarebbe meglio che farlo nel costruttore?
Voo

22
Ti manca il punto. Sì, un modulo di persistenza è assolutamente qualcosa su cui un modulo di livello superiore non dovrebbe dipendere. Ma "non usare mai nuovo" non deriva da "alcune cose dovrebbero essere iniettate e non accoppiate direttamente". Se prendi la tua fedele copia di Agile Software Development vedrai che Martin parla in genere di moduli e non di singole classi: "I moduli di alto livello si occupano delle politiche di alto livello dell'applicazione. Queste politiche generalmente si preoccupano poco dei dettagli che implementano loro."
Voo

11
Quindi il punto è che dovresti evitare le dipendenze tra i moduli, in particolare tra moduli con diversi livelli di astrazione. Ma un modulo può essere più di una singola classe e semplicemente non ha senso erigere interfacce tra codice strettamente accoppiato dello stesso livello di astrazione. In un'applicazione ben progettata che segue i principi SOLID non ogni singola classe dovrebbe implementare un'interfaccia e non tutti i costruttori dovrebbero sempre prendere solo interfacce come parametri.
Voo

18
Ho annullato il voto perché è un sacco di zuppa di parole d'ordine e non molte discussioni su considerazioni pratiche. C'è qualcosa in un repository per due DB, ma onestamente, questo non è un esempio reale.
jpmc26,

5
@ jpmc26, BJoBnh Senza andare su se perdono questa risposta o meno, il punto viene espresso in modo molto chiaro. Se pensi che "principio di inversione di dipendenza", "riferimento concreto" o "inizializzazione dell'oggetto" siano solo parole d'ordine, allora in realtà non sai cosa significano. Non è colpa della risposta.
R. Schmitz,

50

Mentre sono a favore dell'utilizzo del costruttore per inizializzare semplicemente la nuova istanza anziché per creare più altri oggetti, gli oggetti helper sono ok e devi usare il tuo giudizio sul fatto che qualcosa sia un tale helper interno o meno.

Se la classe rappresenta una raccolta, potrebbe avere un array helper interno o un elenco o un hashset. Userebbe newper creare questi aiutanti e sarebbe considerato abbastanza normale. La classe non offre iniezione per utilizzare diversi helper interni e non ha motivo di farlo. In questo caso, si desidera testare i metodi pubblici dell'oggetto, che potrebbero andare ad accumulare, rimuovere e sostituire elementi nella raccolta.


In un certo senso, il costrutto di classe di un linguaggio di programmazione è un meccanismo per creare astrazioni di livello superiore e creiamo tali astrazioni per colmare il divario tra il dominio del problema e le primitive del linguaggio di programmazione. Tuttavia, il meccanismo di classe è solo uno strumento; varia in base al linguaggio di programmazione e, in alcune lingue, alcune astrazioni del dominio richiedono semplicemente più oggetti a livello del linguaggio di programmazione.

In sintesi, è necessario valutare se l'astrazione richiede semplicemente uno o più oggetti interni / di supporto, mentre viene ancora vista dal chiamante come una singola astrazione o se gli altri oggetti sarebbero meglio esposti al chiamante per creare per controllo delle dipendenze, che sarebbe suggerito, ad esempio, quando il chiamante vede questi altri oggetti nell'uso della classe.


4
+1. Questo è esattamente giusto. Devi identificare quale sia il comportamento previsto della classe e che determina quali dettagli di implementazione devono essere esposti e quali no. C'è una sorta di sottile gioco di intuizione che devi giocare per determinare qual è anche la "responsabilità" della classe.
jpmc26

27

Non tutti i collaboratori sono abbastanza interessanti da testare l'unità separatamente, è possibile (indirettamente) testarli attraverso la classe di hosting / istanziazione. Questo potrebbe non allinearsi con l'idea di alcune persone di dover testare ogni classe, ogni metodo pubblico ecc. Specialmente quando si fa il test dopo. Quando si utilizza TDD è possibile effettuare il refactoring di questo "collaboratore" estraendo una classe in cui è già completamente testato dal primo processo di test.


14
Not all collaborators are interesting enough to unit-test separatelyfine della storia :-), questo caso è possibile e nessuno ha osato menzionarlo. @Joppe Ti incoraggio a elaborare un po 'la risposta. Ad esempio, è possibile aggiungere alcuni esempi di classi che sono semplici dettagli di implementazione (non adatti per la sostituzione) e come possono essere estratti in seguito se lo riteniamo necessario.
Laiv

I modelli di dominio @Laiv sono in genere concreti, non astratti, non devi iniettare oggetti nidificati lì. Sono candidati anche un oggetto valore semplice / oggetto complesso che altrimenti non ha logica.
Joppe,

4
+1, assolutamente. Se una lingua è impostata in modo da dover chiamare new File()per fare qualsiasi cosa relativa ai file, non ha senso vietare quella chiamata. Che cosa hai intenzione di fare, scrivere test di regressione contro il Filemodulo di stdlib ? Non probabile D'altra parte, fare appello newa una classe auto-inventata è più dubbio.
Kilian Foth,

7
@KilianFoth: Buona fortuna, unità di prova tutto ciò che chiama direttamente new File(), però.
Phoshi,

1
Questa è una supposizione . Possiamo trovare casi in cui non ha senso, casi in cui non è utile e casi in cui ha senso ed è utile. È una questione di bisogni e preferenze.
Laiv

13

Dato che non ho molta esperienza nei test unitari, sto cercando di raccogliere alcune regole che imparerò per prime.

Fai attenzione a imparare "regole" per i problemi che non hai mai incontrato. Se ti imbatti in qualche "regola" o "migliore pratica", suggerirei di trovare un semplice esempio di giocattolo in cui "si suppone" che questa regola venga utilizzata e di provare a risolvere il problema da solo , ignorando ciò che dice la "regola".

In questo caso, potresti provare a inventare 2 o 3 classi semplici e alcuni comportamenti che dovrebbero implementare. Implementa le lezioni in qualsiasi modo sembri naturale e scrivi un test unitario per ogni comportamento. Fai un elenco di tutti i problemi che hai riscontrato, ad esempio se hai iniziato a lavorare in un modo, poi dovevi tornare indietro e cambiarlo in seguito; se ti sei confuso su come le cose dovrebbero andare insieme; se ti sei infastidito nello scrivere il bollettino; eccetera.

Quindi prova a risolvere lo stesso problema seguendo la "regola". Ancora una volta, fai un elenco dei problemi che hai riscontrato. Confronta gli elenchi e pensa a quali situazioni potrebbero essere migliori quando segui la regola e quali no.


Per quanto riguarda la tua vera domanda, tendo a favorire un approccio di porte e adattatori , in cui facciamo una distinzione tra "logica di base" e "servizi" (questo è simile alla distinzione tra funzioni pure e procedure efficaci).

La logica di base consiste nel calcolare le cose "all'interno" dell'applicazione, in base al dominio del problema. Potrebbe contenere classi come User, Document, Order, Invoice, fine del ecc di avere classi di base richiedono newper le altre classi di base, dato che sono i dettagli "interni" di implementazione. Ad esempio, la creazione di un Orderpotrebbe anche creare un Invoicee un Documentdettaglio di ciò che è stato ordinato. Non c'è bisogno di deriderli durante i test, perché queste sono le cose reali che vogliamo testare!

Le porte e gli adattatori sono il modo in cui la logica di base interagisce con il mondo esterno. Questo è dove le cose come Database, ConfigFile, EmailSender, tempo reale, ecc. Queste sono le cose che rendono difficile il test, quindi è consigliabile crearle al di fuori della logica di base e passarle secondo necessità (con iniezione di dipendenza o come argomenti del metodo, ecc.).

In questo modo, la logica di base (che è la parte specifica dell'applicazione, in cui vive l'importante logica di business, ed è soggetta alla maggior parte dei churn) può essere testata da sola, senza preoccuparsi di database, file, e-mail, ecc. Possiamo semplicemente passare alcuni valori di esempio e verificare di ottenere i valori di output corretti.

Le porte e gli adattatori possono essere testati separatamente, usando simulazioni per il database, il filesystem, ecc. Senza preoccuparsi della logica aziendale. Possiamo semplicemente passare alcuni valori di esempio e assicurarci che vengano memorizzati / letti / inviati / ecc. appropriatamente.


6

Consentitemi di rispondere alla domanda, raccogliendo quelli che considero i punti chiave qui. Citerò alcuni utenti per brevità.

Ci sono sempre delle eccezioni, ma sì, questa regola è generalmente valida e si applica anche al di fuori del costruttore.

L'uso di new in un costruttore viola la D in SOLID (principal di inversione di dipendenza). Rende difficile testare il tuo codice perché i test unitari riguardano l'isolamento; è difficile isolare la classe se ha riferimenti concreti.

-TheCatWhisperer-

Sì, l'uso di newcostruttori interni spesso porta a difetti di progettazione (ad esempio l'accoppiamento stretto) che rendono il nostro design rigido. Difficile da provare sì, ma non impossibile. La proprietà in gioco qui è la resilienza (tolleranza alle modifiche) 1 .

Tuttavia, la citazione sopra non è sempre vera. In alcuni casi, potrebbero esserci classi che devono essere strettamente accoppiate . David Arno ha commentato un paio.

Esistono ovviamente eccezioni in cui la classe è un oggetto valore immutabile , un dettaglio di implementazione , ecc. Dove si suppone che debbano essere strettamente accoppiati .

-David Arno-

Esattamente. Alcune classi (ad esempio classi interne) potrebbero essere semplici dettagli di implementazione della classe principale. Questi devono essere testati insieme alla classe principale e non sono necessariamente sostituibili o estensibili.

Inoltre, se il nostro culto SOLID ci fa estrarre queste classi , potremmo violare un altro buon principio. La cosiddetta legge di Demetra . Che, d'altra parte, trovo che sia davvero importante dal punto di vista del design.

Quindi la risposta probabile, come al solito, è dipende . L'uso di newcostruttori interni può essere una cattiva pratica. Ma non sempre sistematicamente.

Quindi, ci serve per valutare se le classi sono dettagli di implementazione (la maggior parte dei casi non lo faranno) della classe principale. Se lo sono, lasciarli soli. In caso contrario, prendere in considerazione tecniche come la composizione principale o l' iniezione di dipendenza da contenitori IoC .


1: l'obiettivo principale di SOLID non è rendere il nostro codice più testabile. È per rendere il nostro codice più tollerante alle modifiche. Più flessibile e, di conseguenza, più facile da testare

Nota: David Arno, TheWhisperCat, spero che non ti dispiaccia di averti citato.


3

Come semplice esempio, considerare il seguente pseudocodice

class Foo {
  private:
     class Bar {}
     class Baz inherits Bar {}
     Bar myBar
  public:
     Foo(bool useBaz) { if (useBaz) myBar = new Baz else myBar = new Bar; }
}

Dal momento che newè un puro dettaglio di implementazione di Foo, ed entrambi Foo::Bare Foo::Bazfanno parte di Foo, quando i test unitari Foonon hanno senso deridere parti di Foo. Si prendono in giro solo le parti all'esterno Foo durante il test dell'unità Foo.


-3

Sì, l'uso di "nuovo" nelle classi radice dell'applicazione è un odore di codice. Significa che stai bloccando la classe usando un'implementazione specifica e non sarai in grado di sostituirne un'altra. Optare sempre per l'iniezione della dipendenza nel costruttore. In questo modo non solo sarai in grado di iniettare facilmente dipendenze derise durante il test, ma renderà la tua applicazione molto più flessibile, consentendoti di sostituire rapidamente diverse implementazioni se necessario.

EDIT: per downvoter - ecco un link a un libro di sviluppo software che contrassegna "nuovo" come un possibile odore di codice: https://books.google.com/books?id=18SuDgAAQBAJ&lpg=PT169&dq=new%20keyword%20code%20smell&pg=PT169 # v = onepage & q = new% 20keyword% 20code% 20smell & f = false


15
Yes, using 'new' in your non-root classes is a code smell. It means you are locking the class into using a specific implementation, and will not be able to substitute another.Perché questo è un problema? Non ogni singola dipendenza in un albero delle dipendenze dovrebbe essere aperta alla sostituzione
Laiv,

4
@Paul: avere un'implementazione predefinita significa che devi prendere un riferimento strettamente legato alla classe concreta specificata come predefinita. Ciò non lo rende un cosiddetto "odore di codice", però.
Robert Harvey,

8
@EzoelaVacca: sarei cauto nell'usare le parole "odore di codice" in qualsiasi contesto. È un po 'come dire "repubblicano" o "democratico"; cosa significano quelle parole? Non appena dai a qualcosa un'etichetta del genere, smetti di pensare ai problemi reali e l'apprendimento si interrompe.
Robert Harvey,

4
"Più flessibile" non è automaticamente "migliore".
whatsisname

4
@EzoelaVacca: usare la newparola chiave non è una cattiva pratica e non lo è mai stata. È il modo in cui usi lo strumento che conta. Non usi una mazza, ad esempio, dove sarà sufficiente un martello a sfera.
Robert Harvey,
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.