web-dev-qa-db-fra.com

Dépendance circulaire dans l'injection de dépendance

nous sommes tout à fait nouveaux à DI et nous refactons notre application pour utiliser Prism/Unity. Nous sommes presque là mais sont restés coincés sur une dépendance circulaire. J'ai beaucoup lu, il y a beaucoup de questions similaires, mais je doute quant à ce qui serait une solution "correcte" dans notre situation.

L'application est similaire à certaines IDES. Nous avons un explorateur de projet, une recherche, des documents, ... Les documents sont en réalité des arbres avec des nœuds d'inodeViewModel. Notez que les échantillons de code sont des versions minimisées du code réel.

Nous avons un IDockingService qui gère l'outil et les éléments de document.

public interface IDockingService
{
    INodeViewModelNavigationService NodeViewModelNavigationService { get; }

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

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

Et nous avons un ISpecificationViewModelService qui gère tous les inodévismodels et a la connexion avec le modèle.

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

Nous avons deux exigences que le conflit.

  • Lorsqu'un nouveau nœud est créé quelque part, nous voulons y aller. Actuellement, nous passons IDockingService via l'injection du constructeur à la béton SpecificationViewModelService implémentation et plus bas à chaque NODEVIEWMODEL.
  • Certains outils dans le DockingService doivent savoir sur le ISpecificationViewModelService. Par exemple, l'explorateur doit afficher toutes les racines.

Etant donné que DockingItemService crée et gère les outils tels que l'explorateur qu'il a besoin du ISpecificationViewModelService. Mais le ISpecificationViewModelService nécessite le IDockingService pour pouvoir naviguer vers un nœud. Troubles circulaires.

Notre première tentative n'a pas eu cette question car nous laissons les éléments de l'outil pour être créés dans la racine de la composition, mais cela ne semblait pas correct. Instances que nous n'avons pas géré bien ensemble.

De lire, je comprends qu'une usine (ou une autre troisième classe) pourrait aider ici. Je ne vois pas encore à quel point. Je pense que je crois comprendre que le problème est que DockingService seulement nécessite SpecificationViewModelService pour créer les outils et vice versa. Une usine pourrait prendre ce problème circulaire, mais je ne suis pas sûr que ce soit une solution correcte. Le conteneur ne peut être utilisé que dans la racine de composition, mais ne serait-il pas facoré (ce qui nécessite le conteneur?) Ensuite, il suffit de cacher le conteneur et de reprendre son travail sous un nom différent?

Quel serait un moyen correct de gérer ce problème?

6
Jef Patat

Je vais ressentir de manière significative de votre situation. Je ne sais pas si ce sera la meilleure solution à votre problème. Ce sera A solution, mais cela peut simplement permettre une conception médiocre. Il est difficile de dire, mais la situation se présente certainement dans des conceptions raisonnables.

Il est assez courant que vous obtenez dans une situation qui construira X vous avez besoin Y et de construire Y vous avez besoin X. Bien sûr, cela ne peut pas être que conceptuellement construire X vous devez avoir complètement construit un Y et inversement comme il serait logiquement impossible de construire le système. Au lieu de cela, ce qui est vraiment destiné à construire un que vous avez simplement besoin d'un moyen de faire référence à l'autre. Il existe de nombreuses façons de faire référence à quelque chose qui n'a pas encore été créé. Par exemple, dans C, vous risquez de passer un pointeur à une mémoire allouée mais non initialisée, ou vous pouvez passer une chaîne qui sera utilisée pour rechercher l'objet créé dans un registre. Dans une langue comme Haskell ou Scala, une façon de résoudre ce problème est avec une évaluation paresseuse. Mise en œuvre, cela est plus ou moins équivalent à la transmission d'une fonction d'ordre supérieur avec un état mutable interne, ou de manière équivalente, un objet "usine" qui doit mémoter la résolution de dépendance. C'est l'approche que je vais suggérer ici. Je vais utiliser Autofac Terminologie et API ci-dessous, bien que l'idée soit facilement adaptable à d'autres cadres d'injection de dépendance.

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;
        }
    }
}

Ici IContainer est destiné à être le conteneur d'injection de dépendance et Resolve fait la recherche de dépendance. Je Fortement Recommander contre le récipient d'injection de dépendance aux objets, mais dans ce cas, je vis en visualisant Lazy dans le cadre du cadre d'injection de dépendance. En fait, vous devez vous assurer que votre cadre d'injection de dépendance ne fournit pas déjà de telles fonctionnalités. L'alternative serait de faire une usine pour chaque classe que vous souhaitez injecter paresseusement. Le code serait essentiellement identique, sauf que IContainer serait remplacé par des dépendances et au lieu d'utiliser Resolve pour créer l'objet que vous utiliseriez new. Alternativement, vous pourriez généraliser Lazy _ pour prendre un Action<T> Et simplement le mémoiser, puis ces objets d'usine seraient simplement des singletons que vous vous enregistrez dans la racine de composition comme: container.Register(new Lazy<Foo>(() => new Foo())); L'idéal La situation serait de enregistrer le générique ouvert type Lazy<>, de sorte qu'avec une inscription, vous pourriez gérer tous les cas. (J'ai vérifié que vous pouvez faire ce travail avec Autofac.)

Le résultat de ceci est que vous dépendriez de Lazy<IFoo> Au lieu de IFoo et vous accédez via la propriété Value (mais pas dans le constructeur!)

5

Si vous avez vraiment besoin, une autre solution serait de supprimer votre injection de constructeur, d'ajouter une injection de constructeur IServiceProvider et d'utiliser des activatorutibilités lorsque vous avez besoin du service d'accueil comme celui-ci:

var dockingService = (IDockingService)ActivatorUtilities.CreateInstance(this.serviceProvider, typeof(DockingService));

Bien sûr, ce n'est pas la manière préférée, et vous ne pouvez pas le tester correctement (comme vous insultirez un iDockingService concret), mais c'est une solution.

0
ozba