Dipendenza circolare nell'integrazione delle dipendenze

3

siamo abbastanza nuovi in DI e stiamo effettuando il refactoring della nostra applicazione per utilizzare Prism / Unity. Siamo quasi arrivati ma siamo rimasti bloccati su una dipendenza circolare. Ho letto molto, ci sono un sacco di domande simili ma sono in dubbio su quale sarebbe una soluzione 'corretta' nella nostra situazione.

L'applicazione è simile ad alcuni IDE. Abbiamo un progetto di esplorazione, ricerca, documenti, ... I documenti sono in realtà visualizzazioni ad albero con nodi di INodeViewModel. Nota che gli esempi di codice sono versioni ridotte al minimo del codice reale.

Abbiamo un IDockingService che gestisce gli strumenti e gli elementi del documento.

public interface IDockingService
{
    INodeViewModelNavigationService NodeViewModelNavigationService { get; }

    ExplorerViewModel ExplorerViewModel { get; }
    SearchViewModel SearchViewModel { get; }

    ReadOnlyCollection<ToolItemViewModel> ToolItemViewModels { get; }
    ReadOnlyCollection<DocumentItemViewModel> DocumentItemViewModels { get; }
}

E abbiamo un ISpecificationViewModelService che gestisce tutti gli INodeViewModels e ha la connessione con il modello.

public interface ISpecificationViewModelService
{
    INodeViewModel RootX { get; }
    INodeViewModel RootY { get; }
    INodeViewModel RootZ { get; }
}

Abbiamo due requisiti in conflitto.

  • Quando un nuovo nodo viene creato da qualche parte, vogliamo navigare verso di esso. Attualmente passiamo IDockingService tramite l'iniezione del costruttore a l'implementazione concreta SpecificationViewModelService e più avanti in ogni NodeViewModel.
  • Alcuni strumenti in DockingService devono conoscere il ISpecificationViewModelService . Ad esempio, l'Explorer deve mostrare tutte le radici.

Poiché DockingItemService crea e gestisce i toolitems come Explorer ha bisogno di ISpecificationViewModelService . Ma ISpecificationViewModelService ha bisogno del IDockingService per essere in grado di navigare verso un nodo. Problemi circolari.

Il nostro primo tentativo non ha avuto questo problema perché abbiamo lasciato che gli elementi dello strumento fossero creati nella radice della composizione ma ciò non sembrava corretto. Istanze non gestite bene insieme.

Da leggere in giro capisco che una fabbrica (o un'altra terza classe) potrebbe aiutare qui. Non vedo ancora. Penso di capire che il problema è che DockingService ha solo bisogno di SpecificationViewModelService per creare i toolitems e viceversa. Una fabbrica potrebbe portare via questo problema circolare, ma non sono sicuro che sia una soluzione corretta. Il contenitore può essere usato solo nella radice della composizione ma non dovrebbe fare una facory (che ha bisogno del contenitore?), Quindi nascondere il contenitore e assumere il suo lavoro con un nome diverso?

Quale sarebbe un modo corretto per gestire questo problema?

    
posta Jef Patat 13.06.2017 - 10:35
fonte

1 risposta

3

Ho intenzione di astrarre significativamente dalla tua situazione. Non sono sicuro se questa sarà la migliore soluzione al tuo problema. Sarà una soluzione a , ma potrebbe semplicemente consentire una progettazione scadente. È difficile da dire, ma certamente la situazione si presenta in termini ragionevoli.

È abbastanza comune che ti trovi in una situazione in cui costruire X hai bisogno di Y , e per costruire Y hai bisogno di X . Naturalmente, non può essere che concettualmente costruire X è necessario aver completamente creato un Y e viceversa, altrimenti sarebbe logicamente impossibile costruire il sistema. Invece, ciò che è veramente inteso è costruirne uno, è semplicemente necessario un modo di riferirsi all'altro. Ci sono molti modi di riferirsi a qualcosa che non è stato ancora creato. Ad esempio, in C è possibile passare un puntatore alla memoria allocata ma non inizializzata oppure è possibile passare una stringa che verrà utilizzata per cercare l'oggetto creato in un registro. In un linguaggio come Haskell o Scala, un modo per risolvere questo è con la valutazione pigra. Per quanto riguarda l'implementazione, questo è più o meno equivalente a passare attorno a una funzione di ordine superiore con uno stato interno mutabile o, equivalentemente, un oggetto "fabbrica" che memorizzerà la risoluzione delle dipendenze. Questo è l'approccio che ho intenzione di suggerire qui. Userò la terminologia e l'API Autofac qui sotto, anche se l'idea dovrebbe essere facilmente adattabile ad altri framework di iniezione delle dipendenze.

class Lazy<T> where T : class {
    private T _cached = null;
    private IContainer _container;
    public Lazy(Func<IContainer> container) {
        _container = container();
    }
    public T Value {
        get {
            if(_cached != null) return _cached;
            _cached = _container.Resolve<T>();
            _container = null;
            return _cached;
        }
    }
}

Qui IContainer è inteso come contenitore di iniezione delle dipendenze e Resolve esegue la ricerca delle dipendenze. I consiglio vivamente di non passare il contenitore di dipendenze dell'iniezione agli oggetti, ma in questo caso sto visualizzando Lazy come parte del framework di iniezione delle dipendenze. In effetti, dovresti assicurarti che la tua infrastruttura per le dipendenze non fornisca già tale funzionalità. L'alternativa sarebbe quella di creare una fabbrica per ogni classe che si desidera iniettare pigramente. Il codice sarebbe fondamentalmente identico, ad eccezione del fatto che IContainer verrà sostituito da qualsiasi dipendenza e invece di utilizzare Resolve per creare l'oggetto che utilizzeresti new . In alternativa, puoi generalizzare Lazy per prendere un Action<T> e solo memorizzarlo e quindi questi oggetti di fabbrica sarebbero solo singoletti registrati nella radice di composizione come: container.Register(new Lazy<Foo>(() => new Foo())); La situazione ideale sarebbe register il tipo generico aperto Lazy<> , in modo che con una registrazione puoi gestire tutti i casi . (Ho verificato che puoi farlo funzionare con Autofac.)

Il risultato è che si dipenderà da Lazy<IFoo> anziché IFoo e si accederà tramite la proprietà Value (ma non nel costruttore!)

    
risposta data 13.06.2017 - 22:13
fonte