Question Injection de dépendances et AppSettings


Disons que je définis une classe d'implémentation de navigateur pour mon application:

class InternetExplorerBrowser : IBrowser {
    private readonly string executablePath = @"C:\Program Files\...\...\ie.exe";

    ...code that uses executablePath
}

Cela pourrait à première vue ressembler à une bonne idée, comme le executablePath les données sont proches du code qui les utilisera.

Le problème vient quand j'essaie d'exécuter cette même application sur mon autre ordinateur, qui a un système d'exploitation en langue étrangère: executablePath aura une valeur différente.

Je pourrais résoudre ce problème via une classe singleton AppSettings (ou l'un de ses équivalents), mais personne ne sait que ma classe dépend en fait de cette classe AppSettings (qui va à l'encontre des idéologies DI). Cela pourrait également poser problème pour les tests unitaires.

Je pourrais résoudre les deux problèmes en ayant executablePath être passé par le constructeur:

class InternetExplorerBrowser : IBrowser {
    private readonly string executablePath;

    public InternetExplorerBrowser(string executablePath) {
        this.executablePath = executablePath;
    }
}

mais cela soulèvera des problèmes dans mon Composition Root (la méthode de démarrage qui fera tout le câblage nécessaire aux classes), car alors cette méthode doit savoir à la fois comment brancher les câbles et connaître toutes ces données de paramètres:

class CompositionRoot {
    public void Run() {
        ClassA classA = new ClassA();

        string ieSetting1 = "C:\asdapo\poka\poskdaposka.exe";
        string ieSetting2 = "IE_SETTING_ABC";
        string ieSetting3 = "lol.bmp";

        ClassB classB = new ClassB(ieSetting1);
        ClassC classC = new ClassC(B, ieSetting2, ieSetting3);

        ...
    }
}

ce qui se transformera facilement en gros désordre.

Je pourrais contourner ce problème en passant plutôt une interface du formulaire

interface IAppSettings {
    object GetData(string name);
}

à toutes les classes qui ont besoin d'une sorte de paramètres. Ensuite, je pourrais soit l'implémenter soit comme une classe régulière avec tous les paramètres incorporés, soit comme une classe qui lit les données d'un fichier XML, quelque chose comme ça. Si c'est le cas, devrais-je avoir une instance de classe AppSettings générale pour l'ensemble du système ou une classe AppSettings associée à chaque classe qui en aurait besoin? Cela semble certainement un peu exagéré. En outre, le fait de disposer de toutes les configurations d’applications au même endroit facilite l’analyse et la visualisation de toutes les modifications que je dois effectuer lorsque je souhaite transférer le programme sur différentes plates-formes.

Quelle pourrait être la meilleure façon d'aborder cette situation commune?

Modifier:

Et qu'en est-il de l'utilisation d'un IAppSettings avec tous ses paramètres en dur?

interface IAppSettings {
    string IE_ExecutablePath { get; }
    int IE_Version { get; }
    ...
}

Cela permettrait une sécurité de type à la compilation. Si je voyais l'interface / les classes concrètes croître trop, je pourrais créer d'autres interfaces plus petites de la forme IMyClassXAppSettings. Serait-ce un fardeau trop lourd à supporter dans les projets de grande envergure?

