Bilanciamento dell'iniezione delle dipendenze con la progettazione di API pubbliche


13

Ho riflettuto su come bilanciare la progettazione testabile usando l'iniezione delle dipendenze fornendo una semplice API pubblica fissa. Il mio dilemma è: la gente vorrebbe fare qualcosa di simile var server = new Server(){ ... }e non doversi preoccupare di creare le molte dipendenze e il grafico delle dipendenze che si Server(,,,,,,)possono avere. Durante lo sviluppo, non mi preoccupo troppo, poiché utilizzo un framework IoC / DI per gestire tutto ciò (non sto usando gli aspetti di gestione del ciclo di vita di alcun contenitore, il che complicherebbe ulteriormente le cose).

Ora è improbabile che le dipendenze vengano nuovamente implementate. La componentizzazione in questo caso è quasi puramente per testabilità (e design decente!) Piuttosto che creare cuciture per l'estensione, ecc. Le persone vorranno il 99,999% delle volte utilizzare una configurazione predefinita. Così. Potrei codificare le dipendenze. Non voglio farlo, perdiamo i nostri test! Potrei fornire a un costruttore predefinito dipendenze hardcoded e uno che accetta dipendenze. È ... disordinato e probabilmente confuso, ma praticabile. Potrei rendere la dipendenza che riceve il costruttore interno e rendere la mia unità test un assembly amico (supponendo C #), che riordina l'API pubblica ma lascia una cattiva trappola nascosta in agguato per la manutenzione. Avere due costruttori che sono implicitamente connessi piuttosto che esplicitamente sarebbe un cattivo progetto in generale nel mio libro.

Al momento è il minimo male che mi viene in mente. Opinioni? Saggezza?

Risposte:


11

L'iniezione di dipendenza è un modello potente se usata bene, ma troppo spesso i suoi professionisti diventano dipendenti da una struttura esterna. L'API risultante è piuttosto dolorosa per quelli di noi che non vogliono legare liberamente la nostra app con il nastro adesivo XML. Non dimenticare di Plain Old Objects (POO).

Innanzitutto considera come ti aspetteresti che qualcuno usi la tua API - senza il framework coinvolto.

  • Come si crea un'istanza del server?
  • Come si estende il server?
  • Cosa vuoi esporre nell'API esterna?
  • Cosa vuoi nascondere nell'API esterna?

Mi piace l'idea di fornire implementazioni predefinite per i tuoi componenti e il fatto che tu abbia questi componenti. Il seguente approccio è una pratica OO perfettamente valida e decente:

public Server() 
    : this(new HttpListener(80), new HttpResponder())
{}

public Server(Listener listener, Responder responder)
{
    // ...
}

Finché documentate quali sono le implementazioni predefinite per i componenti nei documenti API e fornite un costruttore che esegue tutte le impostazioni, dovreste andare bene. I costruttori a cascata non sono fragili fintanto che il codice di configurazione avviene in un costruttore principale.

In sostanza, tutti i costruttori di fronte al pubblico avrebbero le diverse combinazioni di componenti che si desidera esporre. Internamente, popolare le impostazioni predefinite e rinvia a un costruttore privato che completa l'installazione. In sostanza, questo ti fornisce un po 'di SECCO ed è abbastanza facile da testare.

Se è necessario fornire l' friendaccesso ai test per configurare l'oggetto Server per isolarlo per i test, procedere. Non farà parte dell'API pubblica, che è ciò con cui vuoi stare attento.

Sii gentile con i tuoi consumatori API e non richiede un framework IoC / DI per usare la tua libreria. Per avere un'idea del dolore che stai causando, assicurati che i tuoi test unitari non facciano affidamento nemmeno sul framework IoC / DI. (È una pipì personale per animali domestici, ma non appena si introduce il framework non è più un test unitario, ma diventa un test di integrazione).


Sono d'accordo, e certamente non sono un fan della zuppa di configurazione IoC - la configurazione XML non è qualcosa che uso per quanto riguarda l'IoC. Certamente richiedere l'utilizzo dell'IoC è esattamente ciò che stavo evitando: è il tipo di ipotesi che un progettista di API pubbliche non può mai fare. Grazie per il commento, è sulla falsariga che stavo pensando ed è rassicurante avere un controllo di sanità mentale di tanto in tanto!
Kolektiv,

-1 Questa risposta dice sostanzialmente "non usare l'iniezione di dipendenza" e contiene ipotesi imprecise. Nel tuo esempio se il HttpListenercostruttore cambia, anche la Serverclasse deve cambiare. Sono accoppiati. Una soluzione migliore è ciò che @Winston Ewert dice di seguito, ovvero fornire una classe predefinita con dipendenze pre-specificate, al di fuori dell'API della libreria. Quindi il programmatore non è costretto a impostare tutte le dipendenze da quando prendi la decisione per lui, ma ha ancora la flessibilità di cambiarle in seguito. Questo è un grosso problema quando si hanno dipendenze di terze parti.
M. Dudley,

FWIW l'esempio fornito si chiama "iniezione bastarda". stackoverflow.com/q/2045904/111327 stackoverflow.com/q/6733667/111327
M. Dudley

