riguarda la logica di iniezione delle dipendenze del costruttore

3

Esiste un tipo di regola nella letteratura sull'iniezione delle dipendenze, affermando che dovremmo dichiarare tutti gli argomenti nel costruttore, al fine di ottenere un'iniezione del costruttore, che è migliore di altri approcci.

All'inizio, non c'è niente di sbagliato in questo.

Ma mi sono reso conto che questa "regola" si applica solo al regno di - diciamo - controller MVC. Poiché un controller viene generato per eseguire un determinato lavoro, non si discosta dal suo scopo e quindi dispone. In generale, si applica a un "sistema basato su richiesta". Ora, diciamo che abbiamo un blocco che riceve i dati, li trasforma e restituisce i dati trasformati. Questo blocco ha vari altri parametri che devono essere configurati, ad esempio IConditionStrategy (uguale a, GreaterThan ecc.).

Configuriamo il blocco ed eseguiamo l'applicazione. Quindi, vogliamo ottenere altri risultati e quindi cambiamo i parametri di quel blocco (un buon esempio di questo è simulink di matlab). Il blocco non dispone, esiste in uno spazio di lavoro. Ha svolto il suo lavoro (trasformando i dati) e attende nell'area di lavoro per eseguire nuovamente un altro lavoro. Sarebbe molto costoso (per non dire complicato) ricreare tutti i possibili blocchi dello spazio di lavoro. Ma quando iniziamo a cambiare i suoi parametri, che possono variare dai numeri alle varie strategie, dobbiamo fornire in codice dietro alcune implementazioni concrete a questo blocco che ha bisogno della sua interfaccia. In questo caso, come si adatterebbe l'iniezione della dipendenza del costruttore "standard"?

Sfortunatamente, dobbiamo liberarci del costruttore, perché dobbiamo modificare i parametri in fase di esecuzione. Ma poi di nuovo, se ci liberiamo del costruttore, non saremo in grado di garantire che un parametro sia sempre valido. Dovremmo fornire un tipo di implementazione predefinita? (Cadremo nella trappola del localizzatore di servizi!)

E un'altra cosa mi ha sempre infastidito. Capisco il pericolo di un null che appare in fase di esecuzione (quindi dovremmo avere un'iniezione del costruttore), ma cosa otteniamo effettivamente quando creiamo un oggetto e poi lo vincoliamo dal modificare le sue strategie interne, i parametri ecc. Dopo la sua creazione. Una risposta a questo potrebbe non essere necessario cambiarla affatto, basta eliminarla e crearne un'altra. Certo, ma non vedo come questa tecnica si adatti a domini diversi dai "sistemi basati su richieste", come le app web.

Immagina di avere una rete neurale con neuroni che hanno una strategia interna, ad esempio cambiano le loro funzioni di trasferimento in fase di esecuzione anche mentre è in corso l'identificazione o l'apprendimento. Non possiamo disporre un neurone e crearne un altro con la strategia interna desiderata solo per mantenere l'iniezione del costruttore. Ovviamente, in vari libri sulle iniezioni di dipendenza, sono menzionati altri tipi di iniezione, come l'iniezione di metodo, ma ho avuto la sensazione che l'iniezione del costruttore dovrebbe essere usata nell'80% delle occasioni secondo la letteratura. Date le varie applicazioni, la mia stima è che deve essere inferiore all'80%. Sto fraintendendo qualcosa? Gradirei molto le vostre opinioni in merito.

Ad esempio, fornisco il seguente frammento. L'oggetto deve essere modificato in fase di esecuzione, ma l'iniezione del costruttore lo vincola.

public class Transformation {
  private readonly IConditionStrategy _condition;
  private readonly ISource _input;

  public Transformation(IConditionStrategy condition, ISource input) {
    this._condition = condition;
    this._input = input;
  }

  public ISource Transform() {
    ISource source = new Source();

    foreach(var i in this._input) {
      if(this._condition.Check(i)){
        source.Add(i);
      }
    }

    return source;
  }
}

