【问题标题】:Dependency Injection and AppSettings依赖注入和 AppSettings
【发布时间】:2011-04-04 23:34:06
【问题描述】:

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

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 与这个特定的方面联系起来。

【问题讨论】:

  • 你反对把它放在你的作文根目录中吗?通常Main 方法或Application_Start 例程是最接近并且最了解配置机制的一段代码,因此它是放置配置逻辑的最明智的地方,无论这涉及解析命令行参数,读取配置文件,或获取有关执行上下文的信息。
  • 我只反对在对象的构造函数中传递字符串、整数等,因为这样所有的启动代码都会变得一团糟。另一方面,让启动只是实例化一个 AppSettings 类并将其传递给所有需要它的类对我来说似乎没问题。
  • 不是的部分是什么?

标签: c# oop dependency-injection application-settings


【解决方案1】:

各个类应尽可能不受基础设施的影响——IAppSettingsIMyClassXAppSettings[AppSetting] 等结构将组合细节泄露给最简单的类,这些类实际上只依赖于原始值,例如 @987654326 @。依赖注入的艺术在于考虑因素。

我已经使用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));
    }
}

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

【讨论】:

  • builder.Register(c =&gt; new InternetExplorerBrowser(_executablePath)) 我喜欢你提供单个配置选项而不是整个配置对象的想法。但是,如果一个类需要混合配置选项(通常是原始类型)和服务(复杂对象),你如何实现这一点:class InternetExplorerBrowser(string path, IOtherService service)````IOtherService```` 应​​该由 IoC 容器解决,但是path 手动从我的配置中。像stackoverflow.com/a/2228905/2013911 这样的解决方案需要更多样板代码
  • @NiklasPeter:对于 Autofac,c 参数表示解析上下文,您可以请求服务:Register(c =&gt; new InternetExplorerBrowser(_executablePath, c.Resolve&lt;IOtherService&gt;()));
【解决方案2】:

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

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

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

如果这对您来说是个问题,您可以使用诸如 ninject 之类的依赖注入框架来解决它(抱歉,如果您已经知道像 ninject 这样的项目 - 这可能听起来有点傲慢 - 如果您比较陌生,github上的why use ninject部分是学习的好地方)。

使用 ninject,对于您的主项目,您可以声明您希望任何依赖于 IAppSettings 的类使用基于 AppSettings 的类的单例实例,而无需将其显式传递给各处的构造函数。

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

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

【讨论】:

  • 你好。我非常了解 DI 框架和 Ninject(事实上,我一直在使用它!)。我基本同意你的观点。我已经更新了我的 OP。如果可能的话,看看吧!
  • @devoured elysium - 在阅读了@Bryan 的回答之后,我选择的解决方案似乎有点像 DI-happy,我想我现在更“明白”了你的问题。我忘记了当类需要使用“服务”时,DI 是主要用途。代表某些设置的类对我来说似乎不太像服务(除非它支持更改通知之类的东西)。 @Bryan 的回答似乎是一个很好的建议,它将帮助您保持大多数依赖于设置的类既美观又简单。
猜你喜欢
  • 2021-11-19
  • 1970-01-01
  • 1970-01-01
  • 2017-04-02
  • 2012-02-15
  • 2011-05-11
  • 2017-03-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多