一文了解C#中的依赖注入(Dependency Injection)
依赖注入的作用
DI 有什么作用?DI 本身并不是目的,而是达到目的的一种手段。它的作用是帮助我们尽可能的编写可维护的代码,以便能实现高效地交付可运行的软件。
使代码具有可维护性的方法之一就是松散耦合。
也就是在《设计模式》第二版中提到过的:Program to an interface, not an implementation.(根据接口编程,而不是根据实现编程)
我们先通过两个基础的示例来简单理解一下DI的概念
创建一个控制台程序
添加一个接口IMessageWriter,它有一个接口函数Write
1 public interface IMessageWriter 2 { 3 void Write(string message); 4 }
然后我们定义一个实现类ConsoleMessageWriter
1 public class ConsoleMessageWriter : IMessageWriter 2 { 3 public void Write(string message) 4 { 5 Console.WriteLine(message); 6 } 7 }
接下来我们创建一个MessageService类
1 public class MessageService 2 { 3 private readonly IMessageWriter messageWriter; 4 5 public MessageService(IMessageWriter messageWriter) 6 { 7 this.messageWriter = messageWriter; 8 } 9 10 public void Exclaim() 11 { 12 this.messageWriter.Write("HelloWorld"); 13 } 14 }
最后我们在Main函数里进行调用
1 static void Main(string[] args) 2 { 3 IMessageWriter writer = new ConsoleMessageWriter(); 4 var service = new MessageService(writer); 5 service.Exclaim(); 6 }
这个程序的作用是:
程序需要向控制台写入信息,因此它创建了一个 ConsoleMessageWriter 类的实例,ConsoleMessageWriter类实现了IMessageWrite接口。
IMessageWrite提供了一个Write接口函数,我们在 ConsoleMessageWriter 类中进行实现。
最后,我们创建一个MessageService类,它是一个服务类。
程序将该消息写入器传递给 MessageService类,这样MessageService 实例就知道该在哪里写入消息。
上述代码就是一个最基础的依赖注入示例
我们从代码中来看一下它是如何做的。
MessageService类依赖于一个名为 IMessageWriter 的自定义接口,它通过构造函数获取该接口的实例,这就是所谓的构造函数注入。
IMessageWriter 实例随后将用于 Exclaim 方法中使用,该方法会将适当的消息写到控制台。

