单个配置对象是个坏主意吗?


43

在我的大多数应用程序中,我都有一个单例或静态“ config”对象,负责从磁盘读取各种设置。几乎所有的类都出于各种目的使用它。本质上,这只是名称/值对的哈希表。它是只读的,因此我不必太担心我拥有如此多的全局状态。但是现在我开始进行单元测试,这开始成为一个问题。

一个问题是您通常不希望使用与运行时相同的配置进行测试。有两种解决方案:

  • 为config对象提供一个仅用于测试的设置器,以便您可以传递不同的设置。
  • 继续使用单个配置对象,但是将其从单例更改为一个实例,您可以在需要的任何地方进行传递。然后,您可以使用不同的设置在应用程序中一次构造它,并在测试中一次构造它。

但是无论哪种方式,您仍然面临第二个问题:几乎任何类都可以使用config对象。因此,在测试中,您需要为要测试的类及其所有依赖项设置配置。这会使您的测试代码很难看。

我开始得出这样的配置对象是个坏主意的结论。你怎么看?有哪些替代方案?以及如何开始重构在各处使用配置的应用程序?


该配置通过读取磁盘上的文件来获取其设置,对吗?那么,为什么不仅仅拥有一个可以读取的“ test.config”文件呢?
Anon。

那解决了第一个问题,但没有解决第二个问题。
JW01 2011年

@ JW01:那么,您的所有测试都需要明显不同的配置吗?在测试期间,您将不得不在某个地方设置该配置,不是吗?
Anon。

真正。但是,如果我继续使用单个设置池(使用不同的测试池),则所有测试最终都将使用相同的设置。而且由于我可能想用不同的设置测试行为,所以这不是理想的选择。
JW01 2011年

“因此,在测试中,您需要为要测试的类以及所有依赖项设置配置。这会使测试代码很难看。” 有例子吗?我有一种感觉,不是整个应用程序的结构连接到config类,而是使事情变得“丑陋”的config类本身的结构。设置类的配置是否应该自动配置其依赖项?
AndrewKS 2011年

Answers:


35

我没有单个配置对象的问题,并且可以看到将所有设置都放在一个位置的优势。

但是,在任何地方使用该单个对象都会导致config对象和使用它的类之间的高度耦合。如果需要更改config类,则可能必须访问使用该类的每个实例,并检查是否没有破坏任何东西。

解决此问题的一种方法是创建多个接口,以暴露应用程序不同部分所需的配置对象部分。而不是允许其他类访问config对象,而是将接口的实例传递给需要它的类。这样,应用程序中使用config的部分取决于接口中较小字段的集合,而不是整个config类。最后,对于单元测试,您可以创建一个实现该接口的自定义类。

如果您想进一步探索这些想法,我建议您阅读SOLID 原则,尤其是接口隔离原则依赖倒置原则


7

我用界面将相关设置分组。

就像是:

public interface INotificationEmailSettings {
   public string To { get; set; }
}

public interface IMediaFileSettings {
    public string BasePath { get; set; }
}

等等

现在,对于真实环境,单个类将实现许多这些接口。该类可能来自数据库或应用程序配置或您拥有的东西,但通常它知道如何获取大多数设置。

但是,通过接口隔离可以使实际的依赖关系更加明确和细化,这对于可测试性而言是一个明显的改进。

但是..我并不总是希望被强迫注入或提供相关设置。出于一致性或明确性,我应该提出一个论点。但是在现实生活中,这似乎是不必要的限制。

因此,我将使用静态类作为外观,从任何地方轻松输入设置,在该静态类中,我将服务于定位接口的实现并获取设置。

我知道服务位置会被大多数人拒之门外,但让我们面对现实吧,通过构造函数提供依赖关系是一个权重,有时权重超出了我的承受能力。服务位置是通过对接口进行编程来保持可测试性的解决方案,并允许多种实现,同时仍提供静态单例入口点的便利性(在仔细测量和适当的几种情况下)。

public static class AllSettings {
    public INotificationEmailSettings NotificationEmailSettings {
        get {
            return ServiceLocator.Get<INotificationEmailSettings>();
        }
    }
}

我发现这种组合是世界上最好的。


3

是的,正如您意识到的那样,全局配置对象使单元测试变得困难。拥有一个用于单元测试的“秘密”设置程序是一个快速的技巧,虽然不好,但是非常有用:它使您能够开始编写单元测试,以便随着时间的推移可以将代码重构为更好的设计。

(强制性参考:有效地使用遗留代码。它包含此代码以及许多用于对遗留代码进行单元测试的宝贵技巧。)

以我的经验,最好的办法是尽可能少地依赖全局配置。尽管不一定为零-这完全取决于环境。只要访问者能够做到这一点,那么拥有一些访问全局配置并将实际配置属性传递给它们创建和使用的对象的高级“组织者”类就可以很好地工作,只要组织者仅这样做即可,例如,它们本身并不包含很多可测试的代码。这使您可以测试应用程序的大部分或全部可单元测试的重要功能,同时仍不完全破坏现有的物理配置架构。

