Esiste un’alternativa all’iniezione di bastardo? (Iniezione da povero di AKA tramite costruttore predefinito)

Il più delle volte sono tentato di usare “iniezione bastarda” in alcuni casi. Quando ho un costruttore “corretto” per l’iniezione delle dipendenze:

public class ThingMaker { ... public ThingMaker(IThingSource source){ _source = source; } 

Ma poi, per le classi che intendo utilizzare come API pubbliche (classi che altri team di sviluppo consumeranno), non potrò mai trovare un’opzione migliore rispetto a scrivere un costruttore “bastardo” predefinito con la dipendenza più probabilmente necessaria:

  public ThingMaker() : this(new DefaultThingSource()) {} ... } 

Lo svantaggio ovvio qui è che questo crea una dipendenza statica su DefaultThingSource; idealmente, non ci sarebbe una tale dipendenza, e il consumatore inietterebbe sempre qualsiasi IThingSource che desideravano. Tuttavia, questo è troppo difficile da usare; i consumatori vogliono creare un ThingMaker e iniziare a lavorare con Things, poi, mesi dopo, iniettano qualcos’altro quando se ne presenta la necessità. Questo lascia solo alcune opzioni a mio parere:

  1. Ometti il ​​costruttore bastardo; costringere il consumatore di ThingMaker a comprendere IThingSource, capire come ThingMaker interagisce con IThingSource, trovare o scrivere una class concreta e quindi iniettare un’istanza nella chiamata del costruttore.
  2. Ometti il ​​costruttore bastard e fornisci un factory, un container o un’altra class / metodo bootstrap; in qualche modo fa capire al consumatore che non hanno bisogno di scrivere la propria IThingSource; costringere il consumatore di ThingMaker a trovare e comprendere la fabbrica o il bootstrapper e usarlo.
  3. Mantieni il costruttore bastard, consentendo al consumatore di “rinnovare” un object ed eseguirlo con esso, e gestire la dipendenza statica opzionale su DefaultThingSource.

Il ragazzo, sicuramente # 3 sembra attraente. C’è un’altra opzione migliore? # 1 o # 2 non sembrano valere la pena.

Per quanto ho capito, questa domanda si riferisce a come esporre un’API liberamente accoppiata con alcune impostazioni predefinite appropriate. In questo caso, potresti avere un buon Local Default , nel qual caso la dipendenza può essere considerata opzionale. Un modo per gestire le dipendenze opzionali consiste nell’utilizzare l’ Iniezione di proprietà anziché l’ Iniezione del Costruttore : in realtà, si tratta di uno scenario poster per l’Iniezione di Proprietà.

Tuttavia, il vero pericolo di Bastard Injection è quando il default è un Default straniero , perché ciò significherebbe che il costruttore predefinito trascina lungo un accoppiamento indesiderato all’assembly che implementa l’impostazione predefinita. A quanto ho capito questa domanda, tuttavia, il valore predefinito previsto proviene dallo stesso assembly, nel qual caso non vedo alcun particolare pericolo.

In ogni caso si potrebbe anche considerare una facciata come descritto in una delle mie risposte precedenti: Libreria “friendly” Injection Dependency (DI)

A proposito, la terminologia utilizzata qui è basata sul linguaggio del pattern del mio libro .

Il mio trade-off è un giro su @BrokenGlass:

1) Unico costruttore è un costruttore parametrizzato

2) Utilizzare il metodo factory per creare un ThingMaker e passare in quella fonte predefinita.

 public class ThingMaker { public ThingMaker(IThingSource source){ _source = source; } public static ThingMaker CreateDefault() { return new ThingMaker(new DefaultThingSource()); } } 

