65.9K
CodeProject 正在变化。 阅读更多。
Home

依赖注入与控制反转介绍

starIconstarIconstarIconstarIconstarIcon

5.00/5 (25投票s)

2016 年 2 月 1 日

CPOL

11分钟阅读

viewsIcon

43756

downloadIcon

1772

一个温和的介绍,通过我们大多数人写过的第一个程序——“Hello World”来解释 DI 和 IOC。

引言

写过的最有标志性的程序是什么?在我看来,就是程序员的第一个程序“Hello Word”——那是你看到自己努力的最初成果的时候。第一次将一个想法转化为编程世界的行动。

在这篇文章中,我把这个经典的编程示例弄得过于复杂了。将本应在一个解决方案中一个项目的一个文件中转换成五个独立的示例,第一个示例包含一个解决方案中的五个独立项目。

这些示例的目的不是要展示如何复杂地编写“Hello World”;而是以一种不被其他技术所干扰的方式来帮助解释依赖注入和控制反转。还有什么比让程序的‘核心’只做一件事——显示“Hello World”——更好的方式来做到这一点呢?

有许多依赖注入框架可供选择。这些示例使用了 Structuremap,因为它是最古老的框架之一,也是编写这些示例程序时的首选框架。

背景

最初,这些是为了向我工作的其他开发者解释这些原则的示例而汇编的,作为这次更新的一部分,我还将 Structuremap 从 2.x 版本迁移到了 4.x 版本。

什么是依赖注入

依赖是一个可以被使用的对象(服务)。注入是将一个依赖项传递给一个依赖对象(客户端)使其能够使用它。该服务成为客户端状态的一部分。将服务传递给客户端,而不是让客户端自己构建或查找服务,这是该模式的基本要求。

来源 - https://en.wikipedia.org/wiki/Dependency_injection

举个例子:

static void Main(string[] args)
{
      //Instead of service being created in Client, it is injected through the constructor
      service sc = new service();
      Client cc = new Client(sc);
       
      //Do work. 
      cc.DoSomeThing();
}

虽然依赖可以作为具体类传递给对象,但更普遍接受的是传递一个实现接口的对象。通过接口(契约)进行编程,可以让你传递任何实现该契约的具体类。

示例

  • Public class SQLDBConnection : IDBConnection
  • Public class ODBCConnection : IDBConnection
  • Public class OracleConnection : IDBConnection

所有类都共享相同的 IDBConnection 契约定义。

  • 通过接口进行编程可以让你不必担心实现细节
  • 可以使用工厂来创建具体类
  • IDBConnection = DBFactory(“配置文件”)

什么是控制反转?

在传统的编程中,业务逻辑的流程由与彼此静态绑定的对象决定。而在控制反转中,流程取决于在程序执行期间构建的对象图。这种动态流程是通过抽象来定义对象交互来实现的。这种运行时绑定是通过依赖注入或服务定位器等机制实现的。在 IoC 中,代码也可以在编译期间静态链接,但通过读取外部配置中的描述来查找要执行的代码,而不是直接在代码中进行引用。

在依赖注入中,一个依赖对象或模块在运行时与它需要的对象耦合。在程序执行期间哪个对象将满足依赖关系,通常在编译时无法通过静态分析得知。

来源 - https://en.wikipedia.org/wiki/Inversion_of_control

static void Main(string[] args)
{
     //Instead of service being created in Client, it is injected through the constructor
     //by the IoC Container
     using (var sm = new Injector.ContainerBootstrapper())
     {
          //Using an Interface to code against
          IClient cc = sm.GetInstance<IClient>();

          //Do work. 
          cc.DoSomeThing();
     }
}

具体类是如何注入的?

  • 工厂模式
  • 服务定位器模式
  • 依赖注入
    • 构造函数注入
    • 参数注入
    • Setter 注入
    • 接口注入
  • 其他模式

你将在本文中看到服务定位器模式和依赖注入的示例。

Structuremap

Structuremap 是一个用于 .NET 的依赖注入/控制反转工具,通过减少良好设计技术的机械成本来提高面向对象的系统的架构质量。

考虑使用 StructureMap 如果你

  • 需要显著的可扩展性
  • 只是想一个通用的配置工具
  • 希望支持多种部署配置
  • 使用测试驱动开发理念或希望在很大程度上自动化测试
  • 希望隔离一个有问题的子系统或为从遗留接口平稳迁移提供路径
  • 需要大量可配置的属性或插件热点

