MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#中的依赖注入与控制反转模式

2021-05-038.0k 阅读

依赖注入与控制反转基础概念

控制反转(IoC)的本质

在传统的软件开发中,对象通常会自行创建其依赖对象。例如,在一个简单的订单处理系统中,订单服务可能会在自身代码内直接创建数据库连接对象来保存订单信息。这种方式使得订单服务高度依赖于数据库连接对象的具体实现,当数据库连接的实现方式发生变化,比如从使用 SQL Server 切换到 MySQL,订单服务的代码就需要大量修改。

控制反转(Inversion of Control,IoC)则颠覆了这种创建依赖对象的方式。它将对象创建和对象之间依赖关系的控制权从对象自身转移到了外部容器。这就好比一个人原本自己买菜做饭,现在改为去餐厅点餐,厨师(容器)负责准备饭菜(依赖对象)并提供给他。通过 IoC,对象不再负责创建其依赖对象,而是由外部容器创建并注入给它,这样对象就只专注于自身的业务逻辑,降低了对象间的耦合度。

依赖注入(DI)是 IoC 的具体实现方式

依赖注入(Dependency Injection,DI)是实现 IoC 的一种常见方式。它通过构造函数、属性或方法参数将依赖对象传递给需要它的对象。

  1. 构造函数注入:在对象的构造函数中接收依赖对象。例如,有一个CustomerService类依赖于CustomerRepository类来获取客户数据。
public class CustomerRepository
{
    // 模拟获取客户数据的方法
    public string GetCustomerData()
    {
        return "Customer data from repository";
    }
}

public class CustomerService
{
    private readonly CustomerRepository _customerRepository;

    public CustomerService(CustomerRepository customerRepository)
    {
        _customerRepository = customerRepository;
    }

    public string GetCustomerInfo()
    {
        return _customerRepository.GetCustomerData();
    }
}

在上述代码中,CustomerService通过构造函数接收CustomerRepository的实例,这样CustomerService的实例在创建时就必须传入一个CustomerRepository的实例,明确了依赖关系。

  1. 属性注入:通过对象的属性来设置依赖对象。
public class ProductRepository
{
    // 模拟获取产品数据的方法
    public string GetProductData()
    {
        return "Product data from repository";
    }
}

public class ProductService
{
    public ProductRepository ProductRepository { get; set; }

    public string GetProductInfo()
    {
        return ProductRepository?.GetProductData()?? "No product data available";
    }
}

这里ProductService通过ProductRepository属性接收依赖对象。属性注入的优点是对象可以在创建后再设置依赖,灵活性较高,但缺点是依赖对象可能未初始化就被使用。

  1. 方法注入:在对象的方法参数中传入依赖对象。
public class OrderRepository
{
    // 模拟获取订单数据的方法
    public string GetOrderData()
    {
        return "Order data from repository";
    }
}

public class OrderService
{
    public string GetOrderInfo(OrderRepository orderRepository)
    {
        return orderRepository.GetOrderData();
    }
}

方法注入通常用于对象在运行时根据不同情况需要不同依赖对象的场景。

C# 中实现依赖注入与控制反转的框架

1. Microsoft.Extensions.DependencyInjection

这是.NET 自带的依赖注入框架,广泛应用于.NET Core 项目。它提供了简单易用的 API 来注册服务和解析依赖。

注册服务

using Microsoft.Extensions.DependencyInjection;

class Program
{
    static void Main()
    {
        var services = new ServiceCollection();
        // 注册 Transient 服务
        services.AddTransient<ITransientService, TransientService>();
        // 注册 Scoped 服务
        services.AddScoped<IScopedService, ScopedService>();
        // 注册 Singleton 服务
        services.AddSingleton<ISingletonService, SingletonService>();

        var serviceProvider = services.BuildServiceProvider();
    }
}

public interface ITransientService { }
public class TransientService : ITransientService { }

public interface IScopedService { }
public class ScopedService : IScopedService { }

