第九章-拦截
拦截(Interception)
关于烹饪的最有趣的事情之一是,您可以将许多成分组合在一起,而这些成分本身并不是特别美味,它们的总和要大于各个部分的总和。 通常,您会从简单的食材开始,为餐点打下基础,然后对其进行修饰和修饰,直到最终得到美味的菜肴。
考虑小牛肉片。 如果您绝望了,可以生吃,但是在大多数情况下,您更喜欢油炸。 但是,如果将它拍在热锅上,结果将不那么出色。除了烧焦的味道外,它的味道也不会太多。 幸运的是,您可以采取许多步骤来增强体验:
- 用黄油煎炸猪排可以防止肉燃烧,但味道可能会保持淡淡。
- 添加盐可以增强肉的味道。
- 加入其他香料,例如胡椒,会使味道更复杂。
- 用包含盐和香料的混合物做面包,不仅可以增加口味,而且可以使原始成分具有新的质感。 至此,您已经快要吃可乐饼了。
- 切开肉饼的口袋,然后在口袋中加入火腿,奶酪和大蒜,然后在面包上裹上面包,这样我们就可以越过上面了。 现在,您可以买到最优质的牛肉蓝带。
小牛肉片和小牛肉块的区别很大,但基本成分相同。 差异是由您添加到其中的事物引起的。给小牛肉肉饼,您可以修饰它,而无需更改主要成分来制造不同的菜肴。
通过松散耦合(Loose Coupling),您可以在开发软件时执行类似的功能。对接口进行编程时,可以通过将核心实现包装在该接口的其他实现中来进行转换或增强。 在清单8.19中,您已经看到了一些使用该技术的方法,其中我们使用了该技术,通过将其包装在虚拟代理中来修改昂贵的依赖的生命周期。
这种方法可以推广,使您能够拦截(Interception)用户到服务的呼叫。 这就是本章要介绍的内容。
像小牛肉肉片一样,我们从基本成分开始,然后添加更多成分以使第一种成分更好,但并没有改变其最初的核心。拦截(Interception)是从松耦合中获得的最强大的功能之一。它使您可以轻松地应用单一责任原则(Single Responsibility Principle)和关注点分离(Separation Of Concerns)。
在前面的章节中,我们将大量的能源操纵代码扩展到了真正松散耦合(Loose Coupling)的位置。 在本章中,我们将开始收获这项投资的收益。本章的总体结构是线性的。 我们将从拦截(Interception)的介绍开始,包括一个示例。 在此,我们将继续讨论横切关注点(Cross-Cutting Concerns)。 本章只讲理论,不讲例子,因此,如果您已经熟悉了本主题,则可以考虑直接进入第10章,该章讨论了面向方面的编程。
定义 横切关注点(Cross-Cutting Concerns)是程序中影响应用程序更大部分的方面。它们通常是非功能性需求。它们与任何特定功能没有直接关系,而是应用于现有功能。
完成本章后,您应该能够使用装饰者设计模式使用拦截(Interception)来开发松散耦合(Loose Coupling)的代码。您应该获得成功观察关注点分离和应用横切关注点(Cross-Cutting Concerns)的能力,同时保持代码处于良好状态。
本章从一个基本的、介绍性的例子开始,逐步建立起越来越复杂的概念和例子。最后,也是最先进的概念可以很快用抽象的方式解释。但是,因为它可能只有通过一个可靠的例子才有意义,本章以一个全面的、多页的演示来结束。然而,在我们达到这一点之前,我们必须从一开始就开始,那就是引入拦截(Interception)。
引入拦截(Interception)
拦截(Interception)的概念很简单:我们希望能够拦截(Interception)使用者和服务之间的调用,并在调用服务之前或之后执行一些代码。我们希望这样做,消费者和服务都不必改变。
定义 拦截(Interception)是拦截(Interception)两个协作组件之间的调用的能力,这样您就可以丰富或更改依赖项的行为,而无需更改两个协作者本身。
例如,假设您想向SqlProductRepository
类添加安全检查。尽管您可以通过更改SqlProductRepository
本身或通过更改使用者的代码来实现这一点,但通过拦截(Interception),您可以通过使用一些中间代码段拦截(Interception)对SqlProductRepository
的调用来应用安全检查。在图9.1中,消费者对服务的正常调用被中介截获,中介可以在将调用传递给实际服务之前或之后执行自己的代码。
图9.1 简而言之,拦截(Interception) |
---|
重要 围绕DI的一套软件设计原则和模式(例如但不限于松散耦合(Loose Coupling)和里氏转换原则(Liskov Substitution Principle))是拦截(Interception)的促成因素。没有这些原则和模式,就不可能应用拦截(Interception)。
在本节中,您将了解拦截(Interception),并了解它的核心是如何应用装饰者设计模式的。如果您对装饰者设计模式(Decorator design pattern)的了解有点生疏,请不要担心;作为讨论的一部分,我们将从对该模式的描述开始。当我们完成了,你应该有一个很好的了解如何装修工作。我们将首先看一个简单的示例来展示这个模式,然后讨论拦截(Interception)与装饰者设计模式(Decorator design pattern)的关系。
装饰者设计模式(Decorator design pattern)
与许多其他模式一样,装饰者设计模式(Decorator design pattern)是一种古老且描述良好的设计模式,它比DI早了十年。这是拦截(Interception)的一个基本组成部分,值得一提。
装饰器模式首先在Erich Gamma等人(Addison-Wesley,1994)的《设计模式:可重用面向对象软件的元素》一书中描述。该模式的目的是动态地将额外的职责附加到对象上。装饰者设计模式(Decorator design pattern)提供了一种灵活的替代子类化的方法来扩展功能。
如图9.2所示,装饰器(Decorator )通过将抽象的一个实现包装到同一抽象的另一个实现中来工作。此包装器将操作委托给包含的实现,同时在调用包装对象之前和/或之后添加行为。
图9.2 装饰者模式结构图 |
---|
动态附加职责的能力意味着您可以决定在运行时应用装饰器(Decorator),而不是在编译时将此关系烘焙到程序中,这是您使用子类化所要做的。
一个装饰器(Decorator)可以包装另一个装饰器(Decorator),后者包装另一个装饰器(Decorator),以此类推,提供一个拦截(Interception)的管道。图9.3显示了它的工作原理。在核心,必须有一个自包含的实现来执行所需的工作。
图9.3 就像一组俄罗斯嵌套玩偶一样,一个装饰器包装另一个装饰器,该装饰器包装一个自包含的组件。 |
---|
例如,假设您有一个名为IGreeter
的抽象类,它包含一个Greet
方法:
public interface IGreeter
{
string Greet(string name);
}
对于这个抽象类,您可以创建一个简单的实现来创建一个正式的问候语:
public class FormalGreeter : IGreeter
{
public string Greet(string name)
{
return "Hello, " + name + ".";
}
}
最简单的Decorator
实现是将调用委托给decorated
对象,而不做任何事情:
public class SimpleDecorator : IGreeter
{
private readonly IGreeter decoratee; <--装饰器包装与实现相同的抽象的组件。
public SimpleDecorator(IGreeter decoratee)
{
this.decoratee = decoratee;
}
public string Greet(string name)
{
return this.decoratee.Greet(name); <--对Greet的调用将传递,而无需修饰装饰的组件; 它的返回值也直接返回。
}
}
图9.4显示了IGreeter
、FormalGreeter
和SimpleDecorator
之间的关系。因为SimpleDecorator
除了转发调用之外什么都不做,所以它是非常无用的。相反,装饰程序可以选择在委派调用之前修改输入。
图9.4 SimpleDecorator 和FormalGreeter 都实现了IGreeter ,而SimpleDecorator 包装了一个IGreeter ,并将其Greet 方法中的所有调用转发给装饰对象的Greet 方法。 |
---|
注 在下面的代码示例中,我们主要关注
Greet
方法,因为装饰器(Decorator)的其余代码将保持不变。
让我们看看TitledGreeterDecorator
类的Greet
方法:
public string Greet(string name)
{
string titledName = "Mr. " + name;
return this.decoratee.Greet(titledName);
}
以类似的方式,在创建NiceToMeetYouGreeterDecorator
时,装饰器可能会决定在返回值之前修改返回值:
public string Greet(string name)
{
string greet = this.decoratee.Greet(name);
return greet + " Nice to meet you.";
}
在前面的两个示例中,您可以将后者环绕在前者周围,以组成一个修改输入和输出的组合:
IGreeter greeter =
new NiceToMeetYouGreeterDecorator(
new TitledGreeterDecorator(
new FormalGreeter()));
string greet = greeter.Greet("Samuel L. Jackson");
Console.WriteLine(greet);
这将产生以下输出:
装饰器还可以决定不调用底层实现:
public string Greet(string name)
{
if (name == null) <--防御性语句(Guard Clause)为空输入提供默认行为,在这种情况下,根本不会调用包装的组件。
{
return "Hello world!";
}
return this.decoratee.Greet(name);
}
不调用底层实现比委派调用更重要。尽管跳过装饰器本身没有什么错,但是装饰器现在取代了,而不是丰富了,原来的行为。更常见的场景是通过抛出异常来停止执行,我们将在第9.2.3节中讨论。
装饰器与任何包含依赖项的类的区别在于,包装的对象实现了与装饰器相同的抽象。这使编写器能够用装饰器替换原始组件,而不必更改使用者。包装的对象通常被注入到声明为抽象类型的装饰器中—它包装接口,而不是特定的具体实现。在这种情况下,装饰者必须坚持里氏转换原则(Liskov Substitution Principle),平等对待所有装饰对象。
就这样。没有比这更多的装饰图案。在这本书中,您已经在多个地方看到了装饰师的工作。例如,第1.2.2节中的SecureMessageWriter
示例是一个装饰器。现在让我们看一个具体的例子,说明如何使用装饰器来实现横切关注点。
示例:使用装饰器实现审计
在本例中,我们将再次实现对IUserRepository
的审计。您可能还记得,我们在第6.3节中讨论了审计,在这里我们将它作为一个示例来解释如何修复依赖性周期。通过审计,您可以在系统中记录用户所做的所有重要操作,以供以后分析。
审计是交叉关注的一个常见例子:它可能是必需的,但是阅读和编辑用户的核心功能不应该受到审计的影响。这正是我们在第6.3节中所做的。因为我们将IAuditTrailAppender
接口注入到SqlUserRepository
本身,所以我们强制它了解并实现审计。这是违反单一责任原则。单一责任原则建议我们不应该让SqlUserRepository
实现审计;鉴于此,使用装饰器是更好的选择。
为用户存储库实现审计修饰符
您可以通过引入一个新的AuditingUserRepositoryDecorator
类来使用装饰器实现审计,该类封装另一个IUserRepository
并实现审计。图9.5说明了类型之间的关系。
除了修饰的IUserRepository
之外,AuditingUserRepositoryDecorator
还需要一个实现审计的服务。为此,可以使用第6.3节中的IAuditTrailAppender
。下面的清单显示了这个实现。
图9.5 AuditingUserRepositoryDecorator 将审计添加到任何IUserRepository 实现中。 |
---|
清单9.1 声明
AuditingUserRepositoryDecorator
public class AuditingUserRepositoryDecorator
: IUserRepository <--实现并装饰IUserRepository
{
private readonly IAuditTrailAppender appender;
private readonly IUserRepository decoratee;
public AuditingProductRepository(
IAuditTrailAppender appender,
IUserRepository decoratee)
{
this.appender = appender;
this.decoratee = decoratee;
}
...
}
AuditingUserRepositoryDecorator
实现了它所修饰的相同抽象。它使用标准的构造函数注入(Constructor Injection)来请求一个IUserRepository
,它可以包装这个IUserRepository
并将其核心实现委托给它。除了修饰的存储库之外,它还请求一个IAuditTrailAppender
,它可以用来审核修饰的存储库实现的操作。下面的清单显示了AuditingUserRepositoryDecorator
上两个方法的示例实现。
清单9.2 实现
AuditingUserRepositoryDecorator
public User GetById(Guid id)
{
return this.decoratee.GetById(id); <--省略对读操作的审核
}
public void Update(User user) <---编写带有审计功能的操作
{
this.decoratee.Update(user);
this.appender.Append(user);
}
并不是所有的操作都需要审计。一个常见的要求是审核所有的创建、更新和删除操作,而忽略读取操作。因为GetById
方法是纯读取操作,所以将调用委托给修饰的存储库并立即返回结果。另一方面,必须审核Update
方法。您仍然将实现委托给修饰的存储库,但是在委托的方法成功返回之后,您可以使用注入的IAuditTrailAppender
来审核操作。
一个装饰者,像海选家储藏室的装饰者,类似于小牛肉片周围的面包:它修饰基本成分而不修改它。面包本身不是空壳,而是有自己的配料表。真正的面包是由面包屑和香料制成的;类似地,AuditingUserRepositoryDecorator
包含一个IAuditTrailAppender
。
请注意,注入的IAuditTrailAppender
本身就是一个抽象,这意味着您可以独立于AuditingUserRepositoryDecorator
改变实现。AuditingUserRepositoryDecorator
类所做的就是协调修饰的IUserRepository
和IAuditTrailAppender
的操作。您可以编写任何您喜欢的IAuditTrailAppender
实现,但是在清单6.24中,我们选择了基于实体框架构建一个。让我们看看如何将所有相关的依赖项连接起来以使其工作。
Composing AuditingUserRepositoryDecorator
在第8章中,您看到了几个如何编写HomeController
实例的示例。清单8.11提供了一个简单的实现,该实现涉及具有瞬态生活方式的实例。下面的列表显示了如何使用修饰过的SqlUserRepository
组合这个homeconnector
。
清单9.3 组成装饰器
private HomeController CreateHomeController()
{
var context = new CommerceContext();
IAuditTrailAppender appender =
new SqlAuditTrailAppender(
this.userContext,
context);
IUserRepository userRepository =
new AuditingUserRepositoryDecorator(
appender,
new SqlUserRepository(context)); <--创建一个新的SqlUserRepository实例。 将SqlUserRepository和基于SQL Server的IAuditTrailAppender实现注入到Decorator实例中。 SqlUserRepository和AuditingUserRepositoryDecorator都是IUserRepository实例。
IProductService productService =
new ProductService(
new SqlProductRepository(context),
this.userContext,
userRepository); <--与其直接将SqlUserRepository注入到ProductService实例中,而是注入包装SqlUserRepository的Decorator。 ProductService仅看到IUserRepository接口,而对SqlUserRepository或Decorator一无所知。
return new HomeController(productService);
}
警告 清单9.3是一个忽略生存期问题的简化示例。因为
CommerceContext
是一次性类型,所以代码可能会导致资源泄漏。一个更正确的实现是使用第8.3.3节中讨论的模型插入清单9.3,但我们确信您会意识到,此时它开始变得相当复杂。
请注意,您可以在不更改现有类的源代码的情况下向IUserRepository
添加行为。您不必更改SqlUserRepository
来添加审计。回想第4.4.2节,这是一个理想的特性,称为开放/封闭原则(Open/Closed Principle)。
现在您已经看到了一个使用装饰 AuditingUserRepositoryDecorator
拦截(Interception)具体SqlUserRepository
的示例,让我们将注意力转向在不一致或不断变化的需求面前编写干净且可维护的代码,以及解决横切关注点(Cross-Cutting Concerns)。
实施横切关注点(Cross-Cutting Concerns)
大多数应用程序必须处理与任何特定特性没有直接关系的方面,而是处理更广泛的问题。这些关注点往往涉及许多其他不相关的代码领域,甚至是在不同的模块或层中。因为它们跨越了代码库的很大一部分,所以我们称之为横切关注点(Cross-Cutting Concerns)。表9.1列出了一些例子。此表不是一个全面的列表,而是一个示例。
表9.1横切关注点(Cross-Cutting Concerns)的常见示例
方面 | 描述 |
---|---|
Auditing | 任何更改数据的操作都应留下审计跟踪,包括时间戳,执行更改的用户的身份以及有关更改内容的信息。 您在第9.1.2节中看到了一个示例。 |
Logging | 与审核稍有不同,日志记录倾向于将重点放在记录反映应用程序状态的事件上。 这可能是IT运营人员感兴趣的事件,但也可能是业务事件。 |
Performance monitoring(性能监控) | 与日志记录稍有不同,因为与特定事件相比,它在记录性能上的处理更多。 如果您有无法通过标准基础架构进行监控的服务水平协议(SLA),则必须实施自己的性能监控。 自定义Windows性能计数器是一个不错的选择,但是您仍然必须添加一些捕获数据的代码。 |
Validation | 通常需要使用有效数据来调用操作。 这可以是简单的用户输入验证,也可以是更复杂的业务规则验证。 尽管验证本身始终取决于其上下文,但是该验证的调用和验证结果的处理通常不是,并且可以认为是跨领域的。 |
Security | 通常应基于角色或组的成员身份,仅允许某些用户执行某些操作,并且您必须强制执行此操作。 |
Caching | 您通常可以通过实现缓存来提高性能,但是没有理由为什么特定的数据访问组件应该处理这一方面。 您可能希望能够为不同的数据访问实现启用或禁用缓存。 |
Error handling | 应用程序可能需要处理某些异常并记录它们,对其进行转换或向用户显示消息。 您可以使用错误处理装饰器以适当的方式处理错误。 |
Fault tolerance | 保证进程外资源不时不可用。 关系数据库需要处理事务性操作,以防止数据损坏,这可能导致死锁。 使用装饰器,您可以实现容错模式,例如断路器,以解决此问题。 |
在绘制分层应用程序体系结构的图时,横切关注点(Cross-Cutting Concerns)通常表示为放置在层旁边的垂直块。如图9.6所示。
在本节中,我们将看一些示例,说明如何使用装饰器形式的拦截(Interception)来解决横切关注点。从表9.1中,我们将选择容错、错误处理和安全方面来了解实现方面。与其他许多概念一样,拦截(Interception)在抽象上很容易理解,但在细节上却很难理解。它需要曝光来正确吸收技术,这就是为什么本节展示了三个例子。当我们完成这些,你应该有一个更清晰的画面,什么是拦截(Interception),以及如何应用它。因为您已经在第9.1.2节中看到了一个介绍性示例,所以我们将看一个更复杂的示例来说明如何将拦截(Interception)用于任意复杂的逻辑。
图9.6 在应用程序架构图中横切关注点(Cross-Cutting Concerns)通常由跨越所有层的垂直块表示。在这种情况下,安全是一个贯穿各领域的问题。 |
---|
拦截(Interception)断路器
与进程外资源通信的任何应用程序有时都会发现该资源不可用。 网络连接断开,数据库脱机,Web服务被分布式拒绝服务(DDOS)攻击淹没。 在这种情况下,调用应用程序必须能够恢复并适当处理问题。
大多数.NET API
都有默认超时,以确保进程外调用不会永远阻止使用线程。 但是,在收到超时异常的情况下,如何处理对故障资源的下一次调用? 您是否尝试再次调用该资源? 因为超时通常表明另一端处于脱机状态或被请求淹没,所以进行新的阻止呼叫可能不是一个好主意。 最好假设最坏的情况并立即抛出异常。 这是断路器模式背后的原理。
断路器是一种稳定模式,通过快速失败而不是在挂起时挂起并消耗资源来增加应用程序的健壮性。这是非功能性需求和真正的跨领域关注的一个很好的例子,因为它与进程外调用中实现的功能几乎没有关系。
断路器模式本身有点复杂,实现起来很复杂,但是您只需要进行一次投资即可。如果愿意,您甚至可以在可重用的库中实现它,在这里您可以通过使用装饰器模式轻松地将其应用于多个组件。
断路器模式(The Circuit Breaker pattern)
断路器设计模式(The Circuit Breaker pattern)的名称来自同名的电子开关。该开关设计用于在发生故障时切断连接,以防止故障蔓延。
在软件应用程序中,一旦发生超时或类似的通信错误,如果您不断锤击崩溃的系统,可能会使情况变得更糟。 如果远程系统陷入困境,则多次重试可以使它越过边缘—暂停可能使它有恢复的机会。 在调用层上,阻塞等待超时的线程可能会使使用中的应用程序无响应,从而迫使用户等待错误消息。 最好在一段时间内检测到通信中断并快速失败。
断路器设计通过在发生错误时使开关跳闸来解决此问题。 它通常包括一个超时,使它稍后可以重试连接。 这样,它可以在远程系统恢复时自动恢复。 图9.7示出了断路器中状态转换的简化视图。
图9.7 断路器模式的简化状态转换图 |
---|
您可能希望使断路器比图9.7中描述的更为复杂。 首先,您可能不希望每次发生偶发性错误时都使断路器跳闸,而是使用阈值。 其次,仅应在出现某些类型的错误时使断路器跳闸。 超时和通信异常很好,但是NullReferenceException
可能表示错误而不是间歇性错误。
让我们看一个示例,该示例演示如何使用装饰器模式将Circuit Breaker行为添加到现有的进程外组件中。 在此示例中,我们将重点放在应用可重复使用的断路器上,而不是在实现上。
示例:为IProductRepository
创建断路器(Circuit Breaker)
在7.2节中,我们创建了一个UWP
应用程序,该应用程序使用IProductRepository
接口与后端数据源(例如WCF
或Web API
服务)进行通信。 在清单8.6中,我们使用了WcfProductRepository
,它通过调用WCF
服务操作来实现IProductRepository
。 因为此实现没有显式的错误处理,所以任何通信错误都会冒泡到调用方。
这是使用断路器的绝佳方案。 一旦异常开始发生,您想快速失败; 这样,您将不会阻止调用线程并淹没服务。 如下清单所示,您首先为IProductRepository
声明一个装饰器,然后通过构造函数注入(Constructor Injection)请求必需的依赖项。
清单9.4 用断路器(Circuit Breaker)装饰类
public class CircuitBreakerProductRepositoryDecorator
: IProductRepository
{
private readonly ICircuitBreaker breaker;
private readonly IProductRepository decoratee; <--IProductRepository的装饰器,意味着它既实现又包装了IProductRepository的实现
public CircuitBreakerProductRepositoryDecorator(
ICircuitBreaker breaker, <--另一个依赖项是一个ICircuitBreaker,您可以使用它实现Circuit Breaker模式。
IProductRepository decoratee)
{
this.breaker = breaker;
this.decoratee = decoratee;
}
...
}
现在,您可以包装对装饰后的IProductRepository
的任何调用。
清单9.5 将断路器(Circuit Breaker)应用于Insert方法
public void Insert(Product product)
{
this.breaker.Guard(); <--检查断路器的状态
try
{
this.decoratee.Insert(product); <--调用装饰好的存储库,并在调用成功后调用成功
this.breaker.Succeed();
}
catch (Exception ex) <--当对Insert的调用失败时,使断路器跳闸
{
this.breaker.Trip(ex);
throw;
}
}
调用经过修饰的存储库之前,您需要做的第一件事是检查断路器的状态。 当状态为“关闭”或“半开”时,Guard方法可让您通过,而在状态为“打开”时,它将引发异常。 这样可以确保您有理由认为通话不会成功时快速失败。 如果使它超出了Guard方法,则可以尝试调用经过修饰的存储库。 如果呼叫失败,则使断路器跳闸。 在此示例中,我们将事情保持简单,但是在适当的实现中,您应该仅从选定的异常类型中捕获并触发断路器。
从闭合状态和半断开状态起,断路器跳闸将使您回到断开状态。 从“打开”状态,超时确定何时返回到“半打开”状态。
相反,如果呼叫成功,则向断路器发出信号。 如果您已经处于“关闭”状态,则请保持“关闭”状态。 如果您处于半开状态,则可以转换回“已关闭”状态。 当断路器处于“打开”状态时,这不可能表示成功,因为保护方法可确保您永远不会走得那么远。
IProductRepository
的所有其他方法看起来相似,唯一的区别是它们在被修饰对象上调用的方法以及用于返回值的方法的额外代码行。 您可以在try
块的GetAll
方法中看到此变化:
var products = this.decoratee.GetAll();
this.breaker.Succeed();
return products;
由于必须向断路器表明成功,因此必须在返回修饰的存储库之前保留其返回值。 这是返回值的方法与不返回值的方法之间的唯一区别。
至此,您尚未将ICircuitBreaker
的实现打开,但是真正的实现是使用状态设计模式的类的完全可重用的复合体。7尽管我们在这里不打算更深入地研究CircuitBreaker
的实现, 重要信息是您可以截取任意复杂的代码。
注 如果您对CircuitBreaker
类的实现感到好奇,可以在本书随附的代码中找到该类。
使用断路器(Circuit Breaker)实现编写应用程序
要组成添加了断路器功能的IProductRepository
,可以将装饰器包裹在实际实现的周围:
var channelFactory = new ChannelFactory<IProductManagementService>("*");
var timeout = TimeSpan.FromMinutes(1);
ICircuitBreaker breaker = new CircuitBreaker(timeout);
IProductRepository repository =
new CircuitBreakerProductRepositoryDecorator( <--装饰WcfProductRepository
breaker,
new WcfProductRepository(channelFactory));
在清单7.6中,我们由几个依赖项组成了UWP
应用程序,包括清单8.6中的WcfProductRepository
实例。 您可以通过将WcfProductRepository
注入到CircuitBreakerProductRepositoryDecorator
实例中来装饰它,因为它实现了相同的接口。 在此示例中,每次您解决依赖关系时,您都将创建CircuitBreaker
类的新实例。 这对应于Transient生命周期模式。
在UWP
应用程序中,您只需解决一次依赖关系,使用瞬时断路器不是问题,但通常,对于这种功能而言,这不是最佳的生命周期模式。 另一端只有一个网络服务。 如果该服务不可用,则断路器应断开所有尝试连接到该断路器的尝试。 如果使用了CircuitBreakerProductRepositoryDecorator
的多个实例,则所有这些实例都应该发生。
更紧凑的
ICircuitBreaker
如此处所示,
ICircuitBreaker
接口包含三个成员:Guard,Succeed和Trip。 替代接口定义可以接受委托以将占用空间减少到单个方法:public interface ICircuitBreaker { T Execute<T>(Func<T> action); }
这样,您可以在每种方法中更简洁地使用
ICircuitBreaker
,如下所示:public IEnumerable<Product> GetAll() { this.breaker.Execute(() => this.decoratee.GetAll()); }
我们选择使用更明确,更老式的
ICircuitBreaker
版本,因为我们希望您能够专注于当前的拦截主题。 尽管我们个人喜欢延续过去,但在这种情况下,我们认为它可能比帮助更分散注意力。 我们是否最终选择一个接口定义而不是另一个,都不会改变本章的结论。
有一个明显的案例是设置具有Singleton生命周期的CircuitBreaker
,但这也意味着它必须是线程安全的。 由于其性质,CircuitBreaker
保持状态; 线程安全性必须明确实现。 这使实现更加复杂。
尽管它很复杂,但是您可以使用断路器轻松拦截(Interception)IProductRepository
实例。 尽管第9.1.2节中的第一个拦截(Interception)示例相当简单,但是断路器(Circuit Breaker)示例演示了您可以通过横切关注点(Cross-Cutting Concerns来拦截(Interception)类。 与原始实施方式相比,横切关注点(Cross-Cutting Concerns问题很容易变得更复杂。
断路器模式可确保应用程序快速失败,而不是占用宝贵的资源。 理想情况下,该应用程序完全不会崩溃。 要解决此问题,您可以使用Interception实现某些类型的错误处理。
使用装饰者模式模式报告异常
依赖关系可能会不时引发异常。 如果遇到无法处理的情况,即使是编写最佳的代码也会(并且应该)引发异常。 消耗进程外资源的客户端属于该类别。 示例UWP
应用程序中的WcfProductRepository
之类的类就是一个示例。 当Web
服务不可用时,存储库将开始引发异常。 断路器不会改变这一基本特征。 尽管它拦截(Interception)WCF
客户端,但仍会引发异常-这样做速度更快。
您可以使用拦截(Interception)添加错误处理。 您不希望因错误处理而给依赖项带来负担。 由于依赖关系应被视为可在许多不同场景中使用的可重用组件,因此无法向依赖关系中添加适合所有场景的异常处理策略。 如果您这样做,也将违反单一责任原则(Single Responsibility Principle)。
通过使用拦截(Interception)来处理异常,您可以遵循开放/封闭原则(Open/Closed Principle)。 它使您可以针对任何给定情况实施最佳的错误处理策略。 让我们看一个例子。
在前面的示例中,我们将WcfProductRepository
封装在断路器中,以与产品管理客户端应用程序一起使用,该产品最初在7.2.2节中介绍。 断路器仅通过确保客户端快速失败来处理错误,但仍会引发异常。 如果不加以处理,它们将导致应用程序崩溃,因此您应该实现一个装饰器,该装饰器知道如何处理其中的一些错误。
您可能会希望看到一个消息框,而不是使应用程序崩溃,该消息框告诉用户操作未成功,因此他们应稍后再试。 在此示例中,当引发异常时,它将弹出一条消息,如图9.8所示。
图9.8 产品管理应用程序通过向用户显示一条消息来处理通信异常。 请注意,在这种情况下,错误消息源自断路器,而不是潜在的通信故障。 |
---|
实施此行为很容易。 与在9.2.1节中所做的相同,您添加了一个新的ErrorHandlingProductRepositoryDecorator
类来装饰IProductRepository
接口。 清单9.6显示了该接口方法之一的示例,但是它们都是相似的。
清单9.6 使用
ErrorHandlingProductRepositoryDecorator
处理异常
public void Insert(Product product)
{
try
{
this.decoratee.Insert(product); <--代表被装饰者
}
catch (CommunicationException ex)
{
this.AlertUser(ex.Message); <--修改用户
}
catch (InvalidOperationException ex)
{
this.AlertUser(ex.Message); <--修改用户
}
}
Insert
方法代表ErrorHandlingProductRepositoryDecorator
类的整个实现。 如果引发异常,则尝试调用被修饰对象并用错误消息警告用户。 请注意,您只能处理一组特定的已知异常,因为抑制所有异常可能很危险。 提醒用户涉及格式化字符串,并使用MessageBox.Show
方法将其显示给用户。 这是在AlertUser
方法内部完成的。
再次,您通过实现装饰者模式将功能添加到了原始实现(WcfProductRepository
)。 通过不断添加新类型而不是修改现有代码,您既要遵循单一责任原则(Single Responsibility Principle)又要遵循开放/封闭原则(Open/Closed Principle)。 到现在为止,您应该已经看到一种模式,它提出了一种比装饰器更笼统的安排。 让我们简要地看一下实现安全性的最后一个例子。
防止使用装饰器未经授权访问敏感功能(Preventing unauthorized access to sensitive functionality using a Decorator)
安全性是另一种常见的横切关注点(Cross-Cutting Concerns)。 我们希望尽可能保护我们的应用程序安全,以防止未经授权访问敏感数据和功能。
注 安全是一个涵盖许多领域的重要主题,包括敏感信息的泄露和网络的侵入。在本节中,我们将简要讨论授权主题:确保只有经过授权的人员(或系统)才能执行特定的操作。 动作。
与我们使用Circuit Breaker的方式类似,我们希望拦截(Interception)方法调用并检查是否应允许该调用。 如果不是,则应该抛出异常,而不是允许进行调用。 原理是一样的。 区别在于我们用来确定呼叫有效性的标准。
实施授权逻辑的一种常见方法是通过针对当前操作的硬编码值来检查用户的角色,从而采用基于角色的安全性。 如果我们坚持使用IProductRepository
,则可以从SecureProductRepositoryDecorator
开始。 因为,正如您在上一节中所看到的那样,所有方法看起来都相似,所以下面的清单仅显示了两种方法的实现。
清单9.7 使用装饰器明确检查授权
public class SecureProductRepositoryDecorator
: IProductRepository
{
private readonly IUserContext userContext;
private readonly IProductRepository decoratee;
public SecureProductRepositoryDecorator(
IUserContext userContext, <--装饰器取决于IUserContext,这使它可以检查当前用户的角色。
IProductRepository decoratee)
{
this.userContext = userContext;
this.decoratee = decoratee;
}
public void Delete(Guid id)
{
this.CheckAuthorization(); <--Delete方法以防御性语句(Guard Clause)开头,该子句明确检查当前用户是否被允许执行此操作。 如果没有,它将立即引发异常。 仅当当前用户具有必需的角色时,您才允许其越过防御性语句(Guard Clause)来调用经过修饰的存储库。
this.decoratee.Delete(id);
}
public IEnumerable<Product> GetAll() <--并非所有方法都需要权限检查。 在此应用程序中,仅执行创建,更新和删除(CUD)操作。 允许该系统中的每个用户查询所有产品。
{
return this.decoratee.GetAll();
}
...
private void CheckAuthorization() <--CUD操作所需的角色被硬编码为管理员角色。 如果用户不在该角色中,则会引发异常。
{
if (!this.userContext.IsInRole(
Role.Administrator))
{
throw new SecurityException(
"Access denied.");
}
}
}
注 清单9.2、9.5、9.6和9.7中的装饰器示例仅显示了装饰器的代码的一部分,因为装饰器的所有方法看起来都相似。
在我们当前的设计中,对于给定的横切关注点(Cross-Cutting Concerns),基于装饰器的实现往往是重复的。 实现断路器涉及将相同的代码模板应用于IProductRepository
接口的所有方法。 如果您想将断路器添加到另一个抽象中,则必须将相同的代码应用于更多方法。
使用安全装饰器时,情况变得更糟,因为我们需要扩展某些方法,而其他方法仅仅是传递操作。 但是总体问题是相同的。
如果您需要将此横切关注点(Cross-Cutting Concerns)应用于其他抽象,那么也会导致代码重复,随着系统的变大,这可能会导致重大的可维护性问题。 您可能会想到,有多种方法可以防止代码重复,这使我们进入了面向方面的编程这一重要主题,我们将在下一章中进行讨论。
总结
- 拦截(Interception)是一种拦截(Interception)两个协作组件之间的调用的能力,您可以无需更改两个协作者本身就可以丰富或更改依赖项的行为。
- 松散耦合(Loose Coupling)是拦截(Interception)的促成因素。 对接口进行编程时,可以通过将核心实现包装在该接口的其他实现中来进行转换或增强。
- 拦截(Interception)是其核心,它是Decorator设计模式的一种应用。
- 装饰者设计模式(Decorator design pattern)通过动态地将附加职责附加到对象上,为子类化提供了灵活的替代方法。 它通过将抽象的一个实现包装在同一抽象的另一个实现中来工作。 这允许装饰器像俄罗斯嵌套娃娃一样嵌套。
- 横切关注点是代码的非功能性方面,通常会跨越代码库的大部分区域。 横切关注点的常见示例是审核,日志记录,验证,安全性和缓存。
- 断路器是一种稳定设计模式,通过在发生故障时切断连接来增加系统的坚固性,以防止故障传播。