structuremap 创建的对象可以配置以下生命周期

  • PerRequest - 默认操作。每次请求都会创建一个新实例。
  • Singleton - 一个实例将在所有请求之间共享
  • ThreadLocal - 每个请求线程都会创建一个实例。使用 ThreadLocalStorage 缓存实例。
  • HttpContext - 每个 HttpContext 都会创建一个实例。在 HttpContext.Items 集合中缓存实例。
  • HttpSession - 每个 HttpSession 都会创建一个实例。在 HttpContext.Session 集合中缓存实例。谨慎使用。
  • Hybrid - 如果存在,则使用 HttpContext 存储;否则,则使用 ThreadLocal 存储。

其他 IOC 选项 – 部分列表

  • Microsoft Unity
  • Ninject
  • Castle Windsor
  • Spring.Net
  • Autofac

Using the Code

A_IOC – 我们的起点。仍然比直接编写要复杂

static void Main(string[] args)
{
     Console.Write("Hello World");
     Console.Read();
}

这个项目奠定了基础,所有其他解决方案都将以此为基础。随着本文的进展,您将被引导到每个解决方案中感兴趣的点进行比较和对比。

所有项目都包含以下项目

  • IOC - Main
         static void Main(string[] args)
         {
            LoggerClass.Logger l = 
              new LoggerClass.Logger(LoggerClass.Logger.LogLevel.Info,@"\logA","logfile");
            FacadeClass.FacadeClass f = new FacadeClass.FacadeClass();
    
            l.Log("Start", "Main", LoggerClass.Logger.LogLevel.Info);
            Console.Write(f.StringHelloWorld());
            l.Log("End", "Main", LoggerClass.Logger.LogLevel.Info);
                
            Console.Read();
         }
  • FacadeClass - 使用 ObjectHelloObjectWorld 来创建显示的 string
       public class FacadeClass
        {
            ObjectHello.HelloClass a = new ObjectHello.HelloClass();
            ObjectWorld.WorldClass b = new ObjectWorld.WorldClass();
    
            public FacadeClass()
            {
                a.CreateNewHello("Hello");
                b.CreateNewWorld("World");
            }
         
            public string StringHelloWorld()
            {
                return a.Hello + ' ' + b.World;
            }    
        }
  • ObjectHelloObjectWorld - 虚构的类,返回构造函数中传入的内容
        public class HelloClass
        {
            public void CreateNewHello(string hello)
            {
                Hello = hello;
            }
            public string Hello { get; set; }
        }
  • LoggerClass - 简单的日志记录器
  • 稍后,我们将添加两个名为 InterfaceClassInjector 的附加项目

如果我们查看每个部分是如何相互关联的,它看起来是这样的

B_IOC – 旅程开始

首先,请注意此解决方案包含一个名为 InterfaceClass 的新项目。这是一个项目,最终将成为本文和所有后续解决方案的焦点。

它只是我们保存解决方案中所有接口的地方。请记住,接口是一个契约,实现该接口的对象必须提供具体的实现。接口本身不包含任何实现代码,只有用于编程的签名。

    public interface IFacadeClass
    {
        //any class that implements this must satisfy this contract
        string DisplayStuff();
    }
    public class FacadeClass : Interfaces.IFacadeClass
    {

        Interfaces.IHelloClass a = new ObjectHello.HelloClass();
        Interfaces.IWorldClass b = new ObjectWorld.WorldClass();

        public FacadeClass()
        {
            a.CreateNewHello("Hello");
            b.CreateNewWorld("World");
        }

        //implementation of interface
        public string DisplayStuff()
        {
            return a.Hello + ' ' + b.World;
        }
   }

其次,请注意所有项目文件现在都派生自 Interfaces.IFacadeClass 并必须实现它们各自的契约。

第三,请注意局部变量已被替换为其接口对应项。

而不是

    LoggerClass.Logger l = new LoggerClass.Logger
                           (LoggerClass.Logger.LogLevel.Info,@"\logA","logfile");
    FacadeClass.FacadeClass f = new FacadeClass.FacadeClass();
        
    ObjectHello.HelloClass a = new ObjectHello.HelloClass();
    ObjectWorld.WorldClass b = new ObjectWorld.WorldClass();

