第十一章-基于面向切面方面的编程
本章是我们从第10章开始的面向方面编程(AOP)讨论的延续。第10章以最纯粹的形式描述了AOP,即仅使用SOLID设计实践应用AOP,本章从工具着手研究AOP。基础的观点。 我们将讨论两种应用AOP的常用方法:动态拦截和编译时编织。
如果第10章的设计方法过于激进,那么动态拦截将是您的下一个最佳选择,这就是我们首先讨论它的原因。 动态拦截可能是一个不错的临时解决方案,直到合适的时机到来开始进行上一章中讨论的各种改进。
编译时编织与DI相反,我们认为它是一种反模式(anti-pattern)。但是,我们认为必须包含有关编译时织造的讨论,这很重要,因为它是AOP的一种公知形式,并且我们想清楚地表明,它不是DI的可行替代品。
动态拦截(Dynamic Interception)
10.1节的代码清单实现了CircuitBreakerProductRepository装饰器(Decorator)
的Delete
和Insert
方法,其中包含代码重复。 以下清单再次显示了此代码。
清单11.1违反DRY原则(重复) (代码的味道)
public void Delete(Product product)
{
this.breaker.Guard();
try
{
this.decoratee.Delete(product);
this.breaker.Succeed();
}
catch (Exception ex)
{
this.breaker.Trip(ex);
throw;
}
}
public void Insert(Product product)
{
this.breaker.Guard();
try
{
this.decoratee.Insert(product);
this.breaker.Succeed();
}
catch (Exception ex)
{
this.breaker.Trip(ex);
throw;
}
}
<--这两种方法的代码看起来很像模板。 它们几乎相同,唯一的不同是对Delete和Insert方法的调用。
将装饰器(Decorator)作为方面实现的最难的部分是设计模板。之后,这是一个相当机械的过程:
- 创建一个新的装饰器(Decorator)类
- 从所需的界面派生
- 通过应用模板来实现每个接口成员
此过程是如此重复,以至于您可以使用工具来使其自动化。 .NET Framework
的许多强大功能之一是能够动态发出类型。 这样就可以编写在运行时生成功能齐全的类的代码。这样的类没有基础的源代码文件,而是直接从某些抽象模型编译而来。 这使您能够自动生成在运行时创建的装饰器。 如图11.1所示,这就是动态拦截使您可以执行的操作。
创建动态生成的装饰器(Decorator)及其依赖关系的对象图之后,装饰器(Decorator)可以用作真实类的替代。因为它实现了真实类的抽象类,所以可以将其注入使用该抽象类的客户端中。图11.2描述了客户端调用其拦截抽象时的方法调用流程。
要使用动态拦截,您仍然必须编写实现方面的代码。这可能是断路器方面所需的管道代码,如清单11.1所示。完成此操作后,您必须告知动态拦截器(Interception)库有关应该将方面应用到的抽象类的信息。理论已经足够,让我们来看一个例子。
图11.1 动态拦截器(Interception)库在运行时生成装饰器(Decorator)类。 每个给定的抽象都会发生一次(在这种情况下,对于IRepo)。 生成过程完成后,您可以请求拦截器(Interception)库为您提供该装饰器(Decorator)的新实例,同时提供目标和拦截器。 |
---|
图11.2 客户端调用其拦截抽象时的方法调用流程 |
---|
如代码清单11.1所示。完成此操作后,您必须告知动态拦截器(Interception)库有关应该将方面应用到的抽象类的信息。理论已经足够,让我们来看一个例子。
示例:使用Castle Dynamic Proxy
进行拦截 (Example: Interception with Castle Dynamic Proxy)
凭借其重复的代码,清单中的断路器方面非常适合动态拦截。尽管您可以编写在运行时生成装饰器(Decorator)s的代码,但这是一项相当复杂的操作,此外,已经有出色的工具可供使用。我们将无需直接进行手工生成代码的繁琐过程,而是直接使用工具。例如,让我们看看如何使用Castle Dynamic Proxy
的拦截功能减少代码重复。
注
Castle Dynamic Proxy
是.NET中用于动态拦截的事实上的标准工具。它是免费和开源的。实际上,大多数DI容器(DI Container)都在其内部使用其动态拦截功能。其他动态拦截工具也可以使用,但是Castle
已经成熟并且经受了时间的考验,因此我们将重点讨论Castle
。
实施断路器拦截器(Implementing a Circuit Breaker Interceptor)
实现Castle的拦截器要求您实现其Castle.DynamicProxy.IInterceptor
接口,该接口由一个方法组成。下面的清单显示了如何从清单11.1实现断路器。 但是,与该清单不同的是,下面显示了整个类。
清单11.2 用动态代理实现断路器拦截器
public class CircuitBreakerInterceptor
: Castle.DynamicProxy.IInterceptor
{
private readonly ICircuitBreaker breaker;
public CircuitBreakerInterceptor(
ICircuitBreaker breaker)
{
this.breaker = breaker;
}
public void Intercept(IInvocation invocation)
{
this.breaker.Guard();
try
{
invocation.Proceed();
this.breaker.Succeed();
}
catch (Exception ex)
{
this.breaker.Trip(ex);
throw;
}
}
}
与清单11.1的主要区别在于,您必须更加通用,而不是将方法调用委派给特定的方法,因为您可以将此代码应用于可能的任何方法。 作为参数传递给Intercept
方法的IInvocation
接口表示方法调用。 例如,它可能表示对Insert(Product)方法的调用。Proceed
方法是此接口的关键成员之一,因为它使您可以让调用继续进行到堆栈上的下一个实现。
IInvocation
接口使您可以在继续进行调用之前分配一个返回值。它还提供对有关方法调用的详细信息的访问。从调用参数中,您可以获得有关方法名称和参数值的信息,以及有关当前方法调用的其他信息。实施拦截器是困难的部分。下一步很容易。
使用Pure DI将拦截器应用到组合根(Composition Root)内部
以下清单显示了如何将CircuitBreakerInterceptor
合并到组合根(Composition Root)中。
清单11.3 将拦截器并入组合根(Composition Root)
var generator =
new Castle.DynamicProxy.ProxyGenerator(); <---Castle的ProxyGenerator可以在运行时生成代理类型。
var timeout = TimeSpan.FromMinutes(1);
var breaker = new CircuitBreaker(timeout);
var interceptor =
new CircuitBreakerInterceptor(breaker); <--拦截器使用构造函数注入在其构造函数中接受ICircuitBreaker的实例。
var wcfRepository = new WcfProductRepository(); <--创建真实的IProductRepository实例
IProductRepository repository = generator
.CreateInterfaceProxyWithTarget<IProductRepository>(
wcfRepository,
interceptor); <--请求Castle基于IProductRepository接口创建一个Decorator(代理),并将其包装在原始Repository实例和新创建的Interceptor周围
提示 为了获得良好的性能,通常应一次创建
Castle.DynamicProxy.ProxyGenerato
r并在应用程序的生存期内对其进行缓存。
此示例说明,尽管Castle
控制着IProductRepository
装饰器(Decorator)的构造及其依赖关系的注入,但您仍然可以使用Pure DI引导应用程序。 在下一节中,我们将分析动态拦截,并讨论其优缺点。
动态拦截分析 (Analysis of dynamic Interception)
当我们将上一章中讨论的基于工具的AOP动态截获方法与基于设计方法的AOP方法进行比较时,我们发现两者之间存在许多相似之处:
- 当您针对抽象进行编程时,每种方法都可以解决横切关注点(Cross-Cutting Concerns)。
- 与普通的旧装饰器一样,拦截器可以使用构造函数注入(Constructor Injection),这使它们对DI友好并与正在装饰的代码脱钩。 这些特征使您的业务代码和方面都易于测试。
- 方面可以集中在组合根(Composition Root)中,这可以防止代码重复,并且如果您的Visual Studio解决方案包含多个应用程序,则它允许将方面应用到一个“组合根(Composition Root)”中,而不能应用在其他组合根(Composition Root)中。
尽管存在这些相似之处,但仍有一些差异使动态拦截效果不理想。 表11.1总结了不利之处,我们将在下面讨论。
表11.1 动态拦截的缺点
坏处 | 概括 |
---|---|
失去编译时支持。 | 拦截代码往往比装饰器(Decorator)更为复杂,这使它的读取和维护变得更加困难。 |
方面与工具紧密相关。 | 这种耦合使测试变得更加困难,并且迫使拦截器成为“聚合根(Composition Root)”的一部分,以防止其他程序集需要对动态拦截库的依赖。 |
并非普遍适用 | 方面只能应用于虚拟或抽象方法的边界,例如属于接口定义的方法。 |
无法解决潜在的设计问题。 | 您仍然会得到一个比基于SOLID的设计仅稍微多一点且难以维护的系统。 |
失去编译时支持(Loss of compile-time support)
与普通的旧装饰器(Decorator)相比,动态拦截每次使用拦截时都会涉及大量的运行时反射调用。例如,对于Castle,IInvocation
接口包含Arguments
属性,该属性返回对象实例的数组,该对象实例包含方法参数的列表。读取和更改这些值涉及在整数和布尔值之类的值类型的情况下进行强制转换和装箱。从性能的角度来看,这种持续不断的反思负担在很大程度上可以忽略不计。典型的I/O操作(例如数据库读取和写入)的成本要高几个数量级。
但是,这种反射的使用会使您编写的拦截器复杂化。处理方法参数和返回类型列表时,您必须编写正确的转换和类型检查,并可能更有效地传达转换错误。因此,拦截器比装饰器要复杂得多,这使它的阅读和维护变得更加困难。
方面与工具紧密相关(Aspects are strongly coupled to tooling)
与普通的装饰器(Decorator)相比,使用动态拦截编写的拦截器与使用的拦截器(Interception)库紧密相关。清单11.2的CircuitBreakerInterceptor
就是一个很好的例子。该拦截器实现Castle.DynamicProxy.IInterceptor
并利用Castle.DynamicProxy.IInvocation
抽象。
尽管不如编译时编织普及,但您会在11.2节中看到,但这导致所有方面都与Castle Dynamic Proxy
库结合在一起。这种耦合引入了对需要学习的外部库的额外依赖,这给项目带来了额外的成本和风险。我们将在12.3.1节中详细说明。
并非普遍适用(Not universally applicable)
由于动态拦截是通过使用动态生成的装饰器包装现有抽象来进行工作的,因此只能在抽象的方法边界处扩展类的行为。私有方法不属于接口,因此无法被拦截。
当通过设计实践AOP时,此限制也成立。但是,通过设计AOP,通常不会出现问题,因为您可以以仅在抽象边界处应用方面的方式设计抽象。另一方面,当您应用动态拦截时,您通常会接受现状,因为如果不这样做,最终将设计使然。
它不能解决潜在的设计问题(It doesn’t fix underlying design problems)
在第10章中,我们广泛讨论了大型IProductService
接口存在的设计问题,以及如何通过应用SOLID原理来解决这些问题。 正如讨论的那样,这些问题对系统的影响最大,不包括与横切关注点问题有关的任何问题。
但是,当您接受应用程序当前设计的现状时,可以使用动态拦截。您希望能够应用交叉切割问题,而不必应用大型重构。 这样做的缺点是只能解决部分问题。 您仍然会得到一个比现有设计仅具有稍微更高的可维护性且比基于SOLID的设计更难以维护的系统,这是因为动态拦截仅考虑了问题的应用,而不是代码的其他部分。
应用动态拦截功能需要您对接口进行编程并使用第4章中的DI模式。另一种不需要对接口进行编程的AOP形式是编译时编织。乍一看这听起来很吸引人,但是正如我们接下来将要讨论的那样,这是一种DI反模式(anti-pattern)。
编译时编织(Compile-time weaving)
当我们作为开发人员编写C#代码时,C#编译器会将我们的代码转换为Microsoft中间语言(IL)。 IL由公共语言运行时(CLR)实时(JIT)编译器读取,并当场翻译为机器指令以供CPU执行。 您很可能会熟悉此过程的基础。
编译时编织是一种常见的AOP技术,可以更改此编译过程。 它使用特殊的工具读取由我们的(C#)编译器生成的已编译程序集,对其进行修改并将其写回到磁盘,从而有效地替换了原始程序集。 图11.3显示了此过程。
图11.3 编译时编织过程 |
---|
为了在后编译过程中更改原始编译的程序集,目的是将各方面编织到原始源代码中,如图11.4所示。
图11.4 可视化的编译时编织 |
---|
但是,乍一看似乎很诱人,但当将其应用于易变依赖项时,使用编译时编织会带来一些问题,这些问题从可维护性的角度来看使该技术成为问题。 由于存在这些不利因素,如本节所述,我们认为编译时织法与DI背道而驰-这是DI反模式(anti-pattern)。
重要 编译时编织不是将AOP应用于易失性依赖项的理想方法。建议您采用SOLID原则,或者在不可能的情况下退回动态拦截。
如引言中所述,我们发现讨论编译时编织很重要,即使它是DI反模式(anti-pattern)。编译时编织是AOP的一种众所周知的形式,我们必须警告它的使用。 在讨论它为什么会带来问题之前,我们将以一个示例开始。
示例:使用编译时编织应用事务方面(Example: Applying a transaction aspect using compile-time weaving)
属性与装饰器具有相同的特征:尽管它们可以添加或暗示成员行为的更改,但它们使签名和原始源代码保持不变。在9.2.3节中,您使用装饰器应用了安全方面。但是,编译时编织工具使您可以通过在类,类的成员甚至程序集上放置属性来声明方面。
使用此概念来应用问题听起来很有吸引力。 如果您可以用[Transaction]
属性或自定义[CircuitBreaker]
属性装饰方法或类,然后用单行的声明性代码来应用方面,那岂不是很好吗? 下面的清单显示了如何将自定义TransactionAttribute
方面属性直接应用于SqlProductRepository
的方法。
清单11.4 将[Transaction]方面属性应用于
SqlProductRepository
public class SqlProductRepository : IProductRepository
{
[Transaction] <--自定义方面属性将应用于所有Insert,Update和Delete方法,而其代码保持不变。
public void Insert(Product product) ...
[Transaction] <--自定义方面属性将应用于所有Insert,Update和Delete方法,而其代码保持不变。
public void Update(Product product) ...
[Transaction] <--自定义方面属性将应用于所有Insert,Update和Delete方法,而其代码保持不变。
public void Delete(Guid id) ...
public IEnumerable<Product> GetAll() ... <--并非所有方法都需要事务逻辑,因此该方法未使用属性进行标记。
...
}
注 我们使用术语
Aspect
属性来表示在类或其实现或表示方面的成员上声明的自定义属性。
尽管您可以选择许多编译时编织工具,但在本节中,我们将使用PostSharp(https://www.postsharp.net/)
,这是一种商业工具。 下面的清单显示了使用PostSharp
的TransactionAttribute
的定义。
清单11.5 用
PostSharp
实现TransactionAttribute
方面
[AttributeUsage(AttributeTargets.Method |
AttributeTargets.Class |
AttributeTargets.Assembly,
AllowMultiple = false)]
[PostSharp.Serialization.PSerializable]
[PostSharp.Extensibility.MulticastAttributeUsage(
MulticastTargets.Method,
TargetMemberAttributes =
MulticastAttributes.Instance |
MulticastAttributes.Static)] <[[PostSharp方面的必需属性]]
public class TransactionAttribute
: PostSharp.Aspects.OnMethodBoundaryAspect <--通过继承PostSharp OnMethodBoundaryAspect属性,可以将外观应用于装饰方法的边界,就像使用Decorator一样。
{
public override void OnEntry(
MethodExecutionArgs args)
{
args.MethodExecutionTag =
new TransactionScope();
} <--通过重写OnEntry,OnSuccess和OnExit方法来实现方面
public override void OnSuccess(
MethodExecutionArgs args)
{
var scope = (TransactionScope)
args.MethodExecutionTag;
scope.Complete();
}
public override void OnExit(
MethodExecutionArgs args)
{
var scope = (TransactionScope)
args.MethodExecutionTag;
scope.Dispose();
} <--通过重写OnEntry,OnSuccess和OnExit方法来实现方面
}
因为您希望将事务包装在任意代码周围,所以您需要重写OnMethodBoundaryAspect
的三个方法-OnEntry
,OnSuccess
和OnExit
。 在OnEntry
期间,您将创建一个新的TransactionScope
,在OnExit
期间,您将处置该范围。 保证可以调用OnExit
。PostSharp
将把它的调用包装在finally
块中。 仅当包装操作成功时,您才想调用Complete
方法。这就是为什么要在OnSuccess
方法中实现这一点。您可以使用MethodExecutionTag
属性在方法之间转移创建的TransactionScope
。
注 有关
TransactionScope
的讨论,请参见10.3.3节。
单独查看清单11.4时,您可能会发现这些属性很吸引人,但是如果将清单11.5中的代码与装饰器(Decorator)中的同一方面(清单10.15)进行比较,则会有很多样板。 您需要重写多个方法,应用各种属性,并在方法之间传递状态。
注 将清单10.15的
TransactionCommandServiceDecorator<TCommand>
与清单11.5中的PostSharp
方面的实现进行比较时,我们赞赏装饰器(Decorator)编写的代码更加简洁。 装饰器的编写方式对开发人员而言更自然。 特别是在编写包含最终捕获或使用块的代码的过程中,SOLID 装饰器(Decorator)(甚至是动态拦截方法)在PostSharp
之类的工具提供的编译时编织方面具有出色的简单性和可维护性。
如果要增加可维护性,这可能是一个很小的代价,但是编译时编织还存在其他更多的限制性问题,这使其不适合作为将过度性依赖项(Volatile Dependency)作为横切关注点(Cross-Cutting Concerns)的方法。
编译时编织分析(Analysis of compile-time weaving)
在与DI的关系中,编译时编织具有两个特定的缺点。 在本节中,我们将讨论这些限制。 尽管编译时编织还有其他弊端,但表11.2中描述的两个方面却抓住了核心问题,这使其成为DI的不受欢迎的方法。
表11.2 从DI角度看编译时编织的缺点
缺点 | 概述 |
---|---|
DI不友好 | 没有很好的方法将挥发性依赖项注入到编译时的编织方面。 这些替代方法会导致时间耦合,强制依赖和相互依存测试。 |
编译时耦合 | 方面是在编译时编织的,因此如果不应用方面,就无法调用代码。 这使测试复杂化并降低了灵活性。 |
编译时编织方面不兼容DI
涉及交叉切割问题时,您会发现自己经常使用易失性依赖项。 正如您在第1章中了解到的那样,过度性依赖项(Volatile Dependency)是DI的重点。 对于挥发性依赖关系,您的默认选择应该是使用构造函数注入(Constructor Injection),因为它静态定义了所需依赖关系的列表。
不幸的是,无法在编译时编织方面使用构造函数注入(Constructor Injection)。 看一下下一个清单,在这里我们尝试将构造函数注入(Constructor Injection)与Circuit Breaker方面一起使用。
清单11.6 使用构造函数注入(Constructor Injection)将依赖项注入方面 (坏代码)
[PostSharp.Serialization.PSerializable] <--为简洁起见,省略了其他属性
public class CircuitBreakerAttribute
: OnMethodBoundaryAspect
{
private readonly ICircuitBreaker breaker;
public CircuitBreakerAttribute( <--尝试使用构造函数注入来获取方面的过度性依赖项(Volatile Dependency)。 这将无法正常工作,如下所示。
ICircuitBreaker breaker)
{
this.breaker = breaker;
}
public override void OnEntry(
MethodExecutionArgs args)
{
this.breaker.Guard(); <--在OnEntry,OnSuccess和OnException方法中使用Breaker 过度性依赖项(Volatile Dependency)
}
...
}
尝试将构造方法注入应用于此方面类的尝试失败了。 请记住,您定义的是代表单独代码的属性,该属性将被编织到您在编译时将要使用的方法中。 在.NET中,属性在其构造函数中只能具有原始类型,例如字符串和整数。
即使属性可能具有更复杂的依赖关系,也无法为该方面的实例提供ICircuitBreaker
实例,因为该方面的构建与构建ICircuitBreaker
实例的时间和位置完全不同。 属性实例(例如CircuitBreakerAttribute
)是由.NET运行时创建的,因此您无法影响它们的创建。 您无法将Dependency注入到属性的构造函数中,例如,组合根(Composition Root):
[CircuitBreakerAttribute(???)]
↑ What to inject here? And how?
但是,此问题不仅限于使用属性。 即使AOP
框架使用了除属性以外的其他机制,其后编译器也会在编译时将方面代码编织到您的常规代码中,并将其作为程序集代码的一部分。 另一方面,您的对象图是在运行时作为组合根(Composition Root)的一部分构建的。 这两种模式搭配不佳。 编译时编织无法进行构造函数注入(Constructor Injection)。
环境上下文和服务定位器是此问题的两个解决方法。但是,这两种变通办法都是骇人听闻的弊端。为了争辩,让我们看一下如何使用环境上下文来解决该问题。 下面的清单显示了断路器(Circuit Breaker)方面中公共静态 Breaker
属性的定义。
清单11.7 使用环境上下文(Ambient Conte xt)在方面内使用独立性
public class CircuitBreakerAttribute
: OnMethodBoundaryAspect
{
public static ICircuitBreaker Breaker { get; set; } <--一个公共静态属性,可让您在应用程序启动时在“合成根”内部设置ICircuitBreaker接口。 这是环境上下文反模式。
public override void OnEntry(
MethodExecutionArgs args)
{
Breaker.Guard();
}
...
}
正如您在5.3.2节中所了解的那样,环境上下文会引起时间耦合(Temporal Coupling)。 这意味着,如果您忘记设置Breaker
属性,则应用程序将失败,并显示NullReferenceException
,因为依赖关系不是可选的。
此外,由于唯一的选择是在应用程序启动期间设置一次属性,因此需要将其定义为静态。但是,这可能会导致自身的问题:这可能会导致ICircuitBreaker
成为强制依赖(Captive Dependency),如第8.4.1节中所述。
这种静态属性会导致相互依赖测试,因为在执行下一个测试用例时,其值会保留在内存中。 因此,在每次测试后都必须进行夹具拆卸。这是我们必须永远记住的事情-很容易忘记。 因此,使用环境上下文访问易失性依赖关系的编译时编织方面不容易测试。
注 您可以使用
ICircuitBreaker
的Factory
或Proxy
实施来缓解强制性依赖性,但是我们认为,到此为止,您已经开始了解静态属性导致的复杂性。
另一个解决方法是服务定位器(Service Locator),但与环境上下文相比,只会使情况变得更糟。 服务定位器(Service Locator)在相互依赖测试和时间耦合(Temporal Coupling)方面也表现出相同的问题。 最重要的是,它可以访问一组不受限制的过度性依赖项(Volatile Dependency),因此对其依赖项的含义不明显,并且将服务定位器拖到了冗余的依赖项上。 由于服务定位器(Service Locator)是较差的选择,因此我们为您提供示例,直接跳到编译时编织的第二个缺点—在编译时进行耦合。
编译时编织导致编译时耦合(Compile-time weaving causes coupling at compile time)
尽管编译时编织使源代码与各个方面脱钩,但仍会使编译后的代码与编织方面紧密结合。 这是一个问题,因为横切关注点(Cross-Cutting Concerns)通常取决于外部系统。 当编写单元测试(Unit testing)时,此问题变得很明显,因为单元测试(Unit testing)必须能够独立运行。 您想测试类的逻辑本身而不与它的易失性依赖项相互依存。 您不希望单元测试(Unit testing)跨越过程和网络边界,因为与数据库,文件系统或其他外部系统的通信会影响测试的可靠性和性能。 换句话说,编译时编织方面会影响可测试性。
但是,即使使用广泛定义的集成测试,编译时编织仍会引起问题。 在集成测试中,您将与其他部分集成来测试系统的一部分。 这降低了隔离级别,但是使您能够找出各个组件与其他组件集成时的工作方式。 例如,如果您正在测试SqlProductRepository
,则对它进行单元测试(Unit testing)是没有意义的,因为所有此Repository
所做的只是查询数据库。 因此,您要测试此组件与数据库的交互。
但是即使在那种情况下,您通常也不想在测试过程中应用所有方面。 例如,使用[CheckAuthorization]
方面可能会迫使这种测试经过某种登录过程,以验证组件是否可以成功存储和检索产品。 重要的是要查看这种授权方面是否按预期工作。 不幸的是,对于每个集成测试,都必须将其作为测试设置的一部分来运行,这会使这些测试难以维护,并且可能会慢很多。
如果还启用了缓存,则会出现一个更有趣的问题。 在这种情况下,您可以编写旨在查询数据库的自动测试,但决不要这样做,因为测试代码会命中高速缓存。 因此,如果那些方面与过渡性相关,则您希望完全控制将哪些方面应用于哪个测试以及何时进行测试。 编译时编织使这一过程变得非常复杂。
编译时编织不适用于挥发性依赖项
DI的目的是通过将缝隙(Seam)引入您的应用程序来管理过度性依赖项(Volatile Dependency)。 这使您可以在组合根(Composition Root)内部集中对象图的合成。
这与您在应用编译时编织时获得的结果完全相反:它导致过度性依赖项(Volatile Dependency)在编译时耦合到您的代码。这样就无法使用适当的DI技术,也无法在应用程序的(Composition Root)中安全地组成完整的对象图。 出于这个原因,我们说编译时编织与DI相反-在过度性依赖项上使用编译时编织是一种反模式(anti-pattern)。
重要 我们在明确地谈论将编译时编织与过度性依赖项(Volatile Dependency)结合使用。从DI的角度来看,稳定性依赖项(Stable Dependency)并不是那么有趣。虽然您可能会发现在使用编译时编织来应用稳定依赖关系时会发现价值,但是在使用编译时编织来应用过度性依赖项(Volatile Dependency)时却没有任何价值。
主张采用SOLID原则,如果不可能的话,请改用动态拦截。 关于这一点,我们现在在第3部分中将Pure DI留在后面,然后在第4部分中继续阅读有关DI容器(DI Container)的知识。在那里,您将学习DI容器(DI Container)如何解决您可能面临的一些挑战。
总结
- 动态拦截是一种面向方面的编程(
AOP
)技术,可自动生成要在运行时发出的装饰器。 方面被编写为拦截器,被插入到运行时生成的装饰器(Decorator)中。 - 动态拦截具有以下缺点:
- 失去编译时支持。
- 方面与工具紧密相关。
- 并非普遍适用
- 无法解决潜在的设计问题。
- 为了防止或像我们在第10章中建议的那样进行设计更改,动态拦截可能是一个不错的临时解决方案,直到它开始进行此类改进之前。
- 编译时编织是一种
AOP
技术,可更改编译过程。 它使用特殊的工具通过IL操纵来更改已编译的程序集。 将AOP
应用于易失性依赖关系不是一种理想的方法。 - 关于DI,编译时编织存在以下问题:
- 编译时编织方面是DI不友好的。
- 编译时编织会导致编译时紧密耦合。
- 主张采用SOLID原则,如果不可能的话,请改用动态拦截。