Nell'esempio seguente cambiamo i parametri in fase di esecuzione. Poiché non possiamo semplicemente disporre e ricreare un altro oggetto con i parametri desiderati, non usiamo la pratica classica dell'iniezione di dipendenza.

public class Transformation{
  public IConditionStrategy Condition{get;set;}
  public ISource Input{get;set;}

  public Transformation() {
    // we could provide some default 
    // implementations just to avoid null
  }

  public ISource Transform(){
    ISource source = new Source();

    foreach(var i in this._input){
      if(this._condition.Check(i)){
        source.Add(i);
      }
    }

    return source;
  }
}
    
posta Δημήτρης Κόκκινος 10.04.2018 - 20:45
fonte

4 risposte

2

Come @Mark ha notato che non esiste una regola di questo tipo che tutti gli argomenti debbano essere istanziati nel costruttore.

Given various applications, my estimate is that it has to be way lower than 80%

Penso che non sia giusto prendere decisioni di progettazione basate sui valori percentuali. Puoi usare il modo di iniettare dipendenze che si adattino ai requisiti del contesto attuale.

Nel tuo particolare esempio IConditionStrategy è una dipendenza che potrebbe cambiare durante il runtime - quindi perché non introdurla come dipendenza "esterna" e passarla al metodo come argomento?

public class Transformation
{
    public Transformation() { }

    public ISource Transform(IConditionStrategy condition, ISource input)
    {
        ISource source = new Source();

        foreach(var i in input){
             if (condition.Check(i))
             {
                 source.Add(i);
             }
        }

        return source;
    }
}

Nel costruttore puoi inserire condizioni di strategia predefinite che rimangono invariate durante la vita di trasformazione

public class Transformation
{
    private readonly IConditionStrategy _defaultCondition;

    public Transformation(IConditionStrategy defaultCondition) 
    { 
        _defaultCondition = defaultCondition;
    }

    public ISource Transform(ISource input)
    {
        return Transform(_defaultCondition, input);
    }

    public ISource Transform(IConditionStrategy condition, ISource input)
    {
        ISource source = new Source();

        foreach(var i in input){
             if (condition.Check(i))
             {
                 source.Add(i);
             }
        }

        return source;
    }
}
    
risposta data 11.04.2018 - 23:37
fonte
3

There is a kind of rule in dependency injection literature, stating that we should declare all arguments in the constructor

Dove? In quale letteratura?

Dati i tuoi esempi, se _input è, in effetti, input di runtime, perché non scrivi semplicemente la classe come segue?

public class Transformation
{
    private readonly IConditionStrategy condition;

    public Transformation(IConditionStrategy condition)
    {
        this.condition = condition;
    }

    public ISource Transform(ISource input)
    {
        ISource source = new Source();
        foreach(var i in input)
            if(input.Check(i))
                source.Add(i);

        return source;
    }
}
    
risposta data 10.04.2018 - 21:52
fonte
2

Penso che la tua domanda e le tue preoccupazioni siano basate su una confusione tra l'iniezione di dipendenza e l'immutabilità. Queste sono due preoccupazioni distinte.

  • Iniezione di dipendenza è l'idea che anziché gli oggetti che recuperano le loro dipendenze, vengono spinti verso l'oggetto dall'esterno. Ciò ha dei vantaggi, tra cui la facilità di test e un maggiore potenziale di riutilizzo.

  • Immutabilità è l'idea che gli oggetti, una volta creati, siano corretti. Il loro stato non può essere modificato.