1
@MichaelDudley, hai letto la domanda del PO. Tutte le "ipotesi" erano basate sulle informazioni fornite dal PO. Per un certo periodo, l '"iniezione bastarda" come la chiamavi tu, era il formato di iniezione della dipendenza preferito, in particolare per i sistemi le cui composizioni non cambiano durante il runtime. I setter e i getter sono anche un'altra forma di iniezione di dipendenza. Ho semplicemente usato esempi basati su ciò che l'OP ha fornito.
Berin Loritsch,

4

In Java in questi casi è normale avere una configurazione "predefinita" costruita da una fabbrica, mantenuta indipendente dall'effettivo server "correttamente". Quindi, il tuo utente scrive:

var server = DefaultServer.create();

mentre il Servercostruttore accetta ancora tutte le sue dipendenze e può essere utilizzato per una personalizzazione profonda.


+1, SRP. La responsabilità di uno studio dentistico non è quella di costruire uno studio dentistico. La responsabilità di un server non è quella di costruire un server.
R. Schmitz,

2

Che ne dici di fornire una sottoclasse che fornisce "api pubblica"

class StandardServer : Server
{
    public StandardServer():
        this( depends1, depends2, depends3)
    {
    }
}

L'utente può new StandardServer()ed essere sulla buona strada. Possono inoltre utilizzare la classe base Server se desiderano un maggiore controllo sul funzionamento del server. Questo è più o meno l'approccio che uso. (Non uso un framework in quanto non ho ancora visto il punto.)

Questo espone ancora l'api interno, ma penso che dovresti. Hai suddiviso i tuoi oggetti in diversi componenti utili che dovrebbero funzionare in modo indipendente. Non si sa quando una terza parte potrebbe voler utilizzare uno dei componenti in isolamento.


1

Potrei rendere la dipendenza che riceve il costruttore interno e rendere la mia unità test un assembly amico (supponendo C #), che riordina l'API pubblica ma lascia una cattiva trappola nascosta in agguato per la manutenzione.

Non mi sembra una "cattiva trappola nascosta". Il costruttore pubblico dovrebbe semplicemente chiamare quello interno con le dipendenze "predefinite". Finché è chiaro che il costruttore pubblico non dovrebbe essere modificato, tutto dovrebbe andare bene.


0

Sono totalmente d'accordo con la tua opinione. Non vogliamo inquinare il limite di utilizzo dei componenti solo ai fini del test unitario. Questo è l'intero problema della soluzione di test basata su DI.

Suggerirei di usare il modello InjectableFactory / InjectableInstance . InjectableInstance sono semplici classi di utilità riutilizzabili che sono detentori di valori mutabili . L'interfaccia del componente contiene un riferimento a questo detentore del valore che è stato inizializzato con un'implementazione predefinita. L'interfaccia fornirà un metodo singleton get () che delegano al detentore del valore. Il client componente chiamerà il metodo get () anziché new per ottenere un'istanza di implementazione per evitare la dipendenza diretta. Durante il tempo di test, possiamo facoltativamente sostituire l'implementazione predefinita con una simulazione. Di seguito userò una TimeProvider diclasse di esempio che è un'astrazione diDate () in modo da consentire l'iniezione di test unitari e la simulazione di momenti diversi. Mi dispiace userò java qui come ho più familiarità con java. C # dovrebbe essere simile.

public interface TimeProvider {
  // A mutable value holder with default implementation
  InjectableInstance<TimeProvider> instance = InjectableInstance.of(Impl.class);  
  static TimeProvider get() { return instance.get(); }  // Singleton method.

  class Impl implements TimeProvider {        // Default implementation                                    
    @Override public Date getDate() { return new Date(); }
    @Override public long getTimeMillis() { return System.currentTimeMillis(); }
  }

  class Mock implements TimeProvider {   // Mock implemention
    @Setter @Getter long timeMillis = System.currentTimeMillis();
    @Override public Date getDate() { return new Date(timeMillis); }
    public void add(long offset) { timeMillis += offset; }
  }

  Date getDate();
  long getTimeMillis();
}

// The client of TimeProvider
Order order = new Order().setCreationDate(TimeProvider.get().getDate()));

// In the unit testing
TimeProvider.Mock timeMock = new TimeProvider.Mock();
TimeProvider.instance.setInstance(timeMock);  // Inject mock implementation

InjectableInstance è abbastanza facile da implementare, ho un'implementazione di riferimento in Java. Per i dettagli, consultare il mio post sul blog Dipendenza indiretta con Injectable Factory


Quindi ... sostituisci l'iniezione di dipendenza con un singleton mutabile? Sembra orribile, come faresti test indipendenti? Aprite un nuovo processo per ogni test o li forzate in una sequenza specifica? Java non è il mio punto forte, ho frainteso qualcosa?
nvoigt,

Nel progetto Java maven, l'istanza condivisa non è un problema poiché il motore junit eseguirà ogni test nel caricatore di classi diverse per impostazione predefinita, tuttavia riscontro questo problema in alcuni IDE in quanto potrebbe riutilizzare lo stesso caricatore di classi. Per risolvere questo problema, la classe fornisce un metodo di ripristino da chiamare per ripristinare lo stato originale dopo ogni test. In pratica, è una situazione rara che test diversi vogliano usare simulazioni diverse. Abbiamo applicato questo modello per progetti su larga scala per anni, tutto ha funzionato senza intoppi, abbiamo goduto di codice leggibile e pulito senza sacrificare portabilità e testabilità.
Jianwu Chen,
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.