问题 依赖注入和AppSettings


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

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

    ...code that uses executablePath
}

乍一看,这可能看起来像一个好主意 executablePath 数据靠近将使用它的代码。

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

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

我可以解决这两个问题 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与这个特定方面联系起来。


1624
2017-08-27 19:10


起源

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


答案:


各个类应该尽可能没有基础设施 - 像 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));
    }
}

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


13
2017-08-28 20:40



builder.Register(c => new InternetExplorerBrowser(_executablePath)) 我喜欢你提供单一配置选项而不是整个配置对象的想法。但是,如果一个类需要混合配置选项(通常是原始类型)和服务(复杂对象),那么如何实现呢? class InternetExplorerBrowser(string path, IOtherService service) `````IOtherService````应该由IoC容器解决,但是 path  从我的配置手动。解决方案如 stackoverflow.com/a/2228905/2013911 需要更多的样板代码 - Niklas Peter
@NiklasPeter:在Autofac的情况下, c argument表示解析上下文,您可以请求服务: Register(c => new InternetExplorerBrowser(_executablePath, c.Resolve<IOtherService>()));。 - Bryan Watts


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

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

但是,我认为你已经考虑过这个问题,并且看到如果你有很多依赖于设置的类,那将是多么痛苦。

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

使用ninject,对于您的主项目,您可以声明您希望任何具有依赖关系的类 IAppSettings 使用你的单例实例 AppSettings 基于类而不必将其明确地传递给各地的构造函数。

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

我希望我的问题得到了正确的答案,并且我帮助过了 - 你已经听起来像你知道你在做什么:)


2
2017-08-28 11:23



你好。我很清楚DI框架和Ninject(事实上我一直在使用它!)。我基本同意你的观点。我已经更新了我的OP。如果可能的话,看看吧! - devoured elysium
@devoured elysium - 在阅读了@ Bryan的回答之后,看起来有点像我选择的解决方案有点DI快乐,我想我现在'得到'你的问题了。我忘了当一个类需要消费'服务'时,DI是主要的用途。表示某些设置的类似乎不像是对我的服务(除非它支持更改通知等内容)。 @ Bryan的回答似乎是一个很好的建议,它将有助于保持大多数依赖于设置的类很简单。 - Alex Humphrey