public interface ISingletonService { }
public class SingletonService : ISingletonService { }
  • Transient:每次请求都会创建一个新的实例。适用于轻量级、无状态的服务。
  • Scoped:在一个请求范围内(例如一个 HTTP 请求)共享同一个实例。常用于与数据库上下文相关的服务,在一次请求中确保数据库操作使用同一个上下文实例。
  • Singleton:整个应用程序生命周期内只有一个实例。适用于需要全局共享且状态不变的服务,如配置文件读取服务。

解析依赖

using Microsoft.Extensions.DependencyInjection;

class Program
{
    static void Main()
    {
        var services = new ServiceCollection();
        services.AddTransient<ITransientService, TransientService>();

        var serviceProvider = services.BuildServiceProvider();

        var transientService1 = serviceProvider.GetService<ITransientService>();
        var transientService2 = serviceProvider.GetService<ITransientService>();

        Console.WriteLine(transientService1 == transientService2); // 输出 False
    }
}

public interface ITransientService { }
public class TransientService : ITransientService { }

通过serviceProvider.GetService<T>()方法可以解析出所需的服务实例。

2. Autofac

Autofac 是一个功能强大的依赖注入框架,提供了更多高级特性,如属性注入的自动装配、基于约定的注册等。

安装 Autofac

可以通过 NuGet 包管理器安装Autofac包。

注册服务

using Autofac;

class Program
{
    static void Main()
    {
        var builder = new ContainerBuilder();

        // 注册类型
        builder.RegisterType<CustomerRepository>().As<ICustomerRepository>();
        builder.RegisterType<CustomerService>();

        var container = builder.Build();
    }
}

public interface ICustomerRepository { }
public class CustomerRepository : ICustomerRepository { }

public class CustomerService
{
    private readonly ICustomerRepository _customerRepository;

    public CustomerService(ICustomerRepository customerRepository)
    {
        _customerRepository = customerRepository;
    }
}

Autofac 的注册方式与Microsoft.Extensions.DependencyInjection类似,但语法略有不同。

属性注入的自动装配

using Autofac;

class Program
{
    static void Main()
    {
        var builder = new ContainerBuilder();

        builder.RegisterType<ProductRepository>().As<IProductRepository>();
        builder.RegisterType<ProductService>()
              .PropertiesAutowired();

        var container = builder.Build();
    }
}

public interface IProductRepository { }
public class ProductRepository : IProductRepository { }

public class ProductService
{
    public IProductRepository ProductRepository { get; set; }
}

通过.PropertiesAutowired()方法可以实现属性注入的自动装配,无需手动设置属性值。

3. Ninject

Ninject 是另一个流行的依赖注入框架,以其简洁的语法和强大的功能著称。

安装 Ninject

通过 NuGet 安装Ninject包。

注册服务

using Ninject;

class Program
{
    static void Main()
    {
        var kernel = new StandardKernel();

        kernel.Bind<IOderRepository>().To<OrderRepository>();
        kernel.Bind<OrderService>().ToSelf();
    }
}

public interface IOderRepository { }
public class OrderRepository : IOderRepository { }

public class OrderService
{
    private readonly IOderRepository _oderRepository;

    public OrderService(IOderRepository oderRepository)
    {
        _oderRepository = oderRepository;
    }
}

Ninject 使用Bind<T>().To<TImplementation>()的方式注册服务。

依赖注入与控制反转在不同应用场景中的应用

Web 应用中的应用

在 ASP.NET Core Web 应用中,依赖注入是构建可维护、可测试代码的关键。例如,在一个处理用户登录的控制器中:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;

public interface IUserRepository
{
    bool ValidateUser(string username, string password);
}

public class UserRepository : IUserRepository
{
    public bool ValidateUser(string username, string password)
    {
        // 实际实现可能查询数据库
        return username == "admin" && password == "password";
    }
}

public class AccountController : Controller
{
    private readonly IUserRepository _userRepository;

    public AccountController(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }

    [HttpPost]
    public IActionResult Login(string username, string password)
    {
        if (_userRepository.ValidateUser(username, password))
        {
            return RedirectToAction("Home");
        }
        return View("LoginFailed");
    }
}