我们现在有了

    ILogger l = new LoggerClass.Logger(Interfaces.LogLevel.Info, @"\logB", "logfile");
    Interfaces.IFacadeClass f = new FacadeClass.FacadeClass();
        
    Interfaces.IHelloClass a = new ObjectHello.HelloClass();
    Interfaces.IWorldClass b = new ObjectWorld.WorldClass();

代码图现在看起来是这样的

C_IOC – 准备注入依赖项

在这个项目中,唯一真正改变的是外观(facade)。我们将其更改为不依赖于在外观本身中实例化对象。按原样编写,此项目将编译但执行时会失败。StringHelloWorld 方法将因 null 引用错误而失败。

此外,我们正在将日志对象的引用传递给 facade 类以进行日志记录。

提供了注释以进行更正并成功运行。

    public class FacadeClass : Interfaces.IFacadeClass
    {
        Interfaces.IHelloClass a;
        Interfaces.IWorldClass b;

        public FacadeClass() { }

        //We can't call this directly since we have no reference to an ObjectHello or 
        //ObjectWorld in Program.cs.
        //We can add the reference and call this method. 
        //See Program.cs commented out section.
        public FacadeClass(Interfaces.IHelloClass ac, 
                           Interfaces.IWorldClass bc,Interfaces.ILogger lg)
        {
            a = ac;
            b = bc;
            //before using, make sure we have a reference.
            if (lg != null)
                //Since we have 
                lg.Log("Start", "Facade",Interfaces.LogLevel.Error);

            a.CreateNewHello("Hello");
            b.CreateNewWorld("World");
        }
        
        public string StringHelloWorld()
        {
            //Although this compiles, 
            //it does not run because we do not initialize variables 
            //a and b with instantiated objects
            //We are calling with base constructor and nothing gets instantiated
            //See Program.cs for possible correction. But this is desired effect for demo.
            return a.Hello + ' ' + b.World;
        }
    }

代码图(未运行)

请注意 ObjectABHelloWorld)已被创建,但没有任何东西依赖于它们。

现在对象已被包含。

static void Main(string[] args)
{
  //extraneous code removed.
  
  //This doesn't run
  Interfaces.IFacadeClass f = new FacadeClass.FacadeClass();

  // Uncomment this and include Object Hello and 
  // Object World in references and it should work
  // Interfaces.IFacadeClass f = new FacadeClass.FacadeClass
  // (new ObjectHello.HelloClass(), new ObjectWorld.WorldClass(), log);
}

D_IOC – 包含 structuremap

在此项目中,我们包含了一个名为 injector 的新项目,它有一个名为 ContainerBootStrapper 的类,该类将负责将具体类连接起来以注入到类的构造函数中。

您应该启用 NuGet 包还原,以确保自动获取 structuremap 程序集。

更改的项目包括

IOC - 使用容器返回日志记录器和外观的具体对象。

static void Main(string[] args)
{
    var sm = new Injector.ContainerBootstrapper();

    //Using the injector as a Service locator 
    //to return an instance of the logger and facade
    Interfaces.ILogger log = sm.GetInstance<Interfaces.ILogger>();       
    Interfaces.IFacadeClass f = sm.GetInstance<Interfaces.IFacadeClass>(); 

    if (log != null && f != null)
    {
        log.Log("Start", "Main", Interfaces.LogLevel.Warning);
        Console.Write(f.StringHelloWorld());
        log.Log("End", "Main", Interfaces.LogLevel.Warning);
    }
    Console.Read();
}

ObjectHello - 现在也进行日志记录,并且日志记录器作为构造函数的一部分被注入。

public class HelloClass : Interfaces.IHelloClass
{
   Interfaces.ILogger _lg;

   //You will notice we do not pass in an instance, we allow the Resolver to fix this.
   //ObjectWorld does not log, so it has just the default ctor.
   public HelloClass(Interfaces.ILogger lg)
   {
       _lg = lg;
   }

   string _hello = string.Empty;

   public void CreateNewHello(string hello)
   {
       if(_lg != null)
          _lg.Log("Create Hello","HelloClass",Interfaces.LogLevel.Info);
        _hello = hello;        
   }

