使用静态类来方便访问appsettings是否可行?

5

我在我的应用程序的app.config文件中有一些键,用来存储电子邮件设置。我目前正在为工作开发的应用程序有几个地方需要发送电子邮件(大多数是在错误检查中,以便让我们知道发生了什么不好的事情)。为了更轻松地获取我需要的设置(而不是返回到app.config中查找要在ConfigurationManager.AppSettings["SomeKey"].ToString()中使用的文本字符串),我创建了一个简单的静态类,用于存储一些只读属性以返回我需要的值。这是基本结构。

internal static Class myClass
{
    internal static string setting1
    {
        get { return ConfigurationManager.AppSettings["SomeKey"].ToString(); }
    }
    internal static string setting2
    {
        get { return ConfigurationManager.AppSettings["SomeOtherKey"].ToString(); }
    }
}

这种做法可行吗?我没有发现问题,但总觉得可能会漏掉什么。
附加信息:
我知道可以简单地使用Settings.Settings文件的方法。 由于我工作的代码政策,我无法使用Settings.Settings文件,这使得我现在的做法变得必要。
编辑: 所谓“可接受”,是指它是否普遍被采用或被认为是一个好主意?我意识到社区无法推测我的工作中的可接受性。

4
这取决于你对“可接受”的定义。对我而言,使用可注入的接口包装ConfigurationManager.AppSettings,以便在需要时可以模拟配置依赖是“可接受”的做法。但对你来说可能有些过度了。这完全取决于你的需求。然而,就我个人而言,我尽量避免使用静态类,以提高单元测试的可测试性。 - David L
1
如果你不进行单元测试,或者没有必要模拟对静态方法调用的依赖,那么这似乎是可以接受的。 - jmrah
希望能够得到一些关于负评的解释。我认为这是一个合理的问题?也许我的措辞不太好?我会重新审查并尝试重构它,使其更客观。 - Rocky
1
我认为更多的是因为这个问题的投机本质,而不是问题本身。在codereview.stackexchange.com上可能会更好。这是一个非常非常好的问题。当您意识到您不希望您的类直接与AppSettings交互时,您已经发现了一些重大问题。一旦您得到了答案,您会发现它适用范围更广。对于我个人而言,依赖注入(我使用Windsor)改变了一切。不仅仅是因为DI本身很棒,而是因为它成为了改变我编写代码方式的催化剂。它还促进了其他最佳实践,如单元测试。 - Scott Hannen
静态访问并不意味着您不能使用其他参数测试代码,我不知道为什么有些人一直重复这个论点。在全局配置参数的情况下进行依赖(DEPENDENCY!)注入只是一种过度设计和过度设计。您应该选择正确的工具来完成正确的工作。就是这样。 - Cesar
显示剩余6条评论
3个回答

17

这并不糟糕,但通过小调整它可以变得更好(好吧,也许不是真正的小调整)。

如果你的类依赖于一个静态类,并且该类依赖于AppSettings,那么你的类仍然与AppSettings耦合。换句话说,他们没有其他获取设置的方式。如果你想对你的类进行单元测试,这将是个挑战。这意味着你的单元测试项目必须有相同的<appSettings>部分。但是,如果你想要使用两个不同值的设置进行两个测试怎么办?这是不可能的。或者,如果你有一个需要设置的类,在几年后想在ASP.NET Core应用程序中使用它,但是没有web.config文件怎么办?那就完全行不通了。

为了避免这种情况,你可以这样做:

public interface IMySettings
{
    string Setting1 {get;}
    string Setting2 {get;}
}

public class MyConfigurationSettings : IMySettings
{
    public string Setting1
    {
        get { return ConfigurationManager.AppSettings["SomeKey"].ToString(); }
    }
    public string Setting2
    {
        get { return ConfigurationManager.AppSettings["SomeOtherKey"].ToString(); }
    }
}

然后,在需要进行设置的类中:

public class ClassThatNeedsSettings
{
    private readonly IMySettings _settings;

    public ClassThatNeedsSettings(IMySettings settings)
    {
        _settings = settings;
    }
}

那么,当您创建一个ClassThatNeedsSettings实例时,需要传递一个实现IMySettings接口的类的实例,该类将使用它来检索设置。当您的应用程序正在运行时,您传入MyConfigurationSettings,以便您的值来自AppSettings。但是ClassThatNeedsSettings从不知道这一点。它只知道它正在使用IMySettings的一个实例。

