如何向静态类注入依赖

60

在我的应用程序中,我经常需要将日志消息写入磁盘。我创建了一个简单的日志记录器类,并使用依赖注入进行构建,如下所示:

var logger = new LogService(new FileLogger());
logger.WriteLine("message");

但是现在这意味着我系统中的每个需要记录日志的类都需要注入这个LogService,这似乎是多余的。因此,我想将LogService改为静态。这样就不需要将其注入到使用的类中。

但是问题来了。如果我将日志记录器类设为静态,那么就没有办法通过构造函数向该静态类注入依赖。

因此,我将我的LogService更改为以下内容。

public static class LogService()
{
    private static readonly ILoggable _logger;
    static LogService()
    {
         _logger = new FileLogger();
    }
    
    public static void WriteLine(string message) ...
}

这让我感到很奇怪。我认为这不再是 DI 了。

最好的方法是将依赖注入到静态类中是什么?


8
你想将一个依赖注入到一个静态类中,这表明你可能有错误的思路。 - Mitch Wheat
3
DI系统不仅能够注入构造函数参数,更重要的是使某些对象看起来像是单例——即使它们实际上并不是对象,也能产生静态类的净结果。 - user585968
3
通过将一个类设置为静态,您正在声明它的生命周期。将生命周期管理从类中移开,符合单一职责原则。因此,尽量避免使用静态类,特别是如果它们保留了一些内部状态。 - stop-cran
5个回答

104
依赖注入作为一种实践,旨在引入抽象(或接缝)来解耦易变的依赖关系。易变的依赖关系是指一个类或模块,除其他事项外,可能包含非确定性行为,或者一般来说,是您希望能够替换或拦截的内容。

有关易变依赖关系的更详细讨论,请参阅本免费可阅读介绍的1.3.2节以及我的书

因为你的`FileLogger`写入磁盘,所以它包含着不确定的行为。因此,你引入了`ILoggable`抽象。这使得消费者与`FileLogger`的实现解耦,并且在需要时,你可以轻松地将这个`FileLogger`实现替换为一个将日志记录到SQL数据库的`SqlLogger`实现,甚至可以有一个将调用转发到`FileLogger`或`SqlLogger`的实现。
然而,为了成功地将消费者与其易变的依赖解耦,你需要将该依赖注入到消费者中。有三种常见的模式可供选择:
  • 构造函数注入 - 依赖项被静态定义为类的实例构造函数的参数列表。
  • 属性注入 - 依赖项通过可写的实例属性注入到使用者中。这种模式有时也被称为“setter注入”,尤其在Java世界中。
  • 方法注入 - 依赖项作为方法参数注入到使用者中。
构造函数注入和属性注入都应用在应用程序的启动路径(也称为组合根)内,并要求消费者将依赖项存储在私有字段中以供以后重用。这要求构造函数和属性是实例成员,即非静态的。通常情况下,构造函数注入优于属性注入,因为属性注入会导致时间耦合。静态构造函数不能有任何参数,而静态属性会导致环境上下文反模式(参见第5.3节)——这会影响可测试性和可维护性。
方法注入,另一方面,是应用在组合根之外的,它不会存储任何提供的依赖项,而只是简单地使用它。以下是一个来自之前的参考的示例:
// This method calculates the discount based on the logged in user.
// The IUserContext dependency is injected using Method Injection.
public static decimal CalculateDiscountPrice(decimal price, IUserContext context)
{
    // Note that IUserContext is never stored - only used.
    if (context == null) throw new ArgumentNullException("context");
    decimal discount = context.IsInRole(Role.PreferredCustomer) ? .95m : 1;
    return price * discount;
}

方法注入是三种模式中唯一适用于实例和静态类的模式。
在应用方法注入时,方法的“使用者”必须提供依赖项。然而,这也意味着使用者本身必须通过构造函数、属性或方法注入来提供该依赖项。例如:
public class ProductServices : IProductServices
{
    private readonly IProductRepository repository;
    private readonly IUserContext userContext;

    public ProductServices(
        IProductRepository repository,
        IUserContext userContext) // <-- Dependency applied using Ctor Injection
    {
        this.repository = repository;
        this.userContext = userContext;
    }

    public decimal CalculateCustomerProductPrice(Guid productId)
    {
        var product = this.repository.GetById(productId);

        return CalculationHelpers.CalculateDiscountPrice(
            product.Price,
            this.userContext); // <-- Dep forwarded using Method Injection
    }
}

你在构造函数中创建FileLogger的静态LogService的例子是紧密耦合代码的一个很好的例子。这被称为控制狂反模式(第5.1节),或者一般可以看作是DIP违规——这是DI的相反
为了防止易变依赖的紧密耦合,最好的方法是将LogService设置为非静态,并将其易变依赖注入到其唯一的公共构造函数中:
public class LogService
{
    private readonly ILoggable _logger;

    public LogService(ILoggable logger)
    {
         _logger = logger;
    }
    
    public void WriteLine(string message) ...
}

