4、构造函数注入:构造器注入原理、多构造函数问题、循环依赖检测与解决

4.1 构造器注入的原理

构造器注入,说白了就是通过构造函数把依赖“塞”进来。这是我最推荐的一种注入方式,没有之一。

为什么?因为它强制你遵守一个原则:对象在创建时就必须是完整的。你想想看,如果一个类依赖某个服务,那这个服务就应该在对象出生时就准备好,而不是等对象活到一半再通过属性赋值。

我在项目中遇到过不少团队,一开始用属性注入觉得方便,结果后来发现对象状态不可控——有时候依赖是 null,有时候不是,排查起来特别头疼。后来统一改成构造器注入,这类问题基本绝迹了。

来看一个典型的例子:

public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly ILogger _logger;

    // 构造器注入:依赖通过参数传入
    public OrderService(IOrderRepository repository, ILogger logger)
    {
        _repository = repository ?? throw new ArgumentNullException(nameof(repository));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public void CreateOrder(Order order)
    {
        _logger.Log("开始创建订单...");
        _repository.Save(order);
    }
}

注意看,我在构造函数里做了 null 检查。这是我个人的习惯——防御式编程。一旦传入 null,直接抛异常,绝不藏着掖着。这样问题在启动时就能暴露,而不是等到运行时才炸。

核心要点:构造器注入保证了依赖的不可变性和非空性。只要对象构造成功,你就可以放心使用它的所有依赖。

4.2 多构造函数问题

嗯,这里要注意。当一个类有多个构造函数时,DI 容器会怎么选?

我记得有一次,一个同事写了两个构造函数:

public class PaymentService
{
    private readonly IPaymentGateway _gateway;
    private readonly ILogger _logger;

    // 构造函数 A:两个依赖
    public PaymentService(IPaymentGateway gateway, ILogger logger)
    {
        _gateway = gateway;
        _logger = logger;
    }

    // 构造函数 B:一个依赖
    public PaymentService(IPaymentGateway gateway)
    {
        _gateway = gateway;
        _logger = new NullLogger(); // 默认实现
    }
}

结果 DI 容器(比如 ASP.NET Core 自带的容器)直接报错了——它不知道选哪个。为什么?因为容器发现两个构造函数都能满足,就懵了。

不同的容器处理方式不一样:

DI 容器 多构造函数处理策略
ASP.NET Core 内置容器 选择参数最多的构造函数
Autofac 默认报错,需要显式指定
Unity 选择参数最多的构造函数
Castle Windsor 选择参数最多的构造函数

避坑指南:我曾经在一个项目里看到有人故意写多个构造函数,想通过参数数量来区分不同场景。结果换了个 DI 容器,整个系统就崩了。我的建议是——一个类只保留一个构造函数。如果确实需要不同的构造方式,用工厂模式或者建造者模式,别在构造函数上玩花样。

4.3 循环依赖检测与解决

循环依赖,说白了就是 A 依赖 B,B 又依赖 A。这在构造器注入里是个大坑。

为什么会这样?因为构造器注入要求对象在创建时就把所有依赖准备好。如果 A 需要 B,B 又需要 A,那谁先创建?这就成了死锁。

来看一个典型的循环依赖:

public class AService
{
    private readonly BService _b;
    public AService(BService b) => _b = b;
}

public class BService
{
    private readonly AService _a;
    public BService(AService a) => _a = a;
}

你试试注册这两个服务,DI 容器会直接抛出 CircularDependencyException。ASP.NET Core 的容器会检测到并告诉你:检测到循环依赖

4.3.1 如何检测循环依赖

大多数现代 DI 容器都内置了循环依赖检测。但如果你用的是自定义容器或者老版本,可以手动检测:

// 伪代码:检测循环依赖
public void ValidateDependencies(Type type, HashSet<Type> visited)
{
    if (visited.Contains(type))
    {
        throw new InvalidOperationException($"检测到循环依赖:{type.Name}");
    }

    visited.Add(type);
    var dependencies = GetConstructorDependencies(type);
    foreach (var dep in dependencies)
    {
        ValidateDependencies(dep, visited);
    }
    visited.Remove(type);
}

这个算法其实就是深度优先搜索。每访问一个类型,就把它加入已访问集合。如果再次遇到,说明有环。

4.3.2 如何解决循环依赖

我个人总结了三种解法:

  1. 重构设计(最推荐)
  2. 使用属性注入(临时方案)
  3. 引入中介者模式(架构级方案)

解法一:重构设计

循环依赖往往意味着职责划分不清。比如 A 和 B 互相调用,那是不是应该把公共逻辑抽出来?

// 重构前:A 和 B 互相依赖
public class OrderService
{
    private readonly INotificationService _notification;
    public OrderService(INotificationService notification) => _notification = notification;
}

public class NotificationService
{
    private readonly IOrderService _orderService;
    public NotificationService(IOrderService orderService) => _orderService = orderService;
}

// 重构后:引入 IOrderNotificationCoordinator
public class OrderNotificationCoordinator : IOrderNotificationCoordinator
{
    private readonly IOrderService _orderService;
    private readonly INotificationService _notification;
    // 现在两者都依赖协调器,不再互相依赖
}

解法二:属性注入(临时方案)

如果实在来不及重构,可以用属性注入打破循环。但这是下策,我一般只在遗留系统中用:

public class AService
{
    // 构造器注入 B
    private readonly BService _b;
    public AService(BService b) => _b = b;
}

public class BService
{
    // 属性注入 A(延迟解析)
    public AService A { get; set; }

    public void DoSomething()
    {
        // 注意:此时 A 可能为 null
        A?.SomeMethod();
    }
}

我的经验:属性注入虽然能解决循环依赖,但会引入 null 检查的麻烦。我曾经在一个项目里用属性注入解决循环依赖,结果上线后某个路径下 A 还没赋值就被调用了,直接 NullReferenceException。从那以后,我宁愿花时间重构,也不走这个捷径。

解法三:中介者模式

这是最优雅的解法。引入一个中介者,让 A 和 B 都只依赖中介者:

public interface IMediator
{
    void Notify(object sender, string eventName);
}

public class AService
{
    private readonly IMediator _mediator;
    public AService(IMediator mediator) => _mediator = mediator;

    public void DoWork()
    {
        _mediator.Notify(this, "AFinished");
    }
}

public class BService
{
    private readonly IMediator _mediator;
    public BService(IMediator mediator) => _mediator = mediator;

    public void DoWork()
    {
        _mediator.Notify(this, "BFinished");
    }
}

这样 A 和 B 不再直接依赖对方,循环依赖自然就解了。

4.4 小结

构造器注入是 DI 的基石。记住三点:

  • 一个类只留一个构造函数,避免容器选择困难
  • 循环依赖是设计问题,不是技术问题。遇到先重构,别急着用属性注入
  • 防御式编程:构造函数里做 null 检查,让问题暴露在起点

我个人觉得,构造器注入就像盖房子的地基——地基打好了,上面的结构才稳。别图一时方便用属性注入,后面维护起来真的会哭。