J'ai aussi lu à propos de l'AOP et de ses avantages en matière de préoccupations transversales (je suppose que c'est une question). Ne pourrait-il pas aussi proposer des solutions à ce problème? Peut-être que des variables comme:

class InternetExplorerBrowser : IBrowser {
    [AppSetting] string executablePath;
    [AppSetting] int ieVersion;

    ...code that uses executablePath
}

Ensuite, lors de la compilation du projet, nous aurions également une sécurité de compilation (le fait que le compilateur vérifie que nous avons réellement implémenté du code qui incorporerait des données. Cela relierait bien sûr notre API à cet aspect particulier.


11
2017-08-27 19:10


origine


Réponses:


Les classes individuelles doivent être aussi libres d'infrastructure que possible - se construit comme IAppSettings, IMyClassXAppSettings, et [AppSetting] saigner des détails de composition à des classes qui, à leur plus simple, ne dépendent réellement que de valeurs brutes telles que executablePath. L'art de l'injection de dépendance est dans la prise en compte des préoccupations.

J'ai implémenté ce modèle exact en utilisant Autofac, qui a des modules similaires à Ninject et devrait aboutir à un code similaire (je me rends compte que la question ne mentionne pas Ninject, mais le fait dans un commentaire).

Les modules organisent les applications par sous-système. Un module expose les éléments configurables d'un sous-système:

public class BrowserModule : Module
{
    private readonly string _executablePath;

    public BrowserModule(string executablePath)
    {
        _executablePath = executablePath;
    }

    public override void Load(ContainerBuilder builder)
    {
        builder
            .Register(c => new InternetExplorerBrowser(_executablePath))
            .As<IBrowser>()
            .InstancePerDependency();
    }
}

Cela laisse la racine de la composition avec le même problème: elle doit fournir la valeur de executablePath. Pour éviter la soupe de configuration, nous pouvons écrire un module autonome qui lit les paramètres de configuration et les transmet à BrowserModule:

public class ConfiguredBrowserModule : Module
{
    public override void Load(ContainerBuilder builder)
    {
        var executablePath = ConfigurationManager.AppSettings["ExecutablePath"];

        builder.RegisterModule(new BrowserModule(executablePath));
    }
}

Vous pourriez envisager d'utiliser une section de configuration personnalisée au lieu de AppSettings; les modifications seraient localisées sur le module:

public class BrowserSection : ConfigurationSection
{
    [ConfigurationProperty("executablePath")]
    public string ExecutablePath
    {
        get { return (string) this["executablePath"]; }
        set { this["executablePath"] = value; }
    }
}

public class ConfiguredBrowserModule : Module
{
    public override void Load(ContainerBuilder builder)
    {
        var section = (BrowserSection) ConfigurationManager.GetSection("myApp.browser");

        if(section == null)
        {
            section = new BrowserSection();
        }

        builder.RegisterModule(new BrowserModule(section.ExecutablePath));
    }
}

C'est un motif intéressant car chaque sous-système a une configuration indépendante qui est lue à un seul endroit. Le seul avantage ici est une intention plus évidente. Pour nonstring valeurs ou schémas complexes, cependant, nous pouvons laisser System.Configuration faire le levage lourd.


13
2017-08-28 20:40



J'irais avec la dernière option - passer un objet conforme à la IAppSettings interface. En fait, j'ai récemment effectué ce refactor au travail afin de trier certains tests unitaires et cela a bien fonctionné. Cependant, il y avait peu de classes dépendant des paramètres de ce projet.

Je créerai une instance unique de la classe de paramètres et la transmettrai à tout ce qui en dépend. Je ne vois aucun problème fondamental avec cela.

Cependant, je pense que vous avez déjà pensé à cela et vu à quel point cela peut être pénible si vous avez beaucoup de classes dépendant des paramètres.

Si cela vous pose un problème, vous pouvez le contourner en utilisant un cadre d'injection de dépendance tel que ninject (désolé si vous êtes déjà au courant de projets comme ninject - cela peut paraître un peu condescendant - si vous n'êtes pas familier, le pourquoi utiliser ninject les sections sur github sont un bon endroit pour apprendre).

En utilisant ninject, pour votre projet principal, vous pouvez déclarer que vous voulez n'importe quelle classe avec une dépendance sur IAppSettings utiliser une instance singleton de votre AppSettings classe basée sans avoir à le transmettre explicitement aux constructeurs du monde entier.

Vous pouvez ensuite configurer votre système différemment pour vos tests unitaires en indiquant que vous souhaitez utiliser une instance de MockAppSettings partout où IAppSettings est utilisé, ou simplement en passant explicitement vos objets fictifs directement.

J'espère que j'ai compris l'essentiel de votre question et que j'ai aidé - vous semblez déjà savoir ce que vous faites :)


2
2017-08-28 11:23