   public string Hello { get { return _hello; } }
}

Injector - 这是一个应该尽可能靠近程序启动开始时实例化的类。您可以看到它在 Program.cs 的第一行就被调用了。

public class ContainerBootstrapper
{
    Container _c;

    public ContainerBootstrapper()
    {
            // Initialize a new container
            _c = new Container(x =>
            {
                x.For<Interfaces.IHelloClass>().Use<ObjectHello.HelloClass>();
                x.For<Interfaces.IWorldClass>().Use<ObjectWorld.WorldClass>();
                x.For<Interfaces.IFacadeClass>().Use<FacadeClass.FacadeClass>();
                //The class lifetime can change. Now instantiated once.
                x.For<Interfaces.ILogger>().Singleton().Use<LoggerClass.Logger>()
                    .Ctor<Interfaces.LogLevel>("levelset").Is(Interfaces.LogLevel.Info)
                    .Ctor<String>("pathname").Is(@"\logD")
                    .Ctor<String>("filename").Is("logfile");
            });
    }

    public T GetInstance<T>()
    {
       return _c.TryGetInstance<T>();  
    }     
}

前三行 format x.For<interface>().Use<concrete>() 将具体类映射到我们的接口。

第四行类似,但将其生命周期更改为单例,然后设置要传递给具体类 Logger 的参数。

在早期版本中,我们会这样创建一个日志记录器

Interfaces.ILogger log = 
     new LoggerClass.Logger(Interfaces.LogLevel.Info, @"\logC", "logfile");

现在它由容器创建

Interfaces.ILogger log = sm.GetInstance<Interfaces.ILogger>(); 
//level, location and name are set in the container creation.        

代码图现在看起来是这样的

请注意,由于 injector 仍然需要对每个对象进行引用来解析,因此所有项目 DLL 仍将自动导入到应用程序的 bin 目录中。

E_IOC - 几乎完成!

在此版本中,我们打破了将每个对象包含在容器中进行解析的依赖关系。此外,我有一个 AOP(面向切面编程)的示例,其中真实的具体类被一个类似的类包装成代理,并增加了集中式错误处理的功能。

IOC - 向容器添加了 IDisposable 契约(接口),以便我们可以将实例化包装在 using 语句中。

using (var sm = new Injector.ContainerBootstrapper())
{ ... }

Injector - 从将接口与具体类关联的流畅风格更改为扫描程序集。

public class ContainerBootstrapper : IDisposable
{
    Container _c;
    public ContainerBootstrapper()
    {
         // Initialize the static ObjectFactory container
         _c = new Container(x =>
         {
             x.Scan(s =>
             {
                   
               s.AssembliesFromApplicationBaseDirectory
                           (assm => assm.FullName.StartsWith("Object"));
               s.AssembliesFromApplicationBaseDirectory
                           (assm => assm.FullName.StartsWith("Facade"));
               s.AssembliesFromApplicationBaseDirectory
                           (assm => assm.FullName.StartsWith("Logger"));
               //This will use default assumption of object = IObject for mapping.
               s.WithDefaultConventions();
                     
             });
         });
         //The class lifetime can change. Now instantiated once.
         _c.Configure(a => a.ForSingletonOf<Interfaces.ILogger>());
         //Wrap the facade class with a proxy.
         _c.Configure(a => a.For<Interfaces.IFacadeClass>().DecorateAllWith<facadeAOP>());
       }
 
       public T GetInstance<T>()
       {
          return _c.TryGetInstance<T>();
       }

       public void Dispose()
       {
           _c.Dispose();
       }
   }   
}

请注意,项目添加了一个名为 facadeAOP 的新类,该类实现了与解析到接口的对象相同的接口。DecorateAllWith 将使用这个新类作为代理。代理将在需要时实例化具体的类,调用它以及我们需要记录错误的日志记录器。

public class facadeAOP : Interfaces.IFacadeClass
{
   Interfaces.IFacadeClass ic; 
   Interfaces.ILogger _log

   //These will automatically get resolved
   public facadeAOP(Interfaces.IFacadeClass fc,Interfaces.ILogger log)
   {
      ic = fc;
      _log = log;
   }

   public string StringHelloWorld()
   {
       try
       {
          return ic.StringHelloWorld();
       }
       catch (Exception ex)
       {
          _log.Log(ex.Message,"AOP",Interfaces.LogLevel.Error);
       }
   }
}