class Program
{
    static void Main()
    {
        var services = new ServiceCollection();
        services.AddTransient<IUserRepository, UserRepository>();
        services.AddControllers();

        var serviceProvider = services.BuildServiceProvider();
    }
}

通过依赖注入,AccountController依赖于IUserRepository接口,而不是具体的UserRepository类。这使得在测试AccountController时可以很方便地使用模拟的IUserRepository实现,提高了代码的可测试性。同时,如果需要更换用户验证的方式,只需要创建新的IUserRepository实现并注册到依赖注入容器中,而不需要修改AccountController的代码。

单元测试中的应用

依赖注入在单元测试中起着至关重要的作用。以之前的CustomerService为例,假设我们要测试CustomerServiceGetCustomerInfo方法:

using NUnit.Framework;
using Moq;

[TestFixture]
public class CustomerServiceTests
{
    [Test]
    public void GetCustomerInfo_ShouldReturnCorrectData()
    {
        var mockCustomerRepository = new Mock<ICustomerRepository>();
        mockCustomerRepository.Setup(x => x.GetCustomerData()).Returns("Mocked customer data");

        var customerService = new CustomerService(mockCustomerRepository.Object);

        var result = customerService.GetCustomerInfo();

        Assert.AreEqual("Mocked customer data", result);
    }
}

public interface ICustomerRepository
{
    string GetCustomerData();
}

public class CustomerService
{
    private readonly ICustomerRepository _customerRepository;

    public CustomerService(ICustomerRepository customerRepository)
    {
        _customerRepository = customerRepository;
    }

    public string GetCustomerInfo()
    {
        return _customerRepository.GetCustomerData();
    }
}

通过依赖注入,我们可以使用模拟对象(如Mock<ICustomerRepository>)来替换真实的CustomerRepository,从而隔离CustomerService与外部依赖,专注于测试CustomerService自身的业务逻辑。

分层架构中的应用

在典型的三层架构(表示层、业务逻辑层、数据访问层)中,依赖注入可以有效地管理各层之间的依赖关系。例如,业务逻辑层的服务可能依赖于数据访问层的仓储接口:

// 数据访问层
public interface IProductRepository
{
    Product GetProductById(int id);
}

public class ProductRepository : IProductRepository
{
    public Product GetProductById(int id)
    {
        // 实际实现可能查询数据库
        return new Product { Id = id, Name = "Sample Product" };
    }
}

// 业务逻辑层
public class ProductService
{
    private readonly IProductRepository _productRepository;

    public ProductService(IProductRepository productRepository)
    {
        _productRepository = productRepository;
    }

    public Product GetProductDetails(int id)
    {
        var product = _productRepository.GetProductById(id);
        // 可以在此处添加更多业务逻辑,如计算产品价格等
        return product;
    }
}

// 表示层(例如 ASP.NET Core 控制器)
public class ProductController : Controller
{
    private readonly ProductService _productService;

    public ProductController(ProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public IActionResult GetProduct(int id)
    {
        var product = _productService.GetProductDetails(id);
        return View(product);
    }
}

通过依赖注入,各层之间的依赖关系清晰明了,数据访问层的实现可以方便地替换,业务逻辑层和表示层无需关心具体的数据访问实现,提高了整个系统的可维护性和可扩展性。

依赖注入与控制反转的高级话题

循环依赖问题及解决

循环依赖是依赖注入中可能遇到的问题,即两个或多个对象相互依赖。例如:

public class A
{
    private readonly B _b;

    public A(B b)
    {
        _b = b;
    }
}

public class B
{
    private readonly A _a;

    public B(A a)
    {
        _a = a;
    }
}

在上述代码中,A依赖于B,而B又依赖于A,形成了循环依赖。

  1. 使用构造函数注入时解决循环依赖:一些依赖注入框架(如Microsoft.Extensions.DependencyInjection)在遇到循环依赖时会抛出异常。解决方法之一是打破循环依赖,例如通过引入一个中间层来协调AB的依赖关系。
public class C
{
    private readonly A _a;
    private readonly B _b;