结合前面的MVVM系列文章,我们把这个示例稍微调整一下,并转换到WPF/MVVM程序中
新建一个WPF程序
首先我们还是建立IMessageWriter接口
1 public interface IMessageWriter 2 { 3 void Write(string message); 4 }
然后我们将实现类换成WPF中基于对话框的实现
1 public class MessageBoxWriter : IMessageWriter 2 { 3 public void Write(string message) 4 { 5 System.Windows.MessageBox.Show(message); 6 } 7 }
因为我们需要在ViewModel中调用MessageService,所以需要为MessageService也增加一个接口
1 public interface IMessageService 2 { 3 void Exclaim(); 4 }
MessageService的实现
1 public class MessageService : IMessageService 2 { 3 private readonly IMessageWriter messageWriter; 4 5 public MessageService(IMessageWriter messageWriter) 6 { 7 this.messageWriter = messageWriter; 8 } 9 10 public void Exclaim() 11 { 12 this.messageWriter.Write("HelloWorld"); 13 } 14 }
到这一步基本上跟前面的示例是一致的。但是在WPF中,我们需要将Main函数换成ViewModel,而MessageService的调用,来自于按钮的点击
我们创建一个界面,并放置一个按钮,将按钮的Command绑定到ShowMessageBoxCommand
1 <Window x:Class="IntroduceToDIWPF.MainWindow" 2 Title="MainWindow" Height="450" Width="800"> 3 <Grid> 4 <Button Content="Show" Command="{Binding ShowMessageBoxCommand}" HorizontalAlignment="Center" VerticalAlignment="Center"></Button> 5 </Grid> 6 </Window>
然后我们创建一个ViewModel
在ViewModel中,我们通过构造函数注入IMessageService,然后在按钮点击时,去调用IMessageService的接口。
1 public class MainWindowViewModel 2 { 3 private readonly IMessageService messageService; 4 5 public RelayCommand ShowMessageBoxCommand { get; private set; } 6 7 public MainWindowViewModel(IMessageService messageService) 8 { 9 this.messageService = messageService; 10 ShowMessageBoxCommand = new RelayCommand(ShowMessageBox); 11 } 12 13 private void ShowMessageBox() 14 { 15 this.messageService.Exclaim(); 16 } 17 }
最后一步,需要将IMessageService和IMessageWriter创建实例,也就是我们前面的示例中,Main函数里执行的逻辑。
1 static void Main(string[] args) 2 { 3 //创建实例 4 IMessageWriter writer = new ConsoleMessageWriter(); 5 var service = new MessageService(writer); 6 }
前面的示例中,不需要将MessageService进行注入,因为在Main函数中直接调用了。
1 static void Main(string[] args) 2 { 3 ... 4 service.Exclaim(); 5 }
在WPF/MVVM的示例中,我们需要将MessageService进行注入,供后续使用。
1 public partial class MainWindow : Window 2 { 3 public MainWindow() 4 { 5 InitializeComponent(); 6 7 //创建实例 8 IMessageWriter messageWriter = new MessageBoxWriter(); 9 IMessageService messageService = new MessageService(messageWriter); 10 11 MainWindowViewModel mainWindowViewModel = new MainWindowViewModel(messageService); 12 this.DataContext = mainWindowViewModel; 13 } 14 }
通过这两个示例,应该会对DI的概念有了初步的认识。
在后续的内容中,我们会介绍DI容器的使用,使用DI容器后,实例的过程就交给容器来完成,并统一在一个地方进行配置,代码的结构会更加清晰。
DI vs IoC
刚接触依赖注入的时候,我们一定会听到Ioc这个词语,那这两者有什么区别呢?
DI:Dependency Injenction,依赖注入。
Ioc:Invert of control,控制反转。
许多人将 DI 称为控制反转(IoC)。这两个术语有时可以互换使用,但 DI 是 IoC 的一个子集。
控制反转(Inversion of Control,IoC)一词的原意是指由整体框架或运行时控制程序流程的任何编程风格。
简单来说:
控制反转,是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。
其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),除了DI这种方式,还有其它的方式。
通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。

下面我们通过具体的案例对比使用DI和不使用DI的区别
DI容器UnityContainer的使用
通过XML配置UnityContainer
1、安装包Unity.Container和Unity.Configuration
2、创建IMyService和MyService
3、在App.config增加配置项
1 <?xml version="1.0" encoding="utf-8"?> 2 <configuration> 3 4 <configSections> 5 <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Unity.Configuration"/> 6 </configSections> 7 8 <startup> 9 <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.8" /> 10 </startup> 11 12 <unity xmlns="http://schemas.microsoft.com/practices/2010/unity"> 13 <alias alias="IMyService" type="WpfApp7.IMyService, WpfApp7" /> 14 <alias alias="MyService" type="WpfApp7.MyService, WpfApp7" /> 15 <containers> 16 <container name="ServiceProvider"> 17 <register type="IMyService" mapTo="MyService" /> 18 </container> 19 </containers> 20 </unity> 21 <runtime> 22 <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> 23 <dependentAssembly> 24 <assemblyIdentity name="Unity.Abstractions" publicKeyToken="489b6accfaf20ef0" culture="neutral" /> 25 <bindingRedirect oldVersion="0.0.0.0-5.11.7.0" newVersion="5.11.7.0" /> 26 </dependentAssembly> 27 <dependentAssembly> 28 <assemblyIdentity name="Unity.Container" publicKeyToken="489b6accfaf20ef0" culture="neutral" /> 29 <bindingRedirect oldVersion="0.0.0.0-5.11.11.0" newVersion="5.11.11.0" /> 30 </dependentAssembly> 31 </assemblyBinding> 32 </runtime> 33 </configuration>
4、加载配置项
1 public partial class App : Application 2 { 3 public IUnityContainer Container { get; set; } 4 5 protected override void OnStartup(StartupEventArgs e) 6 { 7 base.OnStartup(e); 8 9 Container = new UnityContainer(); 10 11 Container.LoadConfiguration("ServiceProvider"); 12 13 IMyService myService = Container.Resolve<IMyService>(); 14 myService.ShowMessage(); 15 } 16 }
Unity Container 对象生命周期
| 名称 | 描述 |
| Transient/TransientLifetimeManager | This is the default lifestyle. Instances aren’t tracked by the container |
| Container Controlled/ContainerControlledLifetimeManager | Unity’s name for SINGLETON. |
| Per Resolve/PerResolveLifetimeManager | Unity’s name for PER GRAPH. Instances aren’t tracked by the container. |
| Hierarchical/HierarchicalLifetimeManager | Ties the lifetime of components together with a child container |
| Per Thread/PerThreadLifetimeManager | One instance is created per thread. Instances aren’t tracked by the container. |
| Externally Controlled/ExternallyControlledLifetimeManager | A variation of SINGLETON where the container itself holds only a weak reference |
配置生命周期
通过代码配置
1 Container.RegisterType<IMyService, MyService>(new ContainerControlledLifetimeManager());
通过XML配置
1 <register type="IMyService" mapTo="MyService"> 2 <lifetime type="ContainerControlledLifetimeManager" /> 3 </register>
指定名称
1 Container.RegisterType<IMyService, MyService>("service"); 2 Container.RegisterType<IMyService, MyService2>("service2"); 3 4 //MyService 5 IMyService myService = Container.Resolve<IMyService>(); 6 7 //MyService 8 IMyService myServiceCopy = Container.Resolve<IMyService>("service"); 9 10 //MyService2 11 IMyService myService2 = Container.Resolve<IMyService>("service2");
1 Container.RegisterType<MyService>("service1");
重写自动注入
1 container.RegisterType<TabViewModel>( 2 new InjectionFactory( 3 (c, t, s) => new TabViewModel( 4 c.Resolve<StatusPanelViewModel>(), 5 c.Resolve<ChartViewModel>(), 6 c.Resolve<FlowController>())));
1 container.RegisterType<IMeal, ThreeCourseMeal>( 2 new InjectionConstructor( 3 new ResolvedParameter<ICourse>("entrée"), 4 new ResolvedParameter<ICourse>("mainCourse"), 5 new ResolvedParameter<ICourse>("dessert")));
参考资料
https://devonblog.com/software-development/configure-unity-container-config-file/

浙公网安备 33010602011771号