Quando li metti insieme, questo porta all'iniezione del costruttore perché, per definizione, l'oggetto non può essere modificato dopo la costruzione. L'immutabilità è preferibile principalmente a causa della correttezza, specialmente quando il multi-threading è sempre più comune a causa dell'andamento dell'hardware (cioè l'appiattimento della curva Moore). Può anche avere importanti vantaggi in termini di prestazioni in un contesto multi-thread. Ciò è dovuto al fatto che un oggetto immutabile può essere tenuto nella cache di memoria di un thread indefinitamente senza alcuna necessità di sincronizzare il suo stato tra thread (perché non cambia mai). Quando lo stato di una cache di thread locale di un oggetto viene invalidato, il thread deve attendere che venga aggiornato prima che possa accedere nuovamente allo stato di tali oggetti. Mentre questo veloce nel tempo umano scala, è piuttosto lento nelle scale temporali del computer. Oltre a questo, alcune macchine virtuali moderne possono ottimizzare il codice a seconda degli oggetti immutabili ed eliminare completamente gli oggetti estraendo i valori e posizionandoli in pila. Non sono sicuro che questi tipi di ottimizzazioni esistano nel CLR oggi, ma mi aspetto che vengano aggiunti in futuro.

Quindi le preoccupazioni che avete riguardo alle prestazioni non sono completamente infondate, ma potreste scoprire che in pratica il costo dell'immutabilità è inferiore a quello che pensate. Potrebbero anche essere più veloci. Ad esempio, i garbage collector sono tipicamente generazionali. Senza entrare in tutti i dettagli: gli oggetti iniziano la vita in un'area in cui non vi è praticamente alcun costo per la raccolta se non rimangono a lungo.

Per quanto riguarda l'esempio specifico che offri, se fossi davvero preoccupato per le prestazioni, prenderei in considerazione un approccio Flyweight il che significa che crei un set permanente di oggetti Condition primitivi e poi li componi come necessario. È improbabile che il costo di creare un nuovo oggetto con riferimenti a questi oggetti sia un notevole fattore di trascinamento sulle prestazioni, ma è necessario eseguirlo per conoscerlo veramente.

    
risposta data 10.04.2018 - 23:47
fonte
-1

Il tuo esempio sarebbe meglio servito senza alcuna dipendenza. ad es.

ISource Transform(ISource input)

Tuttavia, sì hai ragione. a volte si ha una dipendenza, che è una vera dipendenza della classe, che normalmente dovrebbe essere iniettata naturalmente dal costruttore. Ma in un caso particolare è necessario cambiarlo dopo che l'oggetto è stato instanciato.

Se tu avessi usato Property Injection, non saresti stato costretto e potresti semplicemente impostarlo su un'altra cosa che dici! Ma in realtà no. Forse è necessario disporre di queste risorse prima di sostituirlo, ma forse è usato altrove, per quanto riguarda la sicurezza dei thread ecc.

L'iniezione del costruttore batte l'iniezione setter per un solo motivo

  1. Non devi etichettare i tuoi oggetti dicendo cose che dovrebbero essere iniettati

Puoi ancora passare null in come dipendenze, puoi comunque tenere traccia degli oggetti che passi e cambiarli dall'esterno dell'oggetto

Quando ti trovi di fronte al problema di voler cambiare una dipendenza, la sua in realtà non è un'iniezione del costruttore che ti impedisce ciò che hai scelto di iniettare.

Ad esempio, ho un repository di database e voglio essere in grado di passare a un altro database durante il runtime.

Normalmente ne dispongo uno e ne accendo un altro, passando la stringa di connessione come dipendenza.

Ma non c'è niente che mi impedisce di aggiungere

ChangeDatabase(string connstr)

Metodi. E 'solo che se lo faccio, ho bisogno di aggiungere un'intera serie di controlli per cattiva, chiusa o qualsiasi altra connessione e appropriata gestione degli errori sugli altri metodi.

Quando non ho bisogno di quell'iniezione delle dipendenze di funzionalità, e in particolare dell'iniezione del costruttore, mi consente di saltare questi controlli. Se l'oggetto esiste, dovrebbe avere una stringa di connessione.

Rende l'oggetto più semplice e fa fallire più velocemente se c'è un problema.

    
risposta data 10.04.2018 - 21:34
fonte

Leggi altre domande sui tag