    public C(A a, B b)
    {
        _a = a;
        _b = b;
    }
}

public class A
{
    // 此时 A 不再直接依赖 B
    public A() { }
}

public class B
{
    // 此时 B 不再直接依赖 A
    public B() { }
}
  1. 使用属性注入解决循环依赖:在某些情况下,可以使用属性注入来延迟依赖对象的设置,从而避免循环依赖问题。但这种方式需要小心处理,确保依赖对象在使用前已正确初始化。
public class A
{
    public B BInstance { get; set; }

    public A() { }
}

public class B
{
    public A AInstance { get; set; }

    public B() { }
}

在依赖注入容器中,可以先创建AB的实例,然后再分别设置它们的属性,避免了构造函数注入时的循环依赖问题。

依赖注入与面向接口编程

依赖注入与面向接口编程紧密相关。面向接口编程提倡针对接口编程,而不是针对具体实现编程。在依赖注入中,对象依赖于接口而不是具体的类,这使得代码更加灵活和可维护。

例如,在一个日志记录系统中:

public interface ILogger
{
    void Log(string message);
}

public class FileLogger : ILogger
{
    public void Log(string message)
    {
        // 将日志写入文件的实现
        System.IO.File.AppendAllText("log.txt", message + Environment.NewLine);
    }
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

public class OrderProcessor
{
    private readonly ILogger _logger;

    public OrderProcessor(ILogger logger)
    {
        _logger = logger;
    }

    public void ProcessOrder()
    {
        _logger.Log("Order processing started");
        // 处理订单的业务逻辑
        _logger.Log("Order processed successfully");
    }
}

OrderProcessor依赖于ILogger接口,而不是具体的FileLoggerConsoleLogger类。这样,在不同的环境中,可以通过依赖注入容器轻松地切换日志记录的实现方式,例如在开发环境中使用ConsoleLogger,在生产环境中使用FileLogger,而OrderProcessor的代码无需修改。

依赖注入与 AOP(面向切面编程)

AOP 是一种编程范式,旨在将横切关注点(如日志记录、性能监控、事务管理等)从业务逻辑中分离出来。依赖注入可以与 AOP 很好地结合。

以日志记录为例,使用依赖注入和 AOP 可以实现对方法调用的自动日志记录:

using Castle.DynamicProxy;
using System;

public interface ICalculator
{
    int Add(int a, int b);
}

public class Calculator : ICalculator
{
    public int Add(int a, int b)
    {
        return a + b;
    }
}

public class LoggerInterceptor : IInterceptor
{
    public void Intercept(IInvocation invocation)
    {
        Console.WriteLine($"Calling method {invocation.Method.Name} with arguments {string.Join(", ", invocation.Arguments)}");
        invocation.Proceed();
        Console.WriteLine($"Method {invocation.Method.Name} returned {invocation.ReturnValue}");
    }
}

class Program
{
    static void Main()
    {
        var proxyGenerator = new ProxyGenerator();
        var calculator = new Calculator();
        var loggerInterceptor = new LoggerInterceptor();

        var proxy = proxyGenerator.CreateInterfaceProxyWithTarget<ICalculator>(calculator, loggerInterceptor);

        var result = proxy.Add(2, 3);
        Console.WriteLine($"Final result: {result}");
    }
}

在上述代码中,通过 Castle DynamicProxy 实现了 AOP,LoggerInterceptor作为切面在方法调用前后记录日志。依赖注入可以用于管理这些切面和目标对象的关系,使得代码更加模块化和可维护。例如,可以将LoggerInterceptor注册到依赖注入容器中,根据需要应用到不同的服务上。

通过深入理解依赖注入与控制反转模式,在 C# 开发中合理应用这些技术,可以构建出更加灵活、可维护、可测试的软件系统。无论是小型项目还是大型企业级应用,依赖注入与控制反转都能发挥重要作用,帮助开发者更好地管理对象之间的依赖关系,提升代码质量。