依赖注入和AppSettings

发布于 2024-09-16 23:44:33 字数 2253 浏览 5 评论 0 原文

假设我正在为我的应用程序定义一个浏览器实现类:

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

    ...code that uses executablePath
}

乍一看,这似乎是一个好主意,因为 executablePath 数据靠近将使用它的代码。

当我尝试在另一台具有外语操作系统的计算机上运行相同的应用程序时,问题就出现了:executablePath 将具有不同的值。

我可以通过 AppSettings 单例类(或其等效类之一)来解决这个问题,但没有人知道我的类实际上依赖于这个 AppSettings 类(这违背了 DI 想法)。它也可能给单元测试带来困难。

我可以通过构造函数传入 executablePath 来解决这两个问题:

class InternetExplorerBrowser : IBrowser {
    private readonly string executablePath;

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

但这会在我的 Composition Root 中引发问题(启动方法将完成所有需要的类接线),然后该方法必须知道如何连接事物,并且必须知道所有这些小设置数据:

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

        ...
    }
}

这很容易变得一团糟。

我可以通过将表单的接口传递

interface IAppSettings {
    object GetData(string name);
}

给所有需要某种设置的类来解决这个问题。然后,我可以将其实现为嵌入了所有设置的常规类,也可以将其实现为从 XML 文件读取数据的类,类似的东西。如果这样做,我应该为整个系统提供一个通用的 AppSettings 类实例,还是为每个可能需要的类关联一个 AppSettings 类?这确实看起来有点矫枉过正。此外,将所有应用程序设置放在同一位置可以轻松查看并了解在尝试将程序移动到不同平台时需要进行的所有更改。

处理这种常见情况的最佳方法是什么?

编辑:

那么使用 IAppSettings 并将其所有设置硬编码在其中怎么样?

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

这将允许编译时类型安全。如果我看到接口/具体类增长太多,我可以创建其他较小的 IMyClassXAppSettings 形式的接口。对于中型/大型项目来说,这会是一个沉重的负担吗?

我还阅读了有关 AOP 及其处理横切关注点的优势(我想这就是其中之一)。难道它不能也为这个问题提供解决方案吗?也许像这样标记变量:

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

    ...code that uses executablePath
}

然后,在编译项目时,我们还具有编译时安全性(让编译器检查我们是否实际实现了会编织数据的代码。当然,这会将我们的 API 与这个特定方面联系起来。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

冷了相思 2024-09-23 23:44:33

各个类应尽可能不受基础设施的影响 - 像 IAppSettingsIMyClassXAppSettings[AppSetting] 这样的构造将组合详细信息传递给类,这些类,最简单的情况是,实际上只依赖于原始值,例如 executablePath。依赖注入的艺术在于关注点的分解。

我已经使用 Autofac 实现了这个确切的模式,它具有与 Ninject 类似的模块,并且应该产生类似的结果代码(我意识到这个问题没有提到 Ninject,但 OP 在评论中提到了)。

模块按子系统组织应用程序。模块公开子系统的可配置元素:

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

这使组合根面临同样的问题:它必须提供 executablePath 的值。为了避免配置混乱,我们可以编写一个独立的模块来读取配置设置并将其传递给 BrowserModule

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

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

您可以考虑使用自定义配置部分而不是 AppSettings;更改将本地化到模块:

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

这是一个很好的模式,因为每个子系统都有一个独立的配置,可以在单个位置读取。这里唯一的好处是意图更明显。不过,对于非字符串值或复杂模式,我们可以让 System.Configuration 来完成繁重的工作。

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.

眼泪也成诗 2024-09-23 23:44:33

我会选择最后一个选项 - 传入一个符合 IAppSettings 接口的对象。事实上,我最近在工作中进行了重构,以整理一些单元测试,并且效果很好。但是,很少有类依赖于该项目中的设置。

我会创建设置类的单个实例,并将其传递给依赖于它的任何东西。我看不出这有什么根本问题。

但是,我认为您已经考虑过这一点,并了解如果您有很多依赖于设置的类,这会是多么痛苦。

如果这对您来说是个问题,您可以使用依赖注入框架来解决它,例如 ninject (抱歉,如果您已经知道像 ninject 这样的项目 - 这可能听起来有点居高临下 - 如果你不熟悉,为什么使用 ninject github 上的部分是一个学习的好地方)。

使用 ninject,对于您的主项目,您可以声明您希望任何依赖于 IAppSettings 的类使用基于 AppSettings 的类的单例实例,而无需显式传递它到各地的建设者那里。

然后,您可以通过声明要在使用 IAppSettings 的地方使用 MockAppSettings 的实例,或者通过简单地显式传递模拟对象来为单元测试设置不同的系统直接地。

我希望我已经正确理解了你的问题的要点并且我已经提供了帮助 - 你听起来已经知道你在做什么了:)

I'd go with the last option - pass in an object that complies with the IAppSettings interface. In fact, I recently performed that refactor at work in order to sort out some unit tests and it worked nicely. However, there were few classes dependent on the settings in that project.

I'd go with creating a single instance of the settings class, and pass that in to anything that's dependant upon it. I can't see any fundamental problem with that.

However, I think you've already thought about this and seen how it can be a pain if you have lots of classes dependent on the settings.

If this is a problem for you, you can work around it by using a dependency injection framework such as ninject (sorry if you're already aware of projects like ninject - this might sound a bit patronizing - if you're unfamiliar, the why use ninject sections on github are a good place to learn).

Using ninject, for your main project you can declare that you want any class with a dependency on IAppSettings to use a singleton instance of your AppSettings based class without having to explicitly pass it in to constructors everywhere.

You can then setup your system differently for your unit tests by stating that you want to use an instance of MockAppSettings wherever IAppSettings is used, or by simply explicitly passing in your mock objects directly.

I hope I've got the gist of your question right and that I've helped - you already sound like you know what you're doing :)

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文