Approcci graduali all'iniezione di dipendenza


12

Sto lavorando per rendere le mie lezioni testabili in unità, usando l'iniezione delle dipendenze. Ma alcune di queste classi hanno molti clienti e non sono ancora pronta a riformattare tutte per iniziare a passare le dipendenze. Quindi sto provando a farlo gradualmente; mantenendo le dipendenze predefinite per ora, ma consentendo loro di essere sovrascritte per i test.

Un approccio che sto affrontando è quello di spostare tutte le "nuove" chiamate nei loro metodi, ad esempio:

public MyObject createMyObject(args) {
  return new MyObject(args);
}

Quindi nei test delle unità, posso semplicemente sottoclassare questa classe e sovrascrivere le funzioni di creazione, in modo che creino invece oggetti falsi.

è un buon approccio? Ci sono degli svantaggi?

Più in generale, va bene avere dipendenze codificate, purché sia ​​possibile sostituirle per i test? So che l'approccio preferito è richiederli esplicitamente nel costruttore, e mi piacerebbe arrivarci alla fine. Ma mi chiedo se questo è un buon primo passo.

Uno svantaggio che mi è appena venuto in mente: se hai sottoclassi reali che devi testare, non puoi riutilizzare la sottoclasse di test che hai scritto per la classe genitore. Dovrai creare una sottoclasse di prova per ogni sottoclasse reale e dovrà sovrascrivere le stesse funzioni di creazione.


Puoi approfondire il motivo per cui non sono ora testabili in unità?
Austin Salonen,

Ci sono molte "nuove X" sparse nel codice, così come molti usi di classi statiche e singoli. Quindi, quando provo a provare una classe, ne sto davvero testando un sacco. Se riesco a isolare le dipendenze e sostituirle con oggetti falsi, avrò un maggiore controllo sui test.
JW01

Risposte:


13

Questo è un buon approccio per iniziare. Si noti che il più importante è coprire il codice esistente con unit test; una volta che hai i test, puoi refactoring più liberamente per migliorare ulteriormente il tuo design.

Quindi il punto iniziale non è rendere il design elegante e brillante, ma solo rendere testabile l'unità di codice con le modifiche meno rischiose . Senza test unitari, devi essere più prudente e cauto per evitare di violare il tuo codice. Queste modifiche iniziali possono persino rendere il codice più goffo o brutto in alcuni casi, ma se ti consentono di scrivere i primi test unitari, alla fine sarai in grado di rifattorizzare verso il design ideale che hai in mente.

Il lavoro fondamentale da leggere su questo argomento è lavorare efficacemente con il codice legacy . Descrive il trucco che mostri sopra e molti, molti altri, usando diverse lingue.


Solo un'osservazione su questo "una volta che hai i test, puoi refactoring più liberamente" - Se non hai test, non stai eseguendo il refactoring, stai solo cambiando il codice.
ottobre

@Slomojo Vedo il tuo punto, ma puoi ancora fare un sacco di refactoring sicuro senza test, in particolare rattrezzamenti IDE automatizzati come, ad esempio (in eclissi per Java), estrarre codice duplicato in metodi, rinominare tutto, spostare classi ed estrarre campi, costanti ecc.
Alb

Sto solo citando le origini del termine Refactoring, che sta modificando il codice in schemi migliorati, protetti da errori di regressione da un framework di test. Ovviamente, il refactoring basato su IDE ha reso molto più banale il software 'refactor', ma tendo a preferire evitare l'auto-illusione, se il mio software non ha test e io "refactor" sto solo cambiando il codice. Certo, potrebbe non essere quello che dico a qualcun altro;)
ocodo

1
@Alb, il refactoring senza test è sempre più rischioso. I refactoring automatici sicuramente riducono quel rischio, ma non a zero. Ci sono sempre casi complicati che gli strumenti potrebbero non essere in grado di gestire correttamente. Nel migliore dei casi il risultato è un messaggio di errore dallo strumento, ma nel peggiore dei casi può introdurre silenziosamente bug. Pertanto si consiglia di mantenere le modifiche più semplici e sicure possibili anche con strumenti automatizzati.
Péter Török,

3

La gente di Guice consiglia di usare

@Inject
public void setX(.... X x) {
   this.x = x;
}

per tutte le proprietà anziché semplicemente aggiungere @Inject alla loro definizione. Ciò ti consentirà di trattarli come normali bean, cosa che puoi fare newdurante il test (e l'impostazione manuale delle proprietà) e @Inject in produzione.


3

Quando dovrò affrontare questo tipo di problema, identificherò ogni dipendenza della mia classe da testare e creerò un costruttore protetto che mi permetterà di superarli. Questo è effettivamente lo stesso che creare un setter per ciascuno, ma preferisco dichiarando dipendenze nei miei costruttori in modo che non dimentichi di istanziarle in futuro.

Quindi la mia nuova classe testabile dell'unità avrà 2 costruttori:

public MyClass() { 
  this(new Client1(), new Client2()); 
}

public MyClass(Client1 client1, Client2 client2) { 
  this.client1 = client1; 
  this.client2 = client2; 
}

2

Potresti prendere in considerazione il linguaggio "crea getter" fino a quando non sarai in grado di usare la dipendenza iniettata.

Per esempio,

public class Example {
  private MyObject myObject=null;

  public MyObject getMyObject() {
    if (myObject==null) {
      myObject=new MyObject();
    } 
    return myObject;
  }

  public void setMyObject(MyObject myObject) {
    this.myObject=myObject;
  }

  private void myMethod() {
    if (getMyObject().doSomething()) {
      // Instance automatically created
    }
  }
}

Ciò ti consentirà di effettuare il refactoring internamente in modo da poter fare riferimento a myObject tramite il getter. Non devi mai chiamare new. In futuro, quando tutti i client saranno configurati per consentire l'iniezione delle dipendenze, tutto ciò che dovrete fare è rimuovere il codice di creazione in un unico punto: il getter. Il setter ti permetterà di iniettare oggetti finti come richiesto.

Il codice sopra riportato è solo un esempio e espone direttamente lo stato interno. Dovresti considerare attentamente se questo approccio è appropriato per la tua base di codice.

Consiglierei anche di leggere " Lavorare efficacemente con il codice legacy " che contiene molti suggerimenti utili per avere successo in un importante refactoring.


Grazie. Sto prendendo in considerazione questo approccio per alcuni casi. Ma cosa fai quando il costruttore di MyObject richiede parametri che sono disponibili solo all'interno della tua classe? O quando devi creare più di un MyObject durante la vita della tua classe? Devi iniettare una MyObjectFactory?
JW01

@ JW01 Puoi configurare il codice creatore getter per leggere lo stato interno della tua classe e adattarlo. La creazione di più di un'istanza durante la vita sarà difficile da orchestrare. Se ti imbatti in quella situazione, suggerirei una riprogettazione, piuttosto che una soluzione. Non rendere i tuoi getter troppo complessi o diventeranno una macina al collo. Il tuo obiettivo è facilitare il processo di refactoring. Se sembra troppo difficile, spostati in un'altra area per ripararlo lì.
Gary Rowe,

è un singleton. Basta non usare i singoli O_O
CoffeDeveloper

@DarioOO In realtà è un singleton molto scarso. Una migliore implementazione userebbe un enum secondo Josh Bloch. I singoli sono un modello di progettazione valido e hanno i loro usi (costosa creazione una tantum ecc.).
Gary Rowe,
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.