Ovviamente questo non elimina la tua dipendenza, ma mi rende più chiaro che questo object ha dipendenze a cui un chiamante può immergersi in profondità se gli interessa. È ansible rendere ancora più esplicito quel metodo di fabbrica, se lo si desidera (CreateThingMakerWithDefaultThingSource) se questo aiuta a comprendere. Preferisco questo a scavalcare il metodo di produzione IThingSource poiché continua a favorire la composizione. È anche ansible aggiungere un nuovo metodo factory quando DefaultThingSource è obsoleto e avere un modo chiaro per trovare tutto il codice utilizzando DefaultThingSource e contrassegnarlo per l’aggiornamento.

Hai coperto le possibilità nella tua domanda. Classe di fabbrica altrove per praticità o praticità all’interno della class stessa. L’unica altra opzione poco attraente sarebbe basata sulla riflessione, nascondendo ulteriormente la dipendenza.

Un’alternativa è avere un metodo factory CreateThingSource() nella class ThingMaker che crea la dipendenza per te.

Per testare o se è necessario un altro tipo di IThingSource è necessario creare una sottoclass di ThingMaker e sovrascrivere CreateThingSource() per restituire il tipo concreto desiderato. Ovviamente questo approccio vale la pena se è principalmente necessario essere in grado di iniettare la dipendenza per il test, ma per la maggior parte / tutti gli altri scopi non è necessario un altro IThingSource

Io voto per # 3. Renderà la tua vita – e la vita di altri sviluppatori – più facile.

Se si dispone di una dipendenza “predefinita”, nota anche come Iniezione di dipendenza di Poor Man, è necessario inizializzare e “colbind” la dipendenza da qualche parte.

Terrò i due costruttori ma ho una fabbrica solo per l’inizializzazione.

 public class ThingMaker { private IThingSource _source; public ThingMaker(IThingSource source) { _source = source; } public ThingMaker() : this(ThingFactory.Current.CreateThingSource()) { } } 

Ora in fabbrica crea l’istanza predefinita e consenti il ​​metodo di essere sovrascritto:

 public class ThingFactory { public virtual IThingSource CreateThingSource() { return new DefaultThingSource(); } } 

Aggiornare:

Perché usare due costruttori: due costruttori mostrano chiaramente come si intende utilizzare la class. Gli stati del costruttore senza parametri: basta creare un’istanza e la class eseguirà tutte le sue responsabilità. Ora il secondo costruttore afferma che la class dipende da IThingSource e fornisce un modo di utilizzare un’implementazione diversa da quella predefinita.

Perché usare una fabbrica: 1- Disciplina: la creazione di nuove istanze non dovrebbe far parte delle responsabilità di questa class, una class di fabbrica è più appropriata. 2- DRY: immagina che nella stessa API altre classi dipendano anche da IThingSource e facciano lo stesso. Esegui l’override quando il metodo factory restituisce IThingSource e tutte le classi dell’API iniziano automaticamente a utilizzare la nuova istanza.

Non vedo alcun problema nell’accoppiamento di ThingMaker a un’implementazione predefinita di IThingSource, purché questa implementazione abbia senso per l’API nel suo insieme e anche tu fornisca dei modi per sovrascrivere questa dipendenza per scopi di test e di estensione.

Non sei soddisfatto dell’impurità OO di questa dipendenza, ma non dici veramente che guai alla fine causi.

  • ThingMaker utilizza DefaultThingSource in modo non conforms a IThingSource? No.
  • Potrebbe venire un momento in cui saresti costretto a ritirare il costruttore senza parametri? Dal momento che è ansible fornire un’implementazione predefinita in questo momento, improbabile.

Penso che il problema più grande qui sia la scelta del nome, non se usare la tecnica.

Gli esempi solitamente correlati a questo stile di iniezione sono spesso estremamente semplicistici: “nel costruttore predefinito per la class B , chiama un costruttore sovraccarico con new A() e mettiti in cammino!”

La realtà è che le dipendenze sono spesso estremamente complesse da build. Ad esempio, cosa succede se B bisogno di una dipendenza non di class come una connessione al database o l’impostazione dell’applicazione? Quindi si associa la class B allo spazio System.Configuration nomi System.Configuration , aumentandone la complessità e l’accoppiamento, mentre si riduce la coerenza, tutto per codificare i dettagli che potrebbero semplicemente essere esternalizzati omettendo il costruttore predefinito.

