使用动态服务定位器和动态代理注入的 .NET 依赖注入






3.45/5 (8投票s)
使用动态服务定位器和动态代理注入的 .NET 依赖注入。
引言
我们所有人都在某个时候编写过代码,并希望在其他项目中共享或重用。编写代码越多,就越会意识到需要一种更快的方式来重用代码。其中一个问题是,有时你希望在多个项目中拥有相同的功能(从数据库读取数据、验证用户、发送电子邮件、发送短信以及加密/解密数据),但实现方式因项目而异。一个项目可能从 MySQL 读取数据,另一个项目可能从 MS SQL Server 读取数据。一个项目可能通过 SMTP 发送短信,另一个项目可能通过 SMPP 发送短信。因此,我们希望能够以最有效的方式实现这一点,我们希望我们的组件像乐高积木一样,只需最少的努力就能将小部件组合起来构建大型模型。
控制反转是编程中的一个通用概念,通常描述任何以某种方式反转控制的模式。依赖注入就是属于控制反转的一种模式。请查看 Martin Fowler 关于控制反转/依赖注入/服务定位器的这篇写得很好的文章。
文章大纲
文章将首先用一小段解释所使用的关键词。接下来,将展示一个示例,以便我们理解问题以及如何通过依赖注入模式解决它。
一旦示例展示完毕并理解了该模式,将展示一个 .NET 解决方案,使我们能够将实现与功能分离,并回顾本文附带的代码。
该解决方案更进一步,向读者展示一个动态代理生成器,它有助于以非常简单的方式在服务和组件之间注入代理对象。动态代理注入可用于多种用途;用于调试、日志记录、更好的测试、安全控制、远程执行等等。
接下来,将提供一个清晰的分步说明,展示如何在您的项目中使用代码。只对使用代码感兴趣的读者应直接跳转到这一段。最后,我们将重新审视这个小框架的优点,并为文章提供一个结论。
关键词
- 组件:任何需要其他代码(我们称之为服务)服务的代码片段。
- 服务:作为服务提供给组件的代码片段。
- 功能/契约/接口:这代表了定义服务的系列方法和属性;这些方法定义了您与代码交互的方式,让开发人员了解服务的作用而无需知道它是如何实现的。
- 实现:服务完成其应有职责的方式,代码的详细信息就是实现。同一个服务可以根据环境或部署的不同而以不同的方式实现。例如,发送短信的服务可以有两种实现,一种使用 SMTP 协议发送,另一种使用 SMPP 协议发送。
示例
与所有旨在解释某个观点的文章一样,本示例将尽可能简单和微不足道,以帮助解释本文中的思想,而不会不必要地使代码复杂化。
假设您正在编写一个应用程序,需要验证用户才能登录。您使用的身份验证机制将从您拥有的本地数据库中读取用户/密码列表。因此,这里的功能将是验证用户,而这种情况下,实现将是从您自己的数据库的用户表中读取用户。假设另一个方想使用您的应用程序/代码并希望将其部署在他们的站点。但是,他们不想维护另一组用户帐户,他们希望从他们已有的数据库表中获取用户的凭据。或者,他们甚至可能拥有一个 Windows 域,并希望从 Active Directory 验证用户。
通常,您必须进入您的代码并更改或重写您验证用户的那部分,以满足目标实现。相反,我们要做的是将功能与实现分离。这里的功能是一个简单的函数,它接受用户名和密码并返回登录信息是否被接受。实现将因不同的部署而异。一个有助于我们将功能与实现分离的面向对象编程概念是抽象类,也称为纯虚类,或接口。C# 拥有这两种,所以我们将继续使用接口。通过将您想要的方法分组到一个接口中,您将创建一个具有一组方法的接口(或契约),这些方法提供您需要的功能。接口只是规定了应该存在哪些函数以及函数接受和返回哪些参数(也称为方法签名)。在我们的简单示例中,它将是一个名为 `AuthenticateUser` 的简单方法,它接受用户名和密码并返回一个简单的布尔值,以指示用户/密码组合是否有效。接下来,您将创建一个从该接口继承的类。当您从接口继承时,编译器将强制您提供代码以填充您派生类的函数,从而强制实现。您填写的代码定义了接口中方法的实际实现。让我们看一下本文附带的代码。
public interface IAuthenticate
{
bool AuthenticateUser(string username, string password);
}
public class SampleAuthenticate : IAuthenticate
{
public bool AuthenticateUser(string username, string password)
{
return (username == "username" && password == "password");
}
}
现在,代码不再直接与 `SampleAuthenticate` 类交互并依赖它,我们将编写代码与 `IAuthenticate` 接口交互。这样,代码将编译并依赖接口,而不是包含实现代码的类或对象。然后,每当您的代码中需要接口时,您都必须创建一个派生(实现)自所需接口的类的实例。创建实现所需接口的对象的代码可以被认为是注入实现。因此,您正在注入依赖项,而不是让您的代码直接依赖于该类。请查看文件 *Form.cs* 和方法 `loginButton_Click`。现在,您的组件不再控制它想要使用哪个代码,而是由该组件外部的一些代码来决定使用哪个代码(此接口的特定实现),因此这是一种控制反转。现在,如果您需要使用 Active Directory 或任何其他实现来验证用户,您只需创建另一个同样继承自 `IAuthenticateUsers` 的类即可。稍后,我们通过更改注入哪个类来决定我们想要哪个实现。您只需更改一小段创建负责提供所需接口的对象的代码。
依赖注入通常通过几种方式实现:构造函数注入、设置器注入,甚至是接口注入。最常见的方式是使用构造函数注入。在这种情况下,应用程序中需要验证用户的任何代码都会在其构造函数中提供一个 `IAuthenticateUsers` 参数以及所有其他需要的接口。然后,在代码中,当您想要创建此类的实例时,您必须将您想要的任何对象传递给它的构造函数,只要它实现了所请求的接口;这就是控制反转。
当我们将功能(接口)与实现(从接口继承的类)分离时,我们解耦了需要验证用户(做什么)的代码与知道如何验证用户(怎么做)的代码。通常,在使用依赖注入模式时,有一段代码决定当请求接口时将提供哪些对象;这个类或代码被称为服务定位器,因为它定位一个服务(创建实现特定接口的适当类),并通过接口将服务传递给请求它的代码。服务定位器将知道,对于每个需要的接口,要创建哪个类,或者换句话说,对于每个需要的服务,提供哪个实现。为了进一步解耦,我们将依赖注入从编译时世界转移到运行时世界。服务定位器将需要在运行时从配置文件或注册表等配置实体中读取链接。我们的服务定位器现在将被称为动态服务定位器,因为它可以在运行时进行链接。如果您查看 `loginButton_Click` 中的以下代码
IAuthenticate iAuthenticate =
(IAuthenticate)DynamicServiceLocator.DynamicServiceLocator.
CreateImplementerByInterfaceName(typeof(IAuthenticate));
CreateImplementerByInterfaceName
方法接受所需的接口并返回一个实现所请求接口的对象。
我希望在文章的这一点上,依赖注入的思想及其好处对读者来说变得更加清晰。
动态代理生成器
我们的动态服务定位器能够在运行时创建一个类(实际上是编写代码)。这个动态类将实现所需的接口,并将封装原始服务。这个类,即代理类,将位于服务请求者和服务定位器之间,并拦截调用。使用本文提供的框架,这样做也非常容易。您只需在配置文件中添加几行配置;请参阅分步指南以获取配置详细信息。
使用代码的分步指南
- 通过定义代码中所需的接口来创建所需的功能。
- 通过编写继承自您创建的接口的类来创建所需的一个或多个实现。
- 当您的代码需要使用接口实例时,请使用 `DynamicServiceLocator.CreateImplementerByInterfaceName` 方法并将其传递给您想要的接口。
- 您需要修改 App.Config 文件,内容如下:
- 在 `<configuration>` 下,在 `<configSections>` 下,添加以下部分配置
- 在 `<configuration>` 下,添加以下 XML 节点
- 在 `<DynamicServiceLocator>` 标签内,添加一个或多个实现标签,如下所示
InterfaceName
:具有所需接口的类型,作为完整类型名称(接口的完整名称,程序集默认命名空间的名称)。InterfaceAssembly
:如果所需的接口在您自己的主程序集中,则为空;如果不是,则指定完整的程序集名称,如“example.dll”。如果您想注入代理对象,则必须创建一个实现 `IDynamicProxy` 的类,并且必须向上述标签添加两个值:`InjectIDynamicProxyClass` = "完整类型名称,程序集命名空间",以及 `InjectAssembly` 是包含您的代理类的程序集。
<section name="
DynamicServiceLocator"
type="DynamicServiceLocator.DynamicServiceLocatorConfiguration,
DynamicServiceLocator"
allowDefinition="MachineToApplication" />
<DynamicServiceLocator>
<add
InterfaceName="DependencyInjectionFor.NET.Contracts.IAuthenticate,
DependencyInjectionFor.NET"
InterfaceAssembly=""
ImplementerClass="DependencyInjectionFor.NET.Implementers.SampleAuthenticate,
DependencyInjectionFor.NET"
ImplementerAssembly="" />
幕后
当您调用 `DynamicServiceLocator` 并请求一个接口时,动态服务定位器(DSL)将从 *App.Config* 文件中找到该接口的条目。然后,DSL 将尝试从当前进程中已加载的程序集中加载类型 (`InterfaceName`);如果找不到,它将尝试从其文件名动态加载程序集 (`InterfaceAssembly`),如果成功,将尝试在加载的程序集中查找该类型。同样,它将尝试加载实现者类 (`ImplementerClass`, `ImplementerAssembly`)。加载接口和实现者对象后,它会确保实际上实现了所需的接口并返回该对象。如果 *App.Config* 为指定的接口指定了代理类 (`InjectIDynamicProxyClass`, `InjectAssembly`),它将尝试加载代理对象并将其放置在服务请求者和实际服务之间。
结论
使用依赖注入模式,它可以将您的编程习惯转变为小型完全解耦的组件,这些组件可以像乐高积木一样每次都能正确地组合在一起。这将通过快速集成您以前编写的组件来最大化您的代码重用。它将通过以非常清晰的方式将您的组件从您的项目中解耦,并通过消除项目中的代码重复来最大化您的代码可靠性。本文介绍的框架是允许您在 .NET 环境中使用依赖注入模式的框架。还有其他框架,但据我所知,没有一个将动态代理生成与依赖注入集成在一起。该框架注入动态代理对象的能力充分利用了依赖注入的强大功能。
非常欢迎评论。我希望我的文章能让很多开发者受益。