但是,这已经接近真正的依赖注入解决方案,例如Spring for Java。如果可以迁移到这样的框架,那就好。但是在现实生活中,尤其是在处理旧版应用程序时,通常缓慢而细致的DI重构是最好的折衷方案。


我真的很喜欢您的方法,因此建议不要将“塞特犬”的想法保持为“秘密”或考虑为“快速破解”。如果您认为单元测试是应用程序的重要用户/用户/部分的立场,那么拥有test_属性和方法就不再是一件坏事了,对吧?我确实同意,解决该问题的真正方法是采用DI框架,但是在像您这样的示例中,当使用遗留代码或其他简单情况时,不会疏远测试代码是完全适用的。
FilipDupanović2011年

1
@kRON,这些是使遗留代码单元可测试的非常有用和实用的技巧,我也正在广泛使用它们。但是,理想情况下,生产代码不应包含仅出于测试目的而引入的任何功能。从长远来看,这是我重构的方向。
彼得Török

2

以我的经验,在Java世界中,Spring就是这个意思。它基于在运行时设置的某些属性来创建和管理对象(bean),否则对您的应用程序是透明的。您可以使用Anon的test.config文件。提及或您可以在配置文件中包含一些逻辑,Spring将处理这些逻辑以基于其他键(例如主机名或环境)设置属性。

关于第二个问题,您可以通过一些重新构造来解决此问题,但这并不像看起来那样严重。在您的情况下,这意味着您不会拥有其他各种类使用的全局Config对象。您只需要通过Spring配置那些其他类,然后就没有config对象,因为所有需要配置的东西都是通过Spring获得的,因此您可以直接在代码中使用这些类。


2

这个问题实际上是一个比配置更普遍的问题。一读到“单身”一词,我立即想到与该模式相关的所有问题,尤其是可测试性差。

辛格尔顿模式被认为“有害”。意思是,这并不总是错误的事情,但通常是错误的。如果您正在考虑对任何事物使用单例模式,请停止考虑:

  • 您是否需要对它进行子类化?
  • 您是否需要对接口进行编程?
  • 您是否需要对其进行单元测试?
  • 您是否需要经常修改?
  • 您的应用程序旨在支持多个生产平台/环境吗?
  • 您甚至还有点担心内存使用情况吗?

如果您对以上任何一项的回答为“是”(可能还有我没想到的其他几件事),那么您可能不想使用单例。配置通常需要比单例(或就此而言,任何无实例的类)所提供的灵活性更大。

如果您想要单身人士的几乎所有好处而又没有任何痛苦,那么请使用依赖注入框架(如Spring或Castle)或适用于您环境的任何东西。这样,您只需要声明一次即可,容器将自动为需要的实例提供实例。


0

我用C#处理此问题的一种方法是让一个单例对象在实例创建期间锁定,并立即初始化所有数据以供所有客户端对象使用。如果此单例可以处理键/值对,则可以存储任何方式的数据,包括供许多不同的客户端和客户端类型使用的复杂键。如果要使其保持动态并根据需要加载新的客户端数据,则可以验证客户端主键的存在,如果丢失,则为该客户端加载数据,然后将其附加到主字典中。主配置单例还可能包含一组客户端数据,其中相同客户端类型的多个客户端使用相同的数据,这些数据全部通过主配置单例进行访问。这种配置对象的组织结构很大程度上取决于您的配置客户端如何访问此信息以及该信息是静态还是动态。我根据需要使用了键/值配置以及特定的对象API。

我的一个配置单例可以接收消息以从数据库重新加载。在这种情况下,我加载到第二个对象集合中,并且仅锁定主集合才能交换集合。这样可以防止阻塞其他线程上的读取。

如果从配置文件加载,则可能具有文件层次结构。一个文件中的设置可以指定要加载其他文件中的哪个。我已将此机制用于具有多个可选组件的C#Windows服务,每个组件都有自己的配置设置。跨文件的文件结构模式相同,但是是否根据主要文件设置进行加载。


0

您正在描述的类听起来像是God对象的反模式,只是用数据而不是函数。那是个问题。您可能应该读取配置数据并将其存储在适当的对象中,而仅在出于某种原因需要时才再次读取数据。

此外,您使用Singleton的原因不适当。仅当存在多个对象会创建不良状态时才应使用单例。在这种情况下,使用Singleton是不合适的,因为拥有多个配置读取器不应立即导致错误状态。如果您拥有多个配置读取器,那么您可能做错了,但是,绝对没有必要只有一个配置读取器。

最后,创建这样的全局状态违反了封装,因为您允许的类比知道直接访问数据所需的更多。


0

我认为,如果有很多设置,并且包含相关的“集合”,那么将它们分成具有各自实现的单独接口是有意义的,然后将这些接口注入DI中。您可以获得可测试性,更低的耦合和SRP。

Los Techies的Joshua Flanagan启发了我。前段时间写了一篇关于此事的文章

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.