Questo stile di iniezione comunica al lettore che hai riconosciuto i vantaggi del design disaccoppiato ma non sei disposto a impegnarti. Sappiamo tutti che quando qualcuno vede quel costruttore di succosa, facile, a basso coefficiente di frizione, lo chiameranno indipendentemente da quanto rigido possa rendere il loro programma da quel momento in poi. Non possono comprendere la struttura del loro programma senza leggere il codice sorgente per quel costruttore predefinito, che non è un’opzione quando si distribuiscono gli assembly. È ansible documentare le convenzioni del nome della stringa di connessione e della chiave delle impostazioni delle app, ma a quel punto il codice non regge da solo e si impone allo sviluppatore di cercare il giusto incantesimo.

Ottimizzare il codice in modo che quelli che lo scrivono possano cavarsela senza capire quello che stanno dicendo è una canzone di sirene, un anti-modello che alla fine porta a più tempo perso nel dipanare la magia del tempo risparmiato nello sforzo iniziale. O disaccoppiare o non fare; tenere un piede in ogni schema diminuisce la concentrazione di entrambi.

Per quello che vale, tutto il codice standard che ho visto in Java lo fa in questo modo:

 public class ThingMaker { private IThingSource iThingSource; public ThingMaker() { iThingSource = createIThingSource(); } public virtual IThingSource createIThingSource() { return new DefaultThingSource(); } } 

Chiunque non desideri un object DefaultThingSource può sovrascrivere createIThingSource . (Se ansible, la chiamata a createIThingSource sarebbe diversa da quella del costruttore.) C # non incoraggia l’override come fa Java, e potrebbe non essere così ovvio come sarebbe in Java che gli utenti possono e forse dovrebbero fornire la propria IThingSource implementazione. (Né altrettanto ovvio come fornirlo.) La mia ipotesi è che il n. 3 sia la strada da percorrere, ma ho pensato di menzionarlo.

Solo un’idea – forse un po ‘più elegante ma purtroppo non si sbarazza della dipendenza:

  • rimuovere il “costruttore bastardo”
  • nel costruttore standard si imposta il parametro source default su null
  • quindi si controlla che la sorgente sia nullo e se questo è il caso, si assegna a “new DefaultThingSource ()” otherweise qualsiasi cosa l’utente inietta

Avere un factory interno (interno alla libreria) che associa DefaultThingSource a IThingSource, che viene chiamato dal costruttore predefinito.

Ciò consente di “rinnovare” la class ThingMaker senza parametri o alcuna conoscenza di IThingSource e senza una dipendenza diretta su DefaultThingSource.

Per le API veramente pubbliche, generalmente gestisco questo utilizzando un approccio in due parti:

  1. Creare un helper all’interno dell’API per consentire a un utente dell’API di registrare implementazioni dell’interfaccia “predefinita” dall’API con il proprio contenitore IoC preferito.
  2. Se è preferibile consentire al consumer dell’API di utilizzare l’API senza il proprio contenitore IoC, ospitare un contenitore facoltativo all’interno dell’API che contiene le stesse implementazioni “predefinite”.

La parte davvero difficile qui è decidere quando triggersre il contenitore n. 2 e l’approccio di scelta migliore dipenderà in larga misura dai consumatori di API previsti.

Supporto l’opzione n. 1, con un’estensione: rendere DefaultThingSource una class pubblica . Il testo sopra riportato implica che DefaultThingSource verrà nascosto ai consumatori pubblici dell’API, ma poiché comprendo la tua situazione non c’è motivo di non esporre l’impostazione predefinita. Inoltre, è ansible documentare facilmente che al di fuori di circostanze particolari, un new DefaultThingSource() può sempre essere passato a ThingMaker .