什么反对静态字段

本文关键字:字段 静态 什么 | 更新日期: 2023-09-27 18:36:29

我的工具中有两种情况,我在静态字段的集成测试中遇到了一些困难。在应用程序中,这些字段没有问题。在我的测试中,我基本上使用每种测试方法创建了一个新应用程序。当我在一次测试运行中运行这些时,您可以想象,会出现一些问题。首先让我展示两个这样的案例。

案例1

class SomeClass
{
    private static IService service;
    public static void Initialize(IService service)
    {
        SomeClass.service = service;
    }
    public void DoSomthing()
    {
        service.Foo();
    }
}

基本上,此类的对象将被大量创建。为了能够使用 IService 对象,它被存储为静态字段。在我的测试中,这是一个问题,因为IService实际上是一个IServiceProvider,并且在第一次测试中仅检索一次服务。在第二个测试中,使用第一个测试的服务。在实际应用程序中,只有一个IServiceIServiceProvider。因此,我们在这里没有问题。

案例2

abstract class BaseClass
{
    private static readonly Lazy<SpecificClass> specificClass = new Lazy<SpecificClass>(() => new SpecificClass());
    public static SpecificClass SpecificClass
    {
        get { return specificClass.Value; }
    }
}
class SpecificClass : BaseClass
{
}

这对于我的测试来说更加麻烦,因为即使我创建了一个完整的新应用程序,当SpecificClass在第一次测试中使用时,它在第二次测试中也是同一个对象。在我的测试中,我这里有内存泄漏,因为SpecificClass有一个列表,它可以记住第一次测试中的对象。随着每次测试,越来越多的对象被添加到列表中。在实际应用程序中,列表仅在应用程序启动时填充一次。所以这里没有内存泄漏。

我知道,测试通常会显示设计缺陷,但我在这里看不到。我能想到的删除这些静态字段的唯一论据是,否则我的测试不起作用。

所以我现在的问题是,为什么在这些情况下使用静态字段被认为是糟糕的代码,还是不是?我不想知道这些情况的解决方案。我只需要一个理由,为什么需要更改代码,而不是"我无法正确测试它"。

什么反对静态字段

这里的设计缺陷是组件决定了其依赖项的生命周期,这在实际应用程序中都是不好的,并且在编码测试时使事情变得更加困难。

您需要的是使用具有依赖注入支持的反转控制容器,并让它通过配置注入的依赖关系的生命周期来决定。

例如,在温莎城堡中,它将按如下方式配置:

IWindsorContainer container = new WindsorContainer();
container.Register
(
     Component.For<SomeClass>().LifeStyleTransient(),
     Component.For<IService>().ImplementedBy<SomeService>().LifeStyleSingleton()
);

因此,您决定IService通过配置是单例,但您的代码依赖于SomeClass实例将被注入IService实现的实例。

为什么对实际应用程序不利。我只需要一个理由 为什么需要更改代码。

  • 没有简单的单元测试:如果不使用实例构造函数和/或属性,则无法使用自动依赖项注入。这使得您的代码更难测试,因为简单的配置无法注入值而不是实际实现。

  • 没有自动依赖注入。不同的应用程序和服务不能配置为使用相同或不同的依赖接口实现,并且您失去了一个重要功能:对象生命周期(瞬态、单例、每个请求、每个线程)可以通过配置定义,您的代码不应该意识到这一点。

  • 没有单一责任原则。您的类是否负责决定其依赖项的生命周期,或者不是应该定义它的框架?

你在问题的一些评论中对乔恩·斯基特说:

@JonSkeet我知道为什么它们对我的测试不利。但这不是 接受的参数,只要它适用于实际应用程序。那 这就是为什么我想找到一个不同的论点。–

软件质量与实际应用程序一样真实。如果开发高质量的软件对你来说不是一个公认的论点,也许你需要回到根源,找到为什么应用程序或服务必须是可测试的。

您描述的是所有不适合依赖注入候选对象的有效用例(ORM 实体就是一个例子)。

在这些情况下,静态字段的替代方法是将依赖项作为方法参数提供:

class SomeClass
{
    public void DoSomething(IService service)
    {
        service.Foo();
    }
}

除了更容易测试之外,这种方法的一个优点是,您不必注意手动正确初始化静态状态(有时可能很难实现,尤其是之后当静态依赖项开始相互依赖时,由于静态字段初始化的顺序等),这尤其难以维护)。

此外,将来重构和更改代码更容易,因为当您决定将使用静态字段定义和初始化的逻辑提取到单独的类等时,您不必移动/复制静态字段定义和初始化。