这很可能违背了你的LogService类的目的,因为现在消费者更好地通过直接注入ILoggable而不是注入LogService来使用。但这又让你回到了最初想要将该类设为静态的原因,即你有许多需要记录日志的类,将ILoggable注入所有这些构造函数可能感觉很繁琐。
然而,这可能是你代码中另一个设计问题导致的。为了理解这一点,你可能想要阅读这个问题和答案(link1:this q&a)来了解一些设计改变,以减少依赖于你的日志记录器类的类的数量。

那是一个很好的答案。我想以一种“大脑友好”的方式学习这些概念。有人能指点我一个好的资源吗?基本上,我想用“像我五岁一样解释”的方式理解答案中所说的任何内容。 - Debugger
3
我认为这些概念最好的资源是我们合著的书籍《依赖注入原理、实践与模式》(Dependency Injection Principles, Practices, and Patterns)。我肯定觉得它很容易理解,但并不适合五岁的孩子。这本书需要一定的软件开发经验。 - Steven
为什么不将日志记录服务作为实际服务呢?这不是断开式架构(服务架构)的重点吗?如果您不希望服务的调用者确定该服务的操作,那怎么办?将这些细节保持在一边,让服务仅提供接口(隐藏其操作的细节)是否更好?这不正是解耦的初衷吗? - MC9000
@MC9000:对我来说,具有具体“LoggerService”接口的设计并不是最直观的。通常情况下,您不希望类依赖于具体的“LoggerService”,而是将其隐藏在某种“ILogger”抽象之后。但是,不幸的是,问题对“LoggerService”的实现细节提供了太少的信息,以至于我无法就此发表强有力的声明。从 DI 的角度来看,在稳定依赖项本身是消费类的一部分的情况下,依赖于具体类是可以的。 - Steven

21

在静态类中使用依赖注入(DI)是没有意义的。不需要使用 DI,只需向您的静态类添加一个初始化方法并传入依赖项即可。

public static class LogService
{
    private static ILoggable _logger;

    public static ILoggable Logger
    {
        get
        {
             return _logger;
        }
    }

    public static void InitLogger(ILoggable logger)
    {
         _logger = logger;
    }
}

要使用记录器,只需确保首先调用InitLogger()

LogService.InitLogger(new FileLogger());
LogService.Logger.WriteLine("message");

1
InitLogger方法不需要返回类型吗?这不是构造函数...我有什么遗漏吗? - Jazimov
@Jazimov 它的返回类型是 void。 - Ch Usman
请注意,这个解决方案是环境上下文反模式的一种实现,我在我的回答中提到过。在决定是否应用它之前,请先了解一下这种模式的缺点。 - Steven

4
你可以对需要注入静态类的任何对象使用延迟初始化。

https://learn.microsoft.com/en-us/dotnet/api/system.lazy-1?view=net-5.0

这将使您能够传递静态对象,可在运行实例和其他需要使用这些对象的类/方法之间共享。例如,您可能希望在整个应用程序中共享的HttpClient。您可以在静态类内部惰性初始化HttpClient,并引用静态类以获取HttpClient。
下面是使用CosmosDB客户端的另一个示例: https://learn.microsoft.com/en-us/azure/azure-functions/manage-connections?tabs=csharp#azure-cosmos-db-clients

懒加载初始化与依赖注入有何等价关系? - John C
@JohnC OP 表示他想使用静态类。延迟初始化将允许他使用一个静态类,就像注入了该类的单例实现一样,在应用程序中的任何地方都可以使用。 - Brendon
1
请分享一些支持您的主张的示例代码,与ops示例相关。我查看了所有这些链接,但没有看到如何将其等效于静态方法的DI。 - John C

0

您可以通过使用方法注入器来找到类似于单例模式的东西

Private static Iconfiguration config;
Public static Iconfiguration Configuration
{
get{
    if(config == null){
     var builder = new Configuration builder().SetBasePath(Directory.GetCurrentDirectory()).AddJsonFile("app setting.josn");
     config = builder.Build();
     return = config;
    }
   else return config;
   }
}

然后在目标方法中调用 Configuration 祝好运

0
请使用这段代码。
using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection;

namespace SJVS.Framework.Logs

public class LogService {
    public static ILogger Logger =>
        new InjectScopeService<ILogContainer<LogContainer>>()
        .Inject(ApplicationBuilder).Logger;
    public static IApplicationBuilder ApplicationBuilder { get; set; } } public interface ILogContainer<T> {
    ILogger<T> Logger { get; } } public class LogContainer : ILogContainer<LogContainer> {
    public ILogger<LogContainer> Logger { get; set; }
    public LogContainer(ILogger<LogContainer> logger)
    {
        Logger = logger;
    } 
 }

public interface IInjectScopeService<TService> {
    TService Inject(IApplicationBuilder applicationBuilder); } public class InjectScopeService<TService>: IInjectScopeService<TService> {
    public TService Inject(IApplicationBuilder applicationBuilder)
    {
        using var scope = applicationBuilder.ApplicationServices.CreateScope();
        var service = scope.ServiceProvider.GetRequiredService<TService>();
        return service;
    } 
 }

并在 Program.cs 中设置 LogService.ApplicationBuilder:

LogService.ApplicationBuilder = app;

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