Sta usando "nuovo" nel costruttore sempre male?

37

Ho letto che usare "nuovo" in un costruttore (per qualsiasi altro oggetto rispetto a quelli di valore semplice) è una cattiva pratica in quanto rende impossibile il test delle unità (come allora anche quei collaboratori devono essere creati e non possono essere presi in giro). Dato che non ho molta esperienza con i test unitari, sto cercando di raccogliere alcune regole che imparerò prima. Inoltre, questa è una regola generalmente valida, indipendentemente dalla lingua utilizzata?

    
posta Ezoela Vacca 01.02.2018 - 16:23
fonte

7 risposte

37

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

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

Non si tratta solo di test unitari. Cosa succede se voglio puntare un repository su due database diversi contemporaneamente? La possibilità di passare nel mio contesto personale mi consente di istanziare due repository diversi che puntano a posizioni diverse.

Non utilizzare nuovo nel costruttore rende il codice più flessibile. Questo vale anche per le lingue che potrebbero utilizzare costrutti diversi da new per l'inizializzazione dell'oggetto.

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, new deve essere chiamato. Fai molta attenzione a chiamare new all'interno di una classe da cui dipendono molte altre classi.

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

Maggiore è il numero di riferimenti a una classe, maggiore è l'attenzione da non chiamare new dall'interno di essa.

    
risposta data 01.02.2018 - 16:31
fonte
50

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

Se la classe rappresenta una raccolta, allora potrebbe avere un array helper interno o un elenco o un hashset. Userebbe new per creare questi aiutanti e sarebbe considerato abbastanza normale. La classe non offre l'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 a seconda del linguaggio di programmazione e, alcune astrazioni di dominio, in alcune lingue, richiedono semplicemente più oggetti al livello del linguaggio di programmazione.

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

    
risposta data 01.02.2018 - 17:26
fonte
27

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

    
risposta data 01.02.2018 - 16:30
fonte
12

As I am not really experienced in unit testing, I am trying to gather some rules that I will learn first.

Stai attento a imparare "regole" per problemi che non hai mai incontrato. Se ti imbatti in "regole" o "best practice", ti suggerirei di trovare un semplice esempio di giocattolo in cui questa regola è "supposta" da usare e di cercare di risolvere quel problema te stesso , ignorando cosa dice la "regola".

In questo caso, potresti provare a creare 2 o 3 classi semplici e alcuni comportamenti da implementare. Implementare le classi in qualsiasi modo si senta naturale e scrivere un test unitario per ogni comportamento. Fai una lista di tutti i problemi che hai riscontrato, ad es. se avevi iniziato a lavorare in un modo, allora dovevi tornare indietro e cambiarlo più tardi; se sei confuso su come le cose dovrebbero combaciare; se ti dava fastidio scrivere a boilerplate; ecc.

Quindi prova a risolvere lo stesso problema seguendo la "regola". Di nuovo, fai una lista dei problemi che hai incontrato. Confronta le liste e pensa a quali situazioni potrebbero essere migliori quando si segue la regola, e quali no.

Per quanto riguarda la tua domanda, tendo a favorire un approccio per porte e adattatori , dove facciamo una distinzione tra "core logic" e "services" (questo è simile alla distinzione tra pure funzioni e procedure efficaci).

La logica di base si basa sul calcolo delle cose "all'interno" dell'applicazione, in base al dominio del problema. Potrebbe contenere classi come User , Document , Order , Invoice , ecc. Va bene che le classi di core chiamino new per altre classi core, dal momento che sono dettagli di implementazione "interni". Ad esempio, la creazione di un Order potrebbe anche creare un Invoice e un Document che dettagliano ciò che è stato ordinato. Non c'è bisogno di prendere in giro questi durante i test, perché queste sono le cose reali che vogliamo testare!

Le porte e gli adattatori sono il modo in cui la logica principale interagisce con il mondo esterno. Qui è dove cose come Database , ConfigFile , EmailSender , ecc. Dal vivo. Queste sono le cose che rendono difficile il testing, quindi è consigliabile creare queste all'esterno della logica di base e passarle in base alle necessità (con l'iniezione della dipendenza, o come argomenti del metodo, ecc.).

In questo modo, la logica di base (che è la parte specifica dell'applicazione, in cui vive la logica aziendale importante ed è soggetta al maggior numero di churn) può essere testata da sola, senza doversi preoccupare di database, file, e-mail , ecc. Possiamo solo passare alcuni valori di esempio e controllare che otteniamo i giusti valori di output.

Le porte e gli adattatori possono essere testati separatamente, usando i mock per il database, il filesystem, ecc. senza doversi preoccupare della logica di business. Possiamo semplicemente passare alcuni valori di esempio e accertarci che vengano memorizzati / letti / inviati / ecc. opportunamente.

    
risposta data 01.02.2018 - 22:52
fonte
6

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

There are always exceptions, but yes, this rule is generally valid, and also applies outside of the constructor as well.

Using new in a constructor violates the D in SOLID (dependency inversion principal). It makes your code hard to test because unit testing is all about isolation; it is hard to isolate class if it has concrete references.

-TheCatWhisperer-

Sì, usare new all'interno dei costruttori porta spesso a difetti di progettazione (ad esempio accoppiamento stretto) che rende rigido il nostro design. Difficile da provare sì, ma non impossibile. La proprietà in gioco qui è resilienza (tolleranza ai cambiamenti) 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.

There are of course exceptions where the class is an immutable value object, an implementation detail, etc. Where they are supposed to be tightly coupled.

-David Arno-

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

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

Quindi la risposta probabile, come al solito, è dipende . L'utilizzo di new all'interno dei costruttori può essere una cattiva pratica. Ma non sistematicamente sempre.

Quindi, ci serve per valutare se le classi sono dettagli di implementazione (la maggior parte dei casi non lo saranno) della classe principale. Se lo sono, lasciali soli. Se non lo sono, considera tecniche come Root di composizione o Iniezione delle dipendenze da contenitori IoC .

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

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

    
risposta data 01.02.2018 - 17:34
fonte
3

Come semplice esempio, considera 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; }
}

Poiché new è un puro dettaglio di implementazione di Foo , e sia Foo::Bar che Foo::Baz fanno parte di Foo , quando il test unitario di Foo non ha senso nel deridere parti di% codice%. Devi solo prendere in giro le parti esterne Foo durante il test unitario Foo .

    
risposta data 02.02.2018 - 11:08
fonte
-3

Sì, l'uso di "nuovo" nelle classi root dell'applicazione è un odore di codice. Significa che stai bloccando la classe nell'uso di 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 le dipendenze derise durante i test, ma renderà la tua applicazione molto più flessibile, consentendo di sostituire rapidamente diverse implementazioni, se necessario.

EDIT: Per i downvoters: ecco un link a un libro di sviluppo software che segnala "nuovo" come possibile odore di codice: link

    
risposta data 01.02.2018 - 16:32
fonte

Leggi altre domande sui tag