Dependency Inversion espande l'API, genera test non necessari

9

Questa domanda mi ha infastidito per alcuni giorni e sembra che diverse pratiche si contraddicono a vicenda.

Esempio

Iterazione 1

public class FooDao : IFooDao
{
    private IFooConnection fooConnection;
    private IBarConnection barConnection;

    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooConnection = fooConnection;
        this.barConnection = barConnection;
    }

    public Foo GetFoo(int id)
    {
        Foo foo = fooConection.get(id);
        Bar bar = barConnection.get(foo);
        foo.bar = bar;
        return foo;
    }
}

Ora, quando eseguo il test, emetto IFooConnection e IBarConnection e utilizzo Dependency Injection (DI) durante l'istanza di FooDao.

Posso modificare l'implementazione, senza modificare la funzionalità.

Iterazione 2

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooBuilder = new FooBuilder(fooConnection, barConnection);
    }

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

Ora, non scriverò questo costruttore, ma immagino che faccia la stessa cosa che fece FooDao prima. Questo è solo un refactoring, quindi, ovviamente, questo non cambia la funzionalità, e così, il mio test passa ancora.

IFooBuilder è Internal, poiché esiste solo per fare del lavoro per la libreria, cioè non è una parte dell'API.

L'unico problema è che non rispondo più a Dependency Inversion. Se ho riscritto questo problema per risolvere il problema, potrebbe apparire come questo.

Iteration 3

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    public FooDao(IFooBuilder fooBuilder)
    {
        this.fooBuilder = fooBuilder;
    }

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

Questo dovrebbe farlo. Ho cambiato il costruttore, quindi il mio test deve cambiare per supportare questo (o viceversa), questo non è il mio problema però.

Affinché questo funzioni con DI, FooBuilder e IFooBuilder devono essere pubblici. Ciò significa che dovrei scrivere un test per FooBuilder, dal momento che è diventato improvvisamente parte dell'API della mia biblioteca. Il mio problema è che i clienti della biblioteca dovrebbero usarlo solo attraverso il mio design API, IFooDao, ei miei test funzionano come clienti. Se non seguo Dependency Inversion i miei test e le API sono più pulite.

In altre parole, tutto quello che mi interessa come client, o come test, è ottenere il Foo corretto, non come è costruito.

Soluzioni

  • Dovrei semplicemente non preoccuparmi, scrivere i test per FooBuilder anche se è solo pubblico per favore DI? - Supporta l'iterazione 3

  • Devo rendermi conto che espandere l'API è un aspetto negativo di Dependency Inversion e essere molto chiaro sul motivo per cui ho scelto di non rispettarlo qui? - Supporta l'iterazione 2

  • Pongo troppa enfasi sull'avere un'API pulita? - Supporta l'iterazione 3

EDIT: Voglio chiarire che il mio problema è non "Come testare gli internals?", piuttosto è qualcosa come "Posso mantenerlo interno, e comunque rispettare il DIP, e dovrei? ".

    
posta Chris Wohlert 31.05.2016 - 11:05
fonte

4 risposte

3

Vorrei andare con:

Should I realise that expanding the API is a downside of Dependency Inversion, and be very clear about why I chose not to comply to it here? - Supports iteration 2

Tuttavia, in The S.O.L.I.D. Principi di OO e Agile Design (a 1:08:55) Lo zio Bob dice che la sua regola sull'iniezione di dipendenza è di non iniettare tutto, si inietta solo in punti strategici. (Dice anche che gli argomenti dell'inversione di dipendenza e dell'iniezione di dipendenza sono gli stessi).

Cioè, l'inversione di dipendenza non è pensata per essere applicata a tutte le dipendenze di classe in un programma. Dovresti farlo in posizioni strategiche (quando paga (o potrebbe pagare di)).

    
risposta data 02.06.2016 - 10:25
fonte
5

Ho intenzione di condividere alcune esperienze / osservazioni di varie basi di codice che ho visto. N.B. In particolare, non sto sostenendo alcun approccio, solo condividendo con te dove vedo che il codice sta andando:

Should I simply not care, write the tests for FooBuilder even though it is only public to please DI

Prima di DI e test automatici, la saggezza accettata era che qualcosa dovrebbe essere limitato all'ambito richiesto. Al giorno d'oggi, tuttavia, non è raro vedere metodi che potrebbero essere lasciati internamente resi pubblici per facilità di test (poiché questo di solito accade in un altro assembly). N.B. c'è una direttiva per esporre i metodi interni ma questo più o meno equivale alla stessa cosa.

