Dependency Injection and AppSettings

devoured elysium picture devoured elysium · Aug 27, 2010 · Viewed 8.6k times · Source

Let's say I am defining a browser implementation class for my application:

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

    ...code that uses executablePath
}

This might at first glance to look like a good idea, as the executablePath data is near the code that will use it.

The problem comes when I try to run this same application on my other computer, that has a foreign-language OS: executablePath will have a different value.

I could solve this through an AppSettings singleton class (or one of its equivalents) but then no-one knows my class is actually dependent on this AppSettings class (which goes against DI ideias). It might pose a difficulty to Unit-Testing, too.

I could solve both problems by having executablePath being passed in through the constructor:

class InternetExplorerBrowser : IBrowser {
    private readonly string executablePath;

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

but this will raise problems in my Composition Root (the startup method that will do all the needed classes wiring) as then that method has to know both how to wire things up and has to know all these little settings data:

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

        ...
    }
}

which will turn easily a big mess.

I could turn this problem around by instead passing an interface of the form

interface IAppSettings {
    object GetData(string name);
}

to all the classes that need some sort of settings. Then I could either implement this either as a regular class with all the settings embedded in it or a class that reads data off a XML file, something along the lines. If doing this, should I have a general AppSettings class instance for the whole system, or have an AppSettings class associated to each class that might need one? That certainly seems like a bit of an overkill. Also, have all the application setings in the same place makes it easy to look and see what might be all the changes I need to do when tryign to move the program to different platforms.

What might be the best way to approach this common situation?

Edit:

And what about using an IAppSettings with all its settings hardcoded in it?

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

This would allow for compile-time type-safety. If I saw the interface/concrete classes grow too much I could create other smaller interfaces of the form IMyClassXAppSettings. Would it be a burden too heavy to bear in med/big sized projects?

I've also reading about AOP and its advantages dealing with cross-cutting-concerns (I guess this is one). Couldn't it also offer solutions to this problem? Maybe tagging variables like this:

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

    ...code that uses executablePath
}

Then, when compiling the project we'd also have compile time safety (having the compiler check that we actually implemented code that would weave in data. This would, of course, tie our API to this particular Aspect.

Answer

Bryan Watts picture Bryan Watts · Aug 28, 2010

The individual classes should be as free from infrastructure as possible - constructs like IAppSettings, IMyClassXAppSettings, and [AppSetting] bleed composition details to classes which, at their simplest, really only depend on raw values such as executablePath. The art of Dependency Injection is in the factoring of concerns.

I have implemented this exact pattern using Autofac, which has modules similar to Ninject and should result in similar code (I realize the question doesn't mention Ninject, but the OP does in a comment).

Modules organize applications by subsystem. A module exposes a subsystem's configurable elements:

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

This leaves the composition root with the same problem: it must supply the value of executablePath. To avoid the configuration soup, we can write a self-contained module which reads configuration settings and passes them to BrowserModule:

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

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

You could consider using a custom configuration section instead of AppSettings; the changes would be localized to the 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));
    }
}

This is a nice pattern because each subsystem has an independent configuration which gets read in a single place. The only benefit here is a more obvious intent. For non-string values or complex schemas, though, we can let System.Configuration do the heavy lifting.