这被称为“依赖注入”。ClassThatNeedsSettings依赖于IMySettings,因此您将其“注入”到构造函数中。这样,ClassThatNeedsSettings将获得所需内容。它不负责创建它。

如果您想进行单元测试,则可以“模拟”IMySettings。也就是说,您可以创建其他实现该接口的类,并使用它们传递任何要测试的值。甚至有像Moq这样的工具可帮助您创建这些类。

通常,如果您使用依赖注入,您还将使用像Windsor、Unity、Autofac或其他类似框架来管理为您创建对象。这感觉有点像诱饵和开关,在最后引入它是因为它需要更多的学习,可能会改变应用程序配置的方式。但这就是我们使用它的原因,以防止一个类对另一个类有绝对依赖性,从而使其不够灵活且难以测试。


刚刚重新编码,使用了接口方法,代码看起来更加简洁,也可以进行单元测试了。之前由于静态类的原因,我一度担心无法进行单元测试。虽然我对依赖注入和模拟还很陌生,但你的示例为我提供了一个很好的起点。 - Rocky
1
@Rocky,既然你对DI还不熟悉,我建议你暂时不要使用IoC容器(例如Windsor和Autofac)。你不需要它们来编写好的代码。当正确使用时,它们对于新手来说看起来像魔法。但是,当使用不当时,它们就会变成服务定位器模式。我记得一开始我也对IoC容器感到困惑。 - user2023861
1
同意 - 很容易混淆模式和帮助我们实现模式的工具。我主要在控制台应用程序中使用Windsor,大多数时候是为了弄清它的工作原理。我稍后会写一些关于它的东西。尽管它对于控制台应用程序来说有点奇怪,但它确实提供了一种演示工具的方式,而不需要关于如何配置应用程序以使用容器的所有背景噪音。 - Scott Hannen
解释得很好,但如果你的意思是构造函数,那么public MySettings(IMySettings settings)应该改为public ClassThatNeedsSettings(...) - S.Serpooshan
@S.Serpooshan 谢谢你! - Scott Hannen

5
这是一个可测试的代码示例,解决了评论者提出的问题:
interface IEmailSettings {
    string Server { get; }
    string Sender { get; }
    string[] Recipients { get; }
}

如果您将设置存储在app.config中,请使用以下内容:
class AppConfigEmailSettings : IEmailSettings {
    public AppConfigEmailSettings() {
        this.Server = ConfigurationManager.AppSettings["server"];
        this.Sender = ConfigurationManager.AppSettings["sender"];
        this.Recipients = ConfigurationManager.AppSettings["recipients"].Split(';');
    }

    public string Server { get; private set; }
    public string Sender { get; private set; }
    public string[] Recipients { get; private set; }
}

如果您将设置存储在数据库中,请使用以下方法:

class DatabaseEmailSettings : IEmailSettings {
    public DatabaseEmailSettings(string connectionString) {
        //code to connect to database and retrieve settings
    }
    //you can use the same fields and properties from AppConfigEmailSettings
}

为了测试,您可以使用类似以下的内容:

class MockSettings : IEmailSettings {
    public string Server { get { return "localhost"; } }
    public string Sender { get { return "sender@example.com" } }
    public string[] Recipients { get { return new string[] { "r1@example.com" }; } }
}

你明白了吧。这个代码比你的代码更容易测试。而且,如果你将IEmailSettings注入到发送电子邮件的代码中,你可以通过更改整个应用程序中的一行代码轻松更改如何存储电子邮件设置。那就是实例化IEmailSettings对象为AppConfigEmailSettings或DatabaseEmailSettings或其他内容的那行代码。

2
没有人能真正定义你的情况下什么是“可接受”的或者不可接受的。我想你真正想问的是这是一个好主意吗?还是有可能会出问题?
简短的回答是对于简单的解决方案,它可能是可以接受的,但对于任何复杂的解决方案都是一个坏主意。静态类使单元测试非常困难(或者根本不可能)。相反,如你在评论中提到的,更好的选择是定义一个接口,在该接口上具有GetSettings方法。您可以使用一个(非静态)类来实现它,该类只是将调用包装在ConfigurationManager.AppSettings中,就像David L在评论中提到的那样。
任何需要访问设置的类都应将此接口传递给构造函数。你可以使用IoC容器或创建一个新的类实例来传递它(取决于复杂性),但更重要的是,现在你可以编写单元测试并传递一个具有自定义设置的类进行单元测试。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接