Should I realise that expanding the API is a downside of Dependency Inversion, and be very clear about why I chose not to comply to it here?

DI comporta indubbiamente un sovraccarico con codice aggiuntivo.

Do I put too much emphasis on having a clean API

Poiché i framework DI fanno introducono un codice aggiuntivo, ho notato uno spostamento verso il codice che mentre scritto nello stile DI non usa DI per sé, cioè la D di SOLID.

In breve:

  1. I modificatori di accesso a volte vengono allentati per semplificare i test

  2. DI introduce il codice aggiuntivo

Alla luce di 1 & 2, alcuni sviluppatori ritengono che questo sia troppo sacrificio e quindi semplicemente scelgono di dipendere dalle astrazioni inizialmente con l'opzione di ri-adattarsi in un secondo momento.

    
risposta data 31.05.2016 - 11:17
fonte
0

Non apprezzo particolarmente questa nozione secondo cui devi esporre metodi privati (con qualunque mezzo) per eseguire test appropriati. Suggerirei inoltre che l'API del tuo client sia non uguale al metodo pubblico del tuo oggetto, ad es. ecco la tua interfaccia pubblica:

interface IFooDao {
   public Foo GetFoo(int id);
}

e non c'è menzione del tuo FooBuilder

Nella tua domanda iniziale, prenderei la terza via, usando il tuo FooBuilder . Potrei testare il tuo FooDao usando un mock , concentrandoti sulla tua funzionalità FooDao (puoi sempre iniettare un vero builder se è più semplice) e vorrei test separati attorno al tuo FooBuilder .

(Mi sorprende che il mocking non sia stato indicato in questa domanda / risposta: va di pari passo con il test delle soluzioni con DI / IoC)

Re. il tuo commento:

As stated, the builder provides no new functionality, so I should not need to test it, however, DIP makes it part of the API, which forces me to test it.

Non penso che questo amplia l'API che presenti ai tuoi clienti. Puoi limitare l'API che i tuoi client usano tramite interfacce (se lo desideri). Vorrei anche suggerire che i tuoi test non dovrebbero circondare solo i tuoi componenti rivolti al pubblico. Dovrebbero abbracciare tutte le classi (implementazioni) che supportano quell'API sottostante. per esempio. ecco l'API REST per il sistema su cui sto lavorando attualmente:

(in pseudo-codice)

void doSomeCalculation(json : CalculationRequestObject);

Ho un metodo API e 1.000 di test, la maggior parte dei quali si trova attorno agli oggetti che lo supportano.

    
risposta data 01.06.2016 - 11:36
fonte
0

Perché vuoi seguire la DI in questo caso se non per scopi di test? Se non solo la tua API pubblica, ma anche i tuoi test sono davvero più puliti senza un parametro IFooBuilder (come hai scritto tu), non ha senso introdurre una tale classe. DI non è un fine in sé, dovrebbe aiutarti a rendere il tuo codice più testabile. Se non vuoi scrivere test automatici per quel particolare livello di astrazione, non applicare DI a quel livello.

Tuttavia, supponiamo per un momento che tu voglia applicare DI per consentire di creare test unitari per il costruttore FooDao in isolamento senza il processo di costruzione, ma non vuoi ancora un costruttore FooDao(IFooBuilder fooBuilder) nella tua API pubblica, solo un costruttore FooDao(IFooConnection fooConnection, IBarConnection barConnection) . Quindi fornire entrambi, il primo come "interno", il secondo come "pubblico":

public class FooDao : IFooDao
{
    private IFooBuilder fooBuilder;

    // only for testing purposes!    
    internal FooDao(IFooBuilder fooBuilder)
    {
        this.fooBuilder = fooBuilder;
    }

    // the public API
    public FooDao(IFooConnection fooConnection, IBarConnection barConnection)
    {
        this.fooBuilder = new FooBuilder(fooConnection, barConnection);
    }    

    public Foo GetFoo(int id)
    {
        return fooBuilder.Build(id);
    }
}

Ora puoi mantenere FooBuilder e IFooBuilder interni e applicare InternalsVisibleTo per renderli accessibili per test di unità.

    
risposta data 01.06.2016 - 12:29
fonte

Leggi altre domande sui tag