.net framework 下可以使用 Ninject 作为 DI 容器, 适合作为依赖注入的对象应该是功能型class, 而不是数据型class, 由DI 容器自动管理不同类的依赖关系和具体类的实现子类.

关于构造函数和属性注入的对比

  • 不推荐属性注入的主要原因是:
  1. 测试困难: 属性注入导致依赖硬编码在类中,不能通过构造函数正确初始化,允许测试。
  2. 顺序依赖: 当一个类依赖于多个属性时,它们的初始化顺序可能存在依赖关系。但是属性注入无法保证依赖的正确顺序。
  3. 难以debug: 因为属性注入是在运行时发生的,调试时很难追踪并理解。
  4. 难以理解: 对于一个新代码许多人来说,属性注入的依赖关系并不易于理解。
  • 相比之下,构造函数注入则不具备这些缺点:
  1. 在构造函数中明确地声明所有的依赖
  2. 通过构造函数的参数顺序保证了正确的依赖注入顺序
  3. 在代码中明确地声明了依赖关系,易于理解和调试
  4. 允许通过传递 mock 依赖实现单元测试

Ninject 简单示例

  1. StandardKernel 为 Ninject 的 DI 容器
  2. 使用容器完成一个接口和实现类的注入.
  3. 使用容器获取了一个接口的实现类对象.
using Ninject;

public interface IHello 
{
    void SayHello();
}

public class HelloImpl : IHello
{
    public void SayHello() 
    {
        Console.WriteLine("Hello from Ninject!");   
    }
}

class Program
{
    static void Main(string[] args)
    {
        // Create the kernel
        IKernel kernel = new StandardKernel();
        
        // Register the interface and implementation
        kernel.Bind<IHello>().To<HelloImpl>();  
        
        // Resolve the dependency
        IHello hello = kernel.Get<IHello>();
        
        // Use the dependency
        hello.SayHello();
    }  
}

Ninject 常用的几种绑定方式

  1. ToSelf(), 最简单, 直接绑定接口和实现类
  2. To(), 最常见, 显式绑定接口到实现类
  3. ToMethod(), 最强大, 该方式一般是使用 lambda 表达式动态创建一个被依赖对象的实例.
  4. ToConstructor(), 使用一个构造函数注入实现类实例

Ninject 使用一个构造函数注入实现类实例

下面的实现类构造器有一个string参数, 代码演示如何使用 Ninject 实现构造函数 .

interface IHello 
{
    void SayHello();
}

class HelloImpl : IHello
{
    public HelloImpl(string name)
    {
        Name = name;   
    }
    
    public string Name { get; set; }
    
    public void SayHello() 
    {
        Console.WriteLine($"Hello {Name}!");   
    }
}

class Program
{
    static void Main(string[] args)
    {
        IKernel kernel = new StandardKernel();
        
        kernel.Bind<IHello>().To<HelloImpl>();
        
        // Bind constructor argument
        kernel.Bind<string>().ToConstant("John");
        
        IHello hello = kernel.Get<IHello>();
        hello.SayHello();  // Prints "Hello John!"
    }
}

Ninject 使用 ToConstructor() 实现多参数构造器注入实例

kernel.Bind<IGreetingService>().ToConstructor(
    ctr => {
        ctr.WithConstructorArgument("name", "John");
        ctr.WithConstructorArgument("age", 30);
        ctr.WithConstructorArgument("isActive", true);   
    }
);

Ninject 使用ToMethod() 注入实现类实例的代码

kernel.Bind<IMyService>().ToMethod(ctx => {
   if(IsTestEnv()) {
       return new TestService();  
   } else {  
       return new ProdService();
   }
});

Ninject 按名称绑定

定义两个实现类:

public interface IGreetingService {...}

public class EnglishGreeting : IGreetingService {...}
public class ChineseGreeting : IGreetingService {...}

完成绑定和解析:

//绑定
kernel.Bind<IGreetingService>().To<EnglishGreeting>().Named("english");
kernel.Bind<IGreetingService>().To<ChineseGreeting>().Named("chinese");

//解析
IGreetingService english = kernel.Get<IGreetingService>("english");
IGreetingService chinese = kernel.Get<IGreetingService>("chinese");

Ninject 如何设置绑定作用域

kernel.Bind<IMyService>().To<MyService>()
    .InSingletonScope();

在 Ninject 中,还有其他作用域可用:

  • InTransientScope() : Transient是默认作用域,每次解析新建实例(非单例)
  • InThreadScope(): 与线程绑定,每个线程解析相同实例
  • InRequestScope(): 与请求绑定,每个请求解析相同实例
  • InSingletonScope: 每次解析依赖时返回相同的实例。

Ninject 真实案例代码

代码讲解:

  1. BusinessService 有logger和name两个构造函数参数
  2. 我们分别为这两个参数绑定值或依赖
  3. 使用 ToSelf(), BusinessService 本身作为依赖
  4. 然后解析 BusinessService 依赖时,Ninject 将先自动创建ConsoleLogger, 然后自动创建 BusinessService
interface ILogger {
    void LogInfo(string message);
}

class ConsoleLogger : ILogger {
    public ConsoleLogger(string name) { ... }
    
    public void LogInfo(string message) { ... }
}

class BusinessService {
    
    private ILogger logger;
    
    public BusinessService(ILogger logger, string name) {
        this.logger = logger;
        this.Name = name;   
    }
    
    public string Name { get; }

}

class Program
{
    static void Main() {
        IKernel kernel = new StandardKernel();
        
        // 绑定 logger 参数, 传入 "App"
        kernel.Bind<ILogger>().To<ConsoleLogger>();
        kernel.Bind<string>().ToConstant("App");
        
        // 绑定名称 string 参数
        kernel.Bind<string>().ToConstant("Business Service");
        
        // 绑定服务类, 没有接口, 所以绑定到自身
        kernel.Bind<BusinessService>().ToSelf();        
        
        //Ninject 将自动使用构造器 获取 BusinessService 对象
        var service = kernel.Get<BusinessService>();
        
        service.logger.LogInfo("Service Started!");    
    }
}

注册和解析不在一个文件的示例

下面代码中, 在Program.cs 中进行绑定一个singleton 实例, 在MainForm.cs 中进行解析实例. 要点:

  1. 将 Ninject 容器定义为静态属性, 其他类可以通过 Program.Kernel 来访问容器
// Program.cs
static class Program  
{
    public static IKernel Kernel { get; }  

    static Program() 
    {
        Kernel = new StandardKernel();
    }
}
public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();  
        
        // 解析依赖
        var service = Program.Kernel.Get<ISomeService>();
    }
}