条件继承:base class 依赖于环境变量

Conditional inheritance: base class depend on environment variable

我有两个摘要classes,'ValidationsWithStorage'继承'Validations'

   public abstract class Validations {
    // methods..
   }

   public abstract class ValidationsWithStorage : Validations { 
    // ... 
    }

我也有一个class:

public abstract class TestsValidations : T

T 应该取决于环境变量:

Environment.GetEnvironmentVariable("useStorage") 

如果此变量为空,我希望 T 将是验证。 否则,我希望 T 将是 ValidationsWithStorage。

最好的方法是什么?

谢谢

我不认为你可以用你的方式做你想做的事。

为什么不让您的 class TestValidations 在其构造函数中使用 ValidationsValidationsWithStorage 类型的参数。如果它们都遵循相同的界面,您的 TestsValidations class 将不需要知道(或关心)它正在使用的是两者中的哪一个。

所以基本上:

  1. 为您的 ValidationsValidationsWithStorage class
  2. 创建一个界面
  3. 检查你的环境变量
  4. 根据环境变量将正确的class传入TestsValidation构造函数

有帮助吗?

我不确定您是否可以通过继承来做到这一点。这不是继承的逻辑。如果你使用类似工厂模式的东西并改变你当前的设计会更好。

也许你可以做这样的事情。我没有测试,但我认为这样会更容易:

    public interface Validations
{
    void ValidationsStuff();
}

public class ValidationsWithStorage : Validations
{

    public void ValidationsStuff()
    {
        //do something
    }
}

public class TestsValidations : Validations
{

    public void ValidationsStuff()
    {
        //do something
    }
}

public class ValidationsFactory
{
    public Validations geValidationsComponent(string useStorage)
    {
        if (string.IsNullOrEmpty(useStorage))
            return new ValidationsWithStorage();
        else
            return new TestsValidations();
    }
}

您可以使用条件编译来做到这一点:

public abstract class TestsValidations
#if USESTORAGE
    : ValidationsWithStorage
#else
    : Validations
#endif
{

}

您可以在项目配置中设置它或通过将附加参数传递给 msbuild:/p:DefineConstants="USESTORAGE"

我不认为这是好的设计,但它是可行的。

如果你想使用继承,我认为如果你使用 Generic Constraints

,你的问题就会得到解决

不该做什么:

我不建议有条件地更改 class 的定义。这样做有一些奇怪的、一次性的原因,但我们很少遇到它们,不应该让它们成为我们编写代码的正常部分。

我也不推荐工厂。工厂意味着您在运行时做出决定,在生产中,是使用“真实”class 还是测试class。仅当某些仅在运行时可用的数据决定了您要使用哪个实现时,工厂才有意义。例如,如果你想验证一个地址,你可能会使用它的国家来确定我们是否使用美国验证器、加拿大验证器等,就像这样:

var validator = _validatorFactory.GetValidator(address.Country);

此外,这意味着“测试”class 将从您的生产代码中引用。这是不可取的,而且有点奇怪。

要做什么:

如果您不在运行时做出这样的决定,那么这应该在组合根中确定 - 也就是说,在我们的应用程序的一部分中确定,在启动时,我们 classes重新使用。

首先,您需要一个抽象。这通常是一个界面,如下所示:

public interface IValidator
{
    ValidationResult Validate(Something value);
}

需要验证的class看起来像这样:

public class ClassThatNeedsValidation
{
    private readonly IValidator _validator;

    public ClassThatNeedsValidation(IValidator validator)
    {
        _validator = validator;
    }

    // now the method that needs to use validation can
    // use _validator.
}

这就是依赖注入。 ClassThatNeedsValidation 不负责创建验证器实例。这将迫使它“知道”IValidator 的实现。相反,它希望有一个 IValidator 提供给它。 (换句话说,它的依赖性——它需要的东西——是注入。)

现在,如果您要创建 ClassThatNeedsValidation 的实例,它可能如下所示:

var foo = new ClassThatNeedsValidation(new ValidationWithStorage());

然后,在您的单元测试项目中,您可能有一个 IValidator 的测试实现。 (您也可以使用像 Moq 这样的框架,但我支持您 - 有时我更喜欢编写一个测试替身 - 一个实现接口的测试 class。)

所以在单元测试中,你可以这样写:

var foo = new ClassThatNeedsValidation(new TestValidator());

这也意味着 TestValidator 可以在您的测试项目中,而不是与您的生产代码混合。

如何让它变得更简单:

在这个例子中:

var foo = new ClassThatNeedsValidation(new ValidationWithStorage());

您可以看到这会变得多么混乱。如果 ValidationWithStorage 有自己的依赖项怎么办?那么你可能不得不开始编写这样的代码:

var foo = new ClassThatNeedsValidation(
    new ValidationWithStorage(
        connectionString, 
        new SomethingElse(
            new Whatever())));

这不好玩。这就是为什么我们经常使用 IoC 容器,a.k.a 依赖注入容器。

如果我们使用 ASP.NET 核心,这很熟悉,但重要的是要知道我们不必使用 ASP.NET 核心来执行此操作。我们可以将 Microsoft.Extensions.DependencyInjection, Autofac、Windsor 或其他项目添加到项目中。

解释这在某种程度上超出了这个答案的范围,它可能比你现在需要的更多。但它使我们能够编写如下代码:

services.AddSingleton<IValidator, ValidationWithStorage>();
services.AddSingleton<Whatever>();
services.AddSingleton<ISomethingElse, SomethingElse>();
services.AddSingleton<ClassThatNeedsValidation>();

现在,如果容器需要创建 ClassThatNeedsValidation 的实例,它将查看构造函数,找出它需要的依赖项,然后创建它们。如果那些 classes 有依赖关系,它也会创建它们,依此类推。

如果这是一个新概念,这需要一分钟或几分钟或更多 reading/trying,但请相信我,它使编写代码和单元测试变得容易得多。 (除非我们做错了,否则一切都会变得更难,但凡事都是如此。)

如果出于某种原因,您想在不同的环境中使用 IValidator 的不同实现怎么办?因为上面的代码只执行一次,在启动的时候,很简单:

if(someVariable = false)
    services.AddSingleton<IValidator, OtherValidator>();
else
    services.AddSingleton<IValidator, ValidationWithStorage>();

您正在做决定,但您只做 一次。依赖于 IValidator 的 class 不需要知道这个决定。它不需要询问它在哪个环境中。如果我们沿着这条路走下去,我们最终会得到类似污染我们所有 classes 的东西。它还将使我们的单元测试更难编写和理解。在启动时做出这样的决定——组合根——消除了所有的混乱。