LoggerClass - 更改了其配置读取方式。在以前的示例中,我们可以将参数传递给构造函数。我尝试了这个版本,就像在 D_IOC 示例中那样传递参数,但在思考之后,我意识到确定它需要从配置文件中获取哪些参数的最佳类是 LoggerClass 本身。

public class Logger : ILogger
{
   Interfaces.LogLevel _levelset;
   string _Pathname;
   string _Filename;
        
   public Logger()
   {
      var logSetting = ConfigurationManager.AppSettings;

     _levelset = (Interfaces.LogLevel)Enum.Parse(typeof(Interfaces.LogLevel), 
          logSetting["levelset"],true);
     _Pathname = logSetting["pathname"];
     _Filename = logSetting["filename"];        
   }

   public void Log(string Message, string module, Interfaces.LogLevel level)
   { ... }
}

这个项目的代码图显示了对象不再是该项目构建的依赖项,并且只要它们实现接口,对象就可以轻松地被替换进出。

注意事项

由于核心程序不再引用这些项目(只引用接口和 injector),它们不会自动移动到应用程序的bin目录。这需要为每个构建的项目单独完成。

我将把构建后事件更改为在每个项目上始终运行。

您需要设置 IOC 的构建顺序,以便所有项目都先构建。

新增

E_IOC - Ninject 为所有代码忍者!

我向本文添加了一个新项目,展示了 Ninject 的使用,而不是 Structuremap IOC。更改仅在 injector 类中进行。程序的其他部分没有其他更改。

在 Structure map 中,配置是这样完成的

_c = new Container(x =>
     {
         x.Scan(s =>
         {
              s.AssembliesFromApplicationBaseDirectory
                     (assm => assm.FullName.StartsWith("Object"));
              s.AssembliesFromApplicationBaseDirectory
                     (assm => assm.FullName.StartsWith("Facade"));
              s.AssembliesFromApplicationBaseDirectory
                     (assm => assm.FullName.StartsWith("Logger"));
              s.WithDefaultConventions();
         });
     });
     //The class lifetime can change. Now instantiated once.
     _c.Configure(a => a.ForSingletonOf<Interfaces.ILogger>());
     _c.Configure(a => a.For<Interfaces.IFacadeClass>().DecorateAllWith<facadeAOP>());
                

在 Ninject 中,配置是这样完成的

_c =new StandardKernel();

_c.Bind(b => b.FromAssembliesMatching("Facade*.*")
          .SelectAllClasses()
          .BindDefaultInterfaces()
          .Configure(c => { c.InSingletonScope(); c.Intercept().With<facadeAOP>(); }));
_c.Bind(b => b.FromAssembliesMatching("Object*.*").SelectAllClasses().BindDefaultInterfaces());
_c.Bind(b => b.FromAssembliesMatching("Logger*.*").SelectAllClasses().BindDefaultInterfaces());

两者都使用了“约定优于配置”来将类映射到接口。它们都将外观类定义为单例,并且都使用另一个类来装饰/拦截 facade 类,将其包装在 try-catch 中。

Structuremap 中,facadeAOP 类被定义为与我们正在装饰的类型相同的类。

public class facadeAOP : Interfaces.IFacadeClass
    {
        Interfaces.IFacadeClass ic;
        Interfaces.ILogger _log;

        //notice we inject the logger and facadeclass using IOC 
        public facadeAOP(Interfaces.IFacadeClass fc,Interfaces.ILogger log)
        {
            ic = fc;
            _log = log;
        }

        //We need to create an object which satisfies the interface
        public string StringHelloWorld()
        {
            try
            {
                //notice we execute the wrapped classes method in a try catch
                return ic.StringHelloWorld();
            }
            catch (Exception ex)
            {
                _log.Log(ex.Message, "AOP", Interfaces.LogLevel.Error);
                return "Error occured - " + ex.Message;
            }
        }
    }

对于 Ninject,我们以类似的方式完成,但是我们将类稍微修改为实现 Ninject 的 IInterceptor 接口。由于我们使用的是 Ninject 的接口,我们需要相应地进行更改。

 public class facadeAOP : IInterceptor
    {
        Interfaces.ILogger _log;

        //notice we still use IOC to pass in instance of logger 
        //but no longer pass in instance of facadeclass
        public facadeAOP(Interfaces.ILogger log)
        {
            _log = log;
        }
                
        //Satisfies the IInterceptor interface
        public void Intercept(IInvocation invocation)
        {
            try
            {
                //invoke the intercepted class
                invocation.Proceed();
            }
            catch (Exception ex)
            {
                _log.Log(ex.Message, "AOP", Interfaces.LogLevel.Error);
                Console.WriteLine("Error occured - " + ex.Message);
            }
        }
    }

Ninject 实现可以用于所有类型的对象,而不是 structuremap,后者需要为每个被装饰的接口创建一个。

我还必须更改以将 dll 文件复制到 bin 目录。

copy $(SolutionDir)Injector\$(OutDir)ninject*.* $(SolutionDir)IOC\bin\Debug
copy $(SolutionDir)Injector\$(OutDir)linfu*.* $(SolutionDir)IOC\bin\Debug

E_IOC - Autofac

在 Autofac 中,配置是这样完成的

public ContainerBootstrapper()
{
   var builder = new ContainerBuilder();    
               
   //load the DLLs and pass into the RegisterAssemblTypes
   var facade = Assembly.LoadFrom("FacadeClass.dll");
   var logger = Assembly.LoadFrom("LoggerClass.dll");
   var hello = Assembly.LoadFrom("ObjectHello.dll");
   var world = Assembly.LoadFrom("ObjectWorld.dll");

   //intercept the facade with a class that implements the class IInterceptor
   builder.RegisterAssemblyTypes(facade).
          As<Interfaces.IFacadeClass>().
          EnableInterfaceInterceptors().
          InterceptedBy(typeof(facadeAOP));

    //Create the Logger as a singleton 
    builder.RegisterAssemblyTypes(logger).As<Interfaces.ILogger>().SingleInstance();
    builder.RegisterAssemblyTypes(hello).As<Interfaces.IHelloClass>();
    builder.RegisterAssemblyTypes(world).As<Interfaces.IWorldClass>();

    //register a new facadeAOP class passing in a resolved logger to log with.
    builder.Register(r => new facadeAOP(r.Resolve<Interfaces.ILogger>()));
             
    //Build creates the container.
    _c = builder.Build();
 }

Autofac 也以类似 Ninject 的方式使用拦截器。

请注意,您可以使用 builder 注册组件,然后使用 Build 返回一个容器。

Unity - 无示例

所以有一个容器我没有接触过,那就是 Microsoft 的 Unity。
主要原因是 Structuremap 切换到使用约定优于配置来加载。

Unity 容器使用 XML 配置将实例映射到具体类型。

XML 配置与 structuremap 的 2.x 版本非常相似,并且类似于下面的内容。

Unity 使用

<configSections>
    <section name="unity" 
     type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, 
     Microsoft.Practices.Unity.Configuration"/>
</configSections> 

<unity xmlns="http://schemas.microsoft.com/practices/2010/unity">
       
    <container>
         <register type="IHelloClass" mapTo="ObjectHello.HelloClass"/>
         <register type="IWorldClass" mapTo="ObjectWorld.WorldClass" /> <!-- type mapping -->
         <register type="IFacadeClass mapTo="FacadeClass" />
         <register type="ILogger" mapTo="LoggerClass"> 
               <lifetime type="singleton" /> 
         </register>
    </container>

</unity>

关注点

Structuremap 的 2.X 版本中,可以通过 XML 配置从配置文件中读取内容来配置自身。从 3.0 版本开始,此功能已被移除,并且不再支持。

我发现以这种方式构建这些项目是一种很好的学习方式,它们逐层叠加,使得您的注意力只集中在变化上。如果您觉得这些有帮助,或者有任何问题,请留言。

切换到 Ninject 仅仅是理解 Ninject 和 Structuremap 之间的区别,而程序的其他任何部分都没有其他更改。

历史

  • 2016 年 2 月 1 日:初始发布
  • 2016 年 2 月 12 日:更新 ninject
  • 2016 年 2 月 16 日:将 Ninject 更改为正确地将日志记录器创建为单例。添加了 AutoFac 解决方案。
  • 2016 年 2 月 26 日:涉及 Unity 进行 IOC。
© . All rights reserved.