C# 解决横切关注点详解

在编写干净的代码核心关注点和横切关注点时,需要考虑两种类型的关注点。核心问题是该软件的原因和开发原因。横切关注点是不属于业务需求的关注点,构成核心关注点,但必须在代码的所有领域解决,如下图所示:

在本章中,我们将通过构建一个可重用类库(您可以根据自己的喜好对其进行修改或扩展)来讨论交叉关注点。交叉关注点包括配置管理、日志记录、审核、安全性、验证、异常处理、检测、事务、资源池、缓存以及线程和并发。我们将使用 decorator 模式和 PostSharp 方面框架来帮助我们构建可重用库,该库在编译时注入。

在阅读本章时,您将看到属性编程如何减少样板代码的使用,以及更小、更可读、更易于维护和扩展的代码。这将只在方法中保留所需的业务代码和样板代码

We have discussed many of these ideas already. However, they are mentioned here again as they are cross-cutting concerns.

在本章中,我们将介绍以下主题:

本章结束时,您将具备以下技能:

  • 实现 decorator 模式。
  • 实现代理模式。
  • 使用 PostSharp 应用 AOP。
  • 构建您自己的可重用 AOP 库,解决您的跨领域问题。

要充分利用本章,您需要安装 Visual Studio 2019 和 PostSharp。本章代码文件参见https://github.com/PacktPublishing/Clean-Code-in-C-/tree/master/CH11 。让我们先看看装饰图案。

装饰设计模式是一种结构模式,用于向现有对象添加新功能而不更改其结构。原始类用 decorator 类包装,新的行为和操作在运行时添加到对象:

Component接口及其包含的成员由ConcreteComponent类和Decorator类实现。ConcreteComponent实现Component接口。Decorator类是实现Component接口的抽象类,包含对Component实例的引用。Decorator类是组件的基类。ConcreteDecorator类继承自Decorator类,并为组件提供装饰器。

我们将编写一个示例,将一个操作包装在一个try/catch块中。trycatch都会向控制台输出一个字符串。创建名为CH10_AddressingCrossCuttingConcerns的新.NET 4.8 控制台应用。然后,添加一个名为DecoratorPattern的文件夹。添加一个名为IComponent的新接口:

public interface IComponent {
   void Operation();
}

为了简单起见,我们的界面只有一个void类型的操作。现在我们已经有了接口,我们需要添加一个实现接口的抽象类。添加一个名为Decorator的新抽象类,该类实现IComponent接口。添加成员变量以存储我们的IComponent对象:

private IComponent _component;

_component成员变量存储IComponent对象,通过构造函数设置,如下所示:

public Decorator(IComponent component) {
    _component = component;
}

在前面的代码中,构造函数设置我们要装饰的组件。接下来,我们添加接口方法:

public virtual void Operation() {
    _component.Operation();
}

我们已经将Operation()方法声明为virtual,以便可以在派生类中重写它。现在我们将创建我们的ConcreteComponent类,它实现了IComponent

public class ConcreteComponent : IComponent {
    public void Operation() {
        throw new NotImplementedException();
    }
}

如您所见,我们的类由一个操作组成,它抛出NotImplementedException。现在,我们可以写关于ConcreteDecorator类的内容:

public class ConcreteDecorator : Decorator {
    public ConcreteDecorator(IComponent component) : base(component) { }
}

ConcreteDecorator类继承Decorator类。构造函数获取一个IComponent参数并将其传递给基构造函数,然后在基构造函数中设置成员变量。接下来,我们将覆盖Operation()方法:

public override void Operation() {
    try {
        Console.WriteLine("Operation: try block.");
        base.Operation();
    } catch(Exception ex)  {
        Console.WriteLine("Operation: catch block.");
        Console.WriteLine(ex.Message);
    }
}

在我们重写的方法中,我们有一个try/catch块。在try块中,我们向控制台写入消息并执行基类“Operation()方法。在catch块中,当遇到异常时,会写入一条消息,然后是错误消息。在使用代码之前,我们需要更新Program类。将DecoratorPatternExample()方法添加到Program类中:

private static void DecoratorPatternExample() {
    var concreteComponent = new ConcreteComponent();
    var concreteDecorator = new ConcreteDecorator(concreteComponent);
    concreteDecorator.Operation();
}

在我们的DecoratorPatternExample()方法中,我们创建了一个新的混凝土构件。然后我们把它交给一个新的混凝土装饰师的建造师。然后,我们在混凝土装饰器上调用Operation()方法。将以下两行添加到Main()方法中:

DecoratorPatternExample();
Console.ReadKey();

这两行执行我们的示例,然后等待用户按键后退出。运行代码,您将看到与以下屏幕截图中相同的输出:

这就结束了我们对装饰图案的研究。现在,我们来看看代理模式。

代理模式是一种结构设计模式,提供的对象可以替代客户端使用的实际服务对象。代理接收客户端请求,执行所需的工作,然后将请求传递给服务对象。代理对象可以与服务互换,因为它们共享相同的接口:

例如,当您有一个不想更改的类,但确实需要添加其他行为时,您会希望使用代理模式。代理将工作委托给其他对象。除非代理是服务的派生,否则代理方法最终应该引用Service对象。

我们将研究代理模式的一个非常简单的实现。在名为ProxyPatternChapter 11项目根目录中添加一个文件夹。添加一个名为IService的接口,使用单个方法处理请求:

public interface IService {
    void Request();
}

Request()方法执行执行请求的工作。代理和服务都将实现此接口以使用Request()方法。现在,添加Service类并实现IService接口:

public class Service : IService {
    public void Request() {
        Console.WriteLine("Service: Request();");
    }
}

我们的Service类实现IService接口并处理实际的服务Request()方法。此Request()方法将由Proxy类调用。实现代理模式的最后一步是编写Proxy类:

public class Proxy : IService {
    private IService _service;

    public Proxy(IService service) {
        _service = service;
    }

    public void Request() {
        Console.WriteLine("Proxy: Request();");
        _service.Request();
    }
}

我们的Proxy类实现IService并具有一个接受单个IService参数的构造函数。Proxy类的Request()方法由客户端调用。Proxy.Request()方法将执行它需要执行的操作,并负责调用_service.Request()。为了让我们能够看到这一点,让我们更新我们的Program类。将ProxyPatternExample()调用添加到Main()方法。然后,添加ProxyPatternExample()方法:

private static void ProxyPatternExample() {
    Console.WriteLine("### Calling the Service directly. ###");
    var service = new Service();
    service.Request();
    Console.WriteLine("## Calling the Service via a Proxy. ###");
    new Proxy(service).Request();
}

我们的测试方法运行Service类方向的Request()方法。然后,它通过Proxy类的Request()方法运行相同的方法。运行该项目,您将看到以下内容:

既然您已经对装饰器和代理模式有了一个工作的理解,让我们用 PASTHARP 看看 AOP。

AOP 可以与 OOP 一起使用。方面是应用于类、方法、参数和属性的属性,这些属性在编译时将代码编织到应用它的类、方法、参数或属性中。这种方法允许将程序的横切关注点从业务源代码转移到类库。关注点作为属性添加到需要的地方。编译器然后在运行时将所需的代码编入。这使您的业务代码保持小型化和可读性。在本章中,我们将使用 PostSharp。您可以从下载 https://www.postsharp.net/download

那么,AOP 如何与 PostSharp 协同工作?

将 PostSharp 包添加到项目中。然后,使用属性对代码进行注释。C# 编译器将您的代码构建成二进制,然后 PostSharp 分析二进制文件并注入方面的实现。尽管二进制文件在编译时被注入代码修改,但项目的源代码保持不变。这意味着您可以保持代码整洁、简洁,从而使维护、重用和扩展现有代码库变得更容易。

PostSharp 有一些非常好的现成图案供您使用。这些内容包括模型视图视图模型MVVM)、缓存、多线程、渴望和架构验证等。但好消息是,如果没有任何东西能够满足您的需求,那么您可以通过扩展方面框架和/或体系结构框架来自动化您自己的模式。

使用方面框架,您可以开发简单或复合方面,将其应用于代码,并验证其使用情况。对于体系结构框架,您可以开发自定义的体系结构约束。在深入研究横切关注点之前,让我们先简单地看一下扩展方面和体系结构框架。

You need to add the PostSharp.Redist NuGet package when writing aspects and attributes. Once done, if you find that your attributes and aspects are not working, then right-click on the project and select Add PostSharp to Project. After you've done this, your aspects should work.

扩展方面框架

在本节中,我们将开发一个简单的方面,并将其应用于一些代码。然后,我们将验证 aspect 的用法。

发展我们的方面

我们的相位将是由单个转换组成的简单相位。我们将从一个基本方面类派生方面。然后,我们将覆盖一些称为通知的方法。如果您想知道如何创建复合方面,可以在阅读如何创建复合方面 https://doc.postsharp.net/complex-aspects

方法执行前后的注入行为

OnMethodBoundaryAspect方面实现了 decorator 模式。您已经在本章前面看到了如何实现 decorator 模式。在这方面,您可以在执行目标方法之前和之后执行逻辑。下表列出了OnMethodBoundaryAspect类中可用的建议方法:

| 建议 | 说明 | | OnEntry(MethodExecutionArgs) | 在方法执行开始时,在任何用户代码之前使用。 | | OnSuccess(MethodExecutionArgs) | 在任何用户代码之后,当方法执行成功(即无异常返回)时使用。 | | OnException(MethodExecutionArgs) | 在任何用户代码之后,当方法执行失败并出现异常时使用。相当于一个catch块。 | | OnExit(MethodExecutionArgs) | 在方法执行退出时使用,无论是成功退出还是异常退出。此建议在任何用户代码之后以及在当前方面的OnSuccess(MethodExecutionArgs)OnException(MethodExecutionArgs)方法之后运行。它相当于一个finally块。 |

对于我们的简单方面,我们将查看所有正在使用的方法。在开始之前,请将 PostSharp 添加到您的项目中。如果您已经下载了 PostSharp,则可以右键单击项目,然后选择“将 PostSharp 添加到项目”。之后,向项目中添加一个名为Aspects的新文件夹,然后添加一个名为LoggingAspect的新类:

[PSerializable]
public class LoggingAspect : OnMethodBoundaryAspect { }

[PSerializeable]属性是一个自定义属性,当应用于类型时,会导致 PostSharp 生成一个序列化程序供PortableFormatter使用。现在,覆盖OnEntry()方法:

public override void OnEntry(MethodExecutionArgs args) {
    Console.WriteLine("The {0} method has been entered.", args.Method.Name);
}

OnEntry()方法在任何用户代码之前执行。现在,覆盖OnSuccess()方法:

public override void OnSuccess(MethodExecutionArgs args) {
    Console.WriteLine("The {0} method executed successfully.", args.Method.Name);
}

OnSuccess()方法在用户代码完成后运行,无异常。覆盖OnExit()方法:

public override void OnExit(MethodExecutionArgs args) {
    Console.WriteLine("The {0} method has exited.", args.Method.Name);
} 

OnExit()方法在用户方法成功或失败完成并退出时执行。相当于一个finally块。最后,覆盖OnException()方法:

public override void OnException(MethodExecutionArgs args) { 
    Console.WriteLine("An exception was thrown in {0}.", args.Method.Name); 
}

在任何用户代码之后,当方法执行失败且出现异常时,OnException()方法执行。相当于一个catch块。

下一步是编写两个我们可以应用LoggingAspect的方法。我们将添加SuccessfulMethod()

[LoggingAspect]
private static void SuccessfulMethod() {
    Console.WriteLine("Hello World, I am a success!");
}

SuccessfulMethod()使用LoggingAspect并向控制台打印消息。现在,让我们添加FailedMethod()

[LoggingAspect]
private static void FailedMethod() {
    Console.WriteLine("Hello World, I am a failure!");
    var x = 1;
    var y = 0;
    var z = x / y;
}

FailedMethod()使用LoggingAspect并向控制台打印消息。然后,它执行零除运算,结果是DivideByZeroException。从Main()方法调用这两种方法,然后运行项目。您应该看到以下输出:

此时,调试器将导致程序退出。就这样。正如您所见,创建自己的 PostSharp 方面以满足您的需求是一个简单的过程。现在,我们将考虑添加我们自己的体系结构约束。

扩展体系结构框架

架构约束是采用定制设计模式,所有模块都必须遵守这些模式。我们将实现一个标量约束来验证代码元素。

我们称为BusinessRulePatternValidation的标量约束将验证从BusinessRule类派生的任何类必须具有名为Factory的嵌套类。首先添加BusinessRulePatternValidation类:

[MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)] 
public class BusinessRulePatternValidation : ScalarConstraint { }

MulticastAttributeUsage指定此验证方面仅在允许类和继承的情况下工作。让我们覆盖ValidateCode()方法:

public override void CodeValidation(object target)  { 
    var targetType = (Type)target; 
    if (targetType.GetNestedType("Factory") == null) { 
        Message.Write( 
            targetType, SeverityType.Warning, 
            "10", 
            "You must include a 'Factory' as a nested type for {0}.", 
            targetType.DeclaringType, 
            targetType.Name); 
    } 
} 

我们的ValidateCode()方法检查目标对象是否具有嵌套的Factory类型。如果Factory类型不存在,则向输出窗口写入异常消息。添加BusinessRule类:

 [BusinessRulePatternValidation]
 public class BusinessRule  { }

BusinessRule类是空的,没有Factory。它被分配了我们的BusinessRulePatternValidation属性,这是一个架构约束。构建您的项目,您将在输出窗口中看到消息。我们现在将开始构建一个可重用类库,您可以在自己的项目中扩展和使用它,以使用 AOP 和装饰器模式解决横切关注点。

在本节中,我们将编写一个可重用库来解决各种交叉关注点。它的功能有限,但它将为您提供进一步扩展项目以满足自己需求所需的知识。您将创建的类库将是一个.NET 标准库,因此它可以用于同时面向.NET Framework 和.NET Core 的应用。您还将创建一个.NET Framework 控制台应用,以查看正在运行的库。

首先创建一个名为CrossCuttingConcerns的新.NET 标准类库。然后,将一个.NET Framework 控制台应用添加到名为TestHarness的解决方案中。我们将添加可重用的功能来解决各种问题,从缓存开始。

添加缓存问题

缓存是一种在访问各种资源时提高性能的存储技术。使用的缓存可以是内存、文件系统或数据库。您使用的缓存类型将取决于项目的需要。在我们的演示中,我们将使用内存缓存来保持简单。

将名为Caching的文件夹添加到CrossCuttingConcerns项目中。然后,添加一个名为MemoryCache的类。将以下 NuGet 包添加到项目中:

  • PostSharp
  • PostSharp.Patterns.Common
  • PostSharp.Patterns.Diagnostics
  • System.Runtime.Caching

使用以下代码更新MemoryCache类:

public static class MemoryCache {
    public static T GetItem<T>(string itemName, TimeSpan timeInCache, Func<T> itemCacheFunction) {
        var cache = System.Runtime.Caching.MemoryCache.Default;
        var cachedItem = (T) cache[itemName];
        if (cachedItem != null) return cachedItem;
        var policy = new CacheItemPolicy {AbsoluteExpiration = DateTimeOffset.Now.Add(timeInCache)};
        cachedItem = itemCacheFunction();
        cache.Set(itemName, cachedItem, policy);
        return cachedItem;
    }
}

GetItem()方法采用缓存项的名称itemName、该项在缓存中的保留时间长度timeInCache,以及调用函数将该项放入缓存(如果该项不在缓存中的话)itemCacheFunction。在TestHarness项目中添加一个新类,并将其命名为TestClass。然后,添加GetCachedItem()GetMessage()方法,如图所示:

public string GetCachedItem() {
    return MemoryCache.GetItem<string>("Message", TimeSpan.FromSeconds(30), GetMessage);
}

private string GetMessage() {
    return "Hello, world of cache!";
}

GetCachedItem()方法从缓存中获取一个名为"Message"的字符串。如果不在缓存中,则通过GetMessage()方法将其存储在缓存中 30 秒。

更新Program类中的Main()方法调用GetCachedItem()方法,如下图:

var harness = new TestClass();
Console.WriteLine(harness.GetCachedItem());
Console.WriteLine(harness.GetCachedItem());
Thread.Sleep(TimeSpan.FromSeconds(1));
Console.WriteLine(harness.GetCachedItem());

GetCachedItem()的第一次调用将该项存储在缓存中,然后返回该项。第二个调用从缓存中获取项并返回它。休眠线程使缓存失效,因此最后一次调用在返回项目之前将其存储在缓存中。

添加文件日志功能

在我们的项目中,日志记录、审核和检测过程将把它们的输出发送到文本文件。因此,我们需要一个类来管理添加不存在的文件,然后将输出添加到这些文件并保存它们。将名为FileSystem的文件夹添加到类库中。然后,添加一个名为LogFile的类。将类设置为public static并添加以下成员变量:

private static string _location = string.Empty;
private static string _filename = string.Empty;
private static string _file = string.Empty;

_location变量被分配给条目程序集的文件夹。_filename变量被分配文件扩展名为的文件名。我们需要在运行时添加Logs文件夹(如果它不存在)。因此,我们将AddDirectory()方法添加到FileSystem类中:

private static void AddDirectory() {
    if (!Directory.Exists(_location))
        Directory.CreateDirectory("Logs");
}

AddDirectory()方法检查位置是否存在。如果不存在,则创建目录。接下来,如果文件不存在,我们需要添加它。因此,增加AddFile()方法:

private static void AddFile() {
    _file = Path.Combine(_location, _filename);
    if (File.Exists(_file)) return;
    using (File.Create($"Logs\\{_filename}")) {

    }
}

AddFile()方法中,我们结合了位置和文件名。如果文件名已经存在,则退出该方法;否则,我们将创建该文件。如果我们不使用using语句,我们在创建第一条记录时会遇到IOException,但随后的保存就可以了。因此,通过使用using语句,我们避免了异常并记录了数据。我们现在可以编写一个方法,实际将数据保存到文件中。增加AppendTextToFile()方法:

public static void AppendTextToFile(string filename, string text) {
    _location = $"{Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location)}\\Logs";
    _filename = filename;
    AddDirectory();
    AddFile();
    File.AppendAllText(_file, text);
}

AppendTextToFile()方法获取文件名和文本,并将位置设置为条目程序集的位置。然后确保文件和目录存在。然后,它将文本保存到指定的文件中。我们的文件日志记录功能现在得到了处理,所以现在,我们可以继续关注日志记录问题。

添加日志问题

大多数应用都需要某种形式的日志记录。通常的日志记录方法包括控制台、文件系统、事件日志和数据库。在我们的项目中,我们将只关注控制台和文本文件日志记录。将名为Logging的文件夹添加到类库中。然后,添加一个名为ConsoleLoggingAspect的文件,并按如下方式进行更新:

[PSerializable]
public class ConsoleLoggingAspect : OnMethodBoundaryAspect { }

[PSerializable]属性通知 PostSharp 生成一个序列化程序供PortableFormatter使用。ConsoleLoggingAspect继承自OnMethodBoundaryAspectOnMethodBoundaryAspect类有一些方法,我们可以重写这些方法,以便在方法体执行之前、方法体执行之后、方法体成功执行时以及遇到异常时添加代码。我们将重写这些方法以向控制台输出消息。在调试代码时,这是一个非常有用的工具,可以查看代码是否实际被调用,以及代码是否成功完成或遇到异常。我们将从覆盖OnEntry()方法开始:

public override void OnEntry(MethodExecutionArgs args) {
    Console.WriteLine($"Method: {args.Method.Name}, OnEntry().");
}

OnEntry()方法在我们的方法主体执行之前执行,我们的覆盖打印出已经执行的方法的名称及其自身的名称。接下来,我们将覆盖OnExit()方法:

public override void OnExit(MethodExecutionArgs args) {
    Console.WriteLine($"Method: {args.Method.Name}, OnExit().");
}

OnExit()方法在我们的方法主体完成执行后执行,我们的覆盖打印出已经执行的方法的名称及其自身的名称。现在,我们将添加OnSuccess()方法:

public override void OnSuccess(MethodExecutionArgs args) {
    Console.WriteLine($"Method: {args.Method.Name}, OnSuccess().");
}

OnSuccess()方法在其应用的方法体完成并返回后执行。当重写执行时,它会打印出已执行方法的名称及其自己的名称。我们将覆盖的最后一个方法是OnException()方法:

public override void OnException(MethodExecutionArgs args) {
    Console.WriteLine($"An exception was thrown in {args.Method.Name}. {args}");
}

OnException()方法在遇到异常时执行,在重写中,我们打印出方法的名称和参数的对象。要应用该属性,请使用[ConsoleLoggingAspect]。要添加文本文件日志方面,请添加一个名为TextFileLoggingAspect的类。TextFileLoggingAspectConsoleLoggingAspect相同,只是覆盖方法的内容不同。OnEntry()OnExit()OnSuccess()方法调用LogFile.AppendTextToFile()方法并将内容附加到Log.txt文件中。OnException()方法也会这样做,只是它会将内容附加到Exception.log文件中。以下是OnEntry()示例:

public override void OnEntry(MethodExecutionArgs args) {
    LogFile.AppendTextToFile("Log.txt", $"\nMethod: {args.Method.Name}, OnEntry().");
}

这就是我们的日志记录。现在,我们将继续添加我们的异常问题。

添加异常处理关注点

软件用户不可避免地会遇到异常。所以,需要有某种方法来记录它们。记录异常的正常方式是将错误存储在用户系统上的文件中,例如使用Exception.log。这就是我们在本节要做的。我们将继承OnExceptionAspect类并将异常数据写入Exception.log文件,该文件将位于我们应用的Logs文件夹中。OnExceptionAspect将标记的方法包装在try/catch块中。在类库中添加一个名为Exceptions的新文件夹,然后添加一个名为ExceptionAspect的文件,代码如下:

[PSerializable]
public class ExceptionAspect : OnExceptionAspect {
    public string Message { get; set; }
    public Type ExceptionType { get; set; }
    public FlowBehavior Behavior { get; set; }

    public override void OnException(MethodExecutionArgs args) {
        var message = args.Exception != null ? args.Exception.Message : "Unknown error occured.";
        LogFile.AppendTextToFile(
            "Exceptions.log", $"\n{DateTime.Now}: Method: {args.Method}, Exception: {message}"
        );
        args.FlowBehavior = FlowBehavior.Continue;
    }

    public override Type GetExceptionType(System.Reflection.MethodBase targetMethod) {
        return ExceptionType;
    }
}

ExceptionAspect类被分配[PSerializable]方面并从OnExceptionAspect继承。我们有三处房产:messageExceptionTypeFlowBehaviormessage包含异常消息,ExceptionType包含遇到的异常类型,FlowBehavior确定异常处理后是继续执行还是进程终止。GetExceptionType()方法返回抛出的异常类型。OnException()方法从构造错误消息开始。然后通过调用LogFile.AppendTextToFile()将异常记录到文件中。最后,异常行为的流被设置为继续。

使用[ExceptionAspect]方面所要做的就是将其作为属性添加到方法中。我们现在已经介绍了异常处理。因此,我们将继续添加我们的安全问题。

添加安全问题

安全需求将特定于正在进行的项目。最常见的问题是,用户经过身份验证和授权,可以访问和使用系统的各个部分。在本节中,我们将使用 decorator 模式使用基于角色的方法实现安全组件。

Security is a very large subject in itself and beyond the scope of this book. There are many good APIs out there, such as the various Microsoft APIs. Refer to https://docs.microsoft.com/en-us/dotnet/standard/security/ for more information, and for OAuth 2.0, refer to https://oauth.net/code/dotnet/. We will leave you to select and implement your own method of security. In this chapter, we simply add our own custom-defined security using the decorator pattern. You can use this as a base for implementing any of the aforementioned security methods.

添加名为Security的新文件夹,并向其添加名为ISecureComponent的接口:

public interface ISecureComponent {
    void AddData(dynamic data);
    int EditData(dynamic data);
    int DeleteData(dynamic data);
    dynamic GetData(dynamic data);
}

我们的安全组件接口包含前面四种方法,它们是不言自明的。dynamic关键字意味着任何类型的数据都可以作为参数传入,GetData()方法可以返回任何类型的数据。接下来,我们需要一个实现接口的抽象类。添加一个名为DecoratorBase的类,如图所示:

public abstract class DecoratorBase : ISecureComponent {
    private readonly ISecureComponent _secureComponent;

    public DecoratorBase(ISecureComponent secureComponent) {
        _secureComponent = secureComponent;
    }
}

DecoratorBase类实现了ISecureComponent。我们声明了一个ISecureComponent类型的成员变量,并在默认构造函数中设置它。我们需要添加缺少的ISecureComponent方法。增加AddData()方法:

public virtual void AddData(dynamic data) {
    _secureComponent.AddData(data);
}

此方法将获取任何类型的数据,然后将其传递到对_secureComponentAddData()方法的调用中。为EditData()DeleteData()GetData()添加缺少的方法。现在,添加一个名为ConcreteSecureComponent的类,它实现了ISecureComponent。对于每个方法,向控制台写入一条消息。对于DeleteData()EditData()方法,也返回一个值1。返回GetData()"Hi!"ConcreteSecureComponent类是执行我们感兴趣的安全工作的类。

我们需要一种方法来验证用户并获得他们的角色。在执行任何方法之前,将检查角色。因此,添加以下结构:

public readonly struct Credentials {
    public static string Role { get; private set; }

    public Credentials(string username, string password) {
        switch (username)
        {
            case "System" when password == "Administrator":
                Role = "Administrator";
                break;
            case "End" when password == "User":
                Role = "Restricted";
                break;
            default:
                Role = "Imposter";
                break;
        }
    }
}

为了简单起见,结构采用用户名和密码并设置适当的角色。受限用户的权限少于管理员。最后一节课是ConcreteDecorator课。添加类,如下所示:

public class ConcreteDecorator : DecoratorBase {
    public ConcreteDecorator(ISecureComponent secureComponent) : base(secureComponent) { }
}

ConcreteDecorator类继承DecoratorBase类。我们的构造函数接受一个类型ISecureComponent并将其传递给基类。增加AddData()方式:

public override void AddData(dynamic data) {
    if (Credentials.Role.Contains("Administrator") || Credentials.Role.Contains("Restricted")) {
        base.AddData((object)data);
    } else {
        throw new UnauthorizedAccessException("Unauthorized");
    }
}

AddMethod()根据允许的AdministratorRestricted角色检查用户的角色。如果用户处于这些角色之一,则在基类中执行AddData()方法;否则,将抛出UnauthorizedAccessException。其余方法遵循相同的模式。覆盖其余的方法,但确保DeleteData()方法只能由管理员执行。

我们现在将把我们的安全关切付诸行动。在Program类的顶部添加以下行:

private static readonly ConcreteDecorator ConcreteDecorator = new ConcreteDecorator(
    new ConcreteSecureComponent()
);

我们正在声明和实例化一个具体的 decorator 对象,并传入具体的安全对象。此对象将在我们的数据方法中引用。更新Main()方法,如下所示:

private static void Main(string[] _) {
    // ReSharper disable once ObjectCreationAsStatement
    new Credentials("End", "User");
    DoSecureWork();
    Console.WriteLine("Press any key to exit.");
    Console.ReadKey();
}

我们将用户名和密码分配给Credentials结构。这导致Role被设置。然后我们称之为DoWork()方法。DoWork()方法将负责调用数据方法。然后我们暂停,让用户按任意键并退出。增加DoWork()方法:

private static void DoSecureWork() {
    AddData();
    EditData();
    DeleteData();
    GetData();
}

DoSecureWork()方法调用在具体装饰器上调用数据方法的每个数据方法。增加AddData()方法:

[ExceptionAspect(consoleOutput: true)]
private static void AddData() {
    ConcreteDecorator.AddData("Hello, world!");
}

[ExceptionAspect]适用于AddData()方法。这将确保将任何错误记录到Exceptions.log文件中。参数设置为true,因此控制台窗口中也会打印错误消息。方法本身调用ConcreteDecorator类上的AddData()方法。按照相同的过程添加其余的方法。然后,运行代码。您应该看到以下输出:

我们现在有了一个工作的基于角色的对象,完成了异常处理。我们的下一步是实现我们的验证关注点。

添加验证关注点

应验证所有用户输入的数据,因为这些数据可能是恶意的、不完整的或格式错误的。您需要确保您的数据是干净的,不会造成伤害。对于我们的演示,我们将实现空验证。首先,将名为Validation的文件夹添加到类库中。然后,添加一个名为AllowNullAttribute的新类:

[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.Property)]
public class AllowNullAttribute : Attribute { }

此属性允许参数、返回值和属性为空。现在,将ValidationFlags枚举添加到同名的新文件中:

[Flags]
public enum ValidationFlags {
    Properties = 1,
    Methods = 2,
    Arguments = 4,
    OutValues = 8,
    ReturnValues = 16,
    NonPublic = 32,
    AllPublicArguments = Properties | Methods | Arguments,
    AllPublic = AllPublicArguments | OutValues | ReturnValues,
    All = AllPublic | NonPublic
}

这些标志用于确定方面可以应用于哪些项目。接下来,我们将添加一个名为ReflectionExtensions的类:

public static class ReflectionExtensions {
    private static bool IsCustomAttributeDefined<T>(this ICustomAttributeProvider value) where T 
        : Attribute  {
        return value.IsDefined(typeof(T), false);
    }

    public static bool AllowsNull(this ICustomAttributeProvider value) {
        return value.IsCustomAttributeDefined<AllowNullAttribute>();
    }

    public static bool MayNotBeNull(this ParameterInfo arg) {
        return !arg.AllowsNull() && !arg.IsOptional && !arg.ParameterType.IsValueType;
    }
}

如果在此成员上定义了属性类型,IsCustomAttributeDefined()方法返回true,否则返回false。如果[AllowNull]属性已经应用,AllowsNull()方法返回true,如果没有,则返回falseMayNotBeNull()方法检查是否允许空值,参数是否可选,以及参数的值类型。然后通过对这些值执行逻辑AND操作返回布尔值。现在是添加DisallowNonNullAspect的时候了:

[PSerializable]
public class DisallowNonNullAspect : OnMethodBoundaryAspect {
    private int[] _inputArgumentsToValidate;
    private int[] _outputArgumentsToValidate;
    private string[] _parameterNames;
    private bool _validateReturnValue;
    private string _memberName;
    private bool _isProperty;

    public DisallowNonNullAspect() : this(ValidationFlags.AllPublic) { }

    public DisallowNonNullAspect(ValidationFlags validationFlags) {
        ValidationFlags = validationFlags;
    }

    public ValidationFlags ValidationFlags { get; set; }
}

此类具有应用于通知 PostSharp 为PortableFormatter生成序列化程序的[PSerializable]属性。它还继承了OnMethodBoundaryAspect类。然后,我们声明变量,将输入和输出参数作为已验证的参数名、返回值验证和成员名保存,并检查正在验证的项是否为属性。默认构造函数配置为允许将验证器应用于所有公共成员。我们还有一个构造函数,它接受一个ValidationFlags值和一个ValidationFlags属性。现在,我们将覆盖CompileTimeValidate()方法:

public override bool CompileTimeValidate(MethodBase method) {
    var methodInformation = MethodInformation.GetMethodInformation(method);
    var parameters = method.GetParameters();

    if (!ValidationFlags.HasFlag(ValidationFlags.NonPublic) && !methodInformation.IsPublic) return false;
    if (!ValidationFlags.HasFlag(ValidationFlags.Properties) && methodInformation.IsProperty) 
        return false;
    if (!ValidationFlags.HasFlag(ValidationFlags.Methods) && !methodInformation.IsProperty) return false;

    _parameterNames = parameters.Select(p => p.Name).ToArray();
    _memberName = methodInformation.Name;
    _isProperty = methodInformation.IsProperty;

    var argumentsToValidate = parameters.Where(p => p.MayNotBeNull()).ToArray();

    _inputArgumentsToValidate = ValidationFlags.HasFlag(ValidationFlags.Arguments) ? argumentsToValidate.Where(p => !p.IsOut).Select(p => p.Position).ToArray() : new int[0];

    _outputArgumentsToValidate = ValidationFlags.HasFlag(ValidationFlags.OutValues) ? argumentsToValidate.Where(p => p.ParameterType.IsByRef).Select(p => p.Position).ToArray() : new int[0];

    if (!methodInformation.IsConstructor) {
        _validateReturnValue = ValidationFlags.HasFlag(ValidationFlags.ReturnValues) &&
                                            methodInformation.ReturnParameter.MayNotBeNull();
    }

    var validationRequired = _validateReturnValue || _inputArgumentsToValidate.Length > 0 || _outputArgumentsToValidate.Length > 0;

    return validationRequired;
}

此方法确保在编译时正确应用方面。如果方面应用于错误类型的成员,则返回false。否则返回true。我们现在覆盖OnEntry()方法:

public override void OnEntry(MethodExecutionArgs args) {
    foreach (var argumentPosition in _inputArgumentsToValidate) {
        if (args.Arguments[argumentPosition] != null) continue;
        var parameterName = _parameterNames[argumentPosition];

        if (_isProperty) {
            throw new ArgumentNullException(parameterName, 
                $"Cannot set the value of property '{_memberName}' to null.");
        } else {
            throw new ArgumentNullException(parameterName);
        }
    }
}

此方法检查输入参数以进行验证。如果有参数为null,则抛出ArgumentNullException;否则,该方法将在不引发异常的情况下退出。现在让我们覆盖OnSuccess()方法:

public override void OnSuccess(MethodExecutionArgs args) {
    foreach (var argumentPosition in _outputArgumentsToValidate) {
        if (args.Arguments[argumentPosition] != null) continue;
        var parameterName = _parameterNames[argumentPosition];
        throw new InvalidOperationException($"Out parameter '{parameterName}' is null.");
    }

    if (!_validateReturnValue || args.ReturnValue != null) return;

    if (_isProperty) {
        throw new InvalidOperationException($"Return value of property '{_memberName}' is null.");
    }
    throw new InvalidOperationException($"Return value of method '{_memberName}' is null.");
}

OnSuccess()方法验证输出参数进行验证。如果任何参数为空,则会抛出InvalidOperationException。接下来我们需要做的是添加private class来提取方法信息。将以下类添加到关闭括号之前的DisallowNonNullAspect类的底部:

private class MethodInformation { }

将以下三个构造函数添加到MethodInformation类中:

 private MethodInformation(ConstructorInfo constructor) : this((MethodBase)constructor) {
     IsConstructor = true;
     Name = constructor.Name;
 }

 private MethodInformation(MethodInfo method) : this((MethodBase)method) {
     IsConstructor = false;
     Name = method.Name;
     if (method.IsSpecialName &&
     (Name.StartsWith("set_", StringComparison.Ordinal) ||
     Name.StartsWith("get_", StringComparison.Ordinal))) {
         Name = Name.Substring(4);
         IsProperty = true;
     }
     ReturnParameter = method.ReturnParameter;
 }

 private MethodInformation(MethodBase method)
 {
     IsPublic = method.IsPublic;
 }

这些构造函数区分构造函数和方法,并对方法执行必要的初始化。添加以下方法:

private static MethodInformation CreateInstance(MethodInfo method) {
    return new MethodInformation(method);
}

CreateInstance()方法根据传入方法的MethodInfo数据创建MethodInformation类的新实例,并返回该实例。增加GetMethodInformation()方式:

public static MethodInformation GetMethodInformation(MethodBase methodBase) {
    var ctor = methodBase as ConstructorInfo;
    if (ctor != null) return new MethodInformation(ctor);
    var method = methodBase as MethodInfo;
    return method == null ? null : CreateInstance(method);
}

此方法将methodBase强制转换为ConstructorInfo并检查null。如果ctor不是null,则根据构造函数生成新的MethodInformation类。但是,如果ctornull,则methodBase被转换为MethodInfo。如果方法不是null,则调用CreateInstance()方法,并传入该方法。否则返回null。最后,向类添加以下属性:

public string Name { get; private set; }
public bool IsProperty { get; private set; }
public bool IsPublic { get; private set; }
public bool IsConstructor { get; private set; }
public ParameterInfo ReturnParameter { get; private set; }

这些属性是应用了方面的方法的属性。我们现在已经完成了验证方面的编写。您现在可以使用验证器通过附加[AllowNull]属性来允许空值。您可以通过附加[DisallowNonNullAspect]来禁止空值。现在,我们将添加我们的事务关注点。

添加事务关注点

事务是必须运行到完成或回滚的进程。在类库Transactions中添加一个新文件夹,然后添加RequiresTransactionAspect类:

[PSerializable]
[AttributeUsage(AttributeTargets.Method)]
public sealed class RequiresTransactionAspect : OnMethodBoundaryAspect {
    public override void OnEntry(MethodExecutionArgs args) {
        var transactionScope = new TransactionScope(TransactionScopeOption.Required);
        args.MethodExecutionTag = transactionScope;
    }

    public override void OnSuccess(MethodExecutionArgs args) {
        var transactionScope = (TransactionScope)args.MethodExecutionTag;
        transactionScope.Complete();
    }

    public override void OnExit(MethodExecutionArgs args) {
        var transactionScope = (TransactionScope)args.MethodExecutionTag;
        transactionScope.Dispose();
    }
}

OnEntry()方法启动事务,OnSuccess()方法完成异常,OnExit()方法处理事务。要使用方面,请在方法中添加[RequiresTransactionAspect]。要记录阻止交易完成的任何异常,您还可以分配[ExceptionAspect(consoleOutput: false)]方面。接下来,我们将添加资源池关注点。

添加资源池关注点

当一个对象的多个实例的创建和销毁成本很高时,资源池是提高性能的好方法。我们将为我们的需求创建一个非常简单的资源池。添加名为ResourcePooling的文件夹,然后添加ResourcePool类:

public class ResourcePool<T> {
    private readonly ConcurrentBag<T> _resources;
    private readonly Func<T> _resourceGenerator;

    public ResourcePool(Func<T> resourceGenerator) {
        _resourceGenerator = resourceGenerator ??
                                 throw new ArgumentNullException(nameof(resourceGenerator));
        _resources = new ConcurrentBag<T>();
    }

    public T Get() => _resources.TryTake(out T item) ? item : _resourceGenerator();
    public void Return(T item) => _resources.Add(item);
}

此类创建一个新的资源生成器并将资源存储在ConcurrentBag中。当请求一个项目时,它会从池中发出一个资源。如果一个不存在,则创建它,将其添加到池中,并发送给调用方:

var pool = new ResourcePool<Course>(() => new Course()); // Create a new pool of Course objects.
var course = pool.Get(); // Get course from pool.
pool.Return(course); // Return the course to the pool.

您刚才看到的代码向您展示了如何使用ResourcePool类创建一个池,获取一个资源,并将其返回到池中。

添加配置设置问题

配置设置应始终集中。由于桌面应用将其设置存储在app.config文件中,而 web 应用将其设置存储在Web.config中,因此我们可以使用ConfigurationManager访问应用设置。将System.Configuration.ConfigurationNuGet 库添加到类库中并测试线束。然后,添加名为Configuration的文件夹和以下Settings类:

public static class Settings {
    public static string GetAppSetting(string key) {
        return System.Configuration.ConfigurationManager.AppSettings[key];
    }

    public static void SetAppSettings(this string key, string value) {
        System.Configuration.ConfigurationManager.AppSettings[key] = value;
    }
}

此类将获取并设置Web.config文件和App.config文件中的应用设置。要在文件中包含该类,请添加以下using语句:

using static CrossCuttingConcerns.Configuration.Settings;

以下代码显示了如何使用这些方法:

Console.WriteLine(GetAppSetting("Greeting"));
"Greeting".SetAppSettings("Goodbye, my friends!");
Console.WriteLine(GetAppSetting("Greeting"));

使用静态导入,您不必包含class前缀。您可以扩展Settings类以获取连接字符串,或者在应用中执行任何需要的配置。

添加仪器问题

我们的最后一个交叉关注点是仪器仪表。我们使用工具来评测应用,并查看方法执行所需的时间。在类库中添加一个名为Instrumentation的文件夹,然后添加InstrumentationAspect类,如图所示:


[PSerializable]
[AttributeUsage(AttributeTargets.Method)]
public class InstrumentationAspect : OnMethodBoundaryAspect {
    public override void OnEntry(MethodExecutionArgs args) {
        LogFile.AppendTextToFile("Profile.log", 
            $"\nMethod: {args.Method.Name}, Start Time: {DateTime.Now}");
        args.MethodExecutionTag = Stopwatch.StartNew();
    }

    public override void OnException(MethodExecutionArgs args) {
        LogFile.AppendTextToFile("Exception.log", 
            $"\n{DateTime.Now}: {args.Exception.Source} - {args.Exception.Message}");
    }

    public override void OnExit(MethodExecutionArgs args) {
        var stopwatch = (Stopwatch)args.MethodExecutionTag;
        stopwatch.Stop();
        LogFile.AppendTextToFile("Profile.log", 
            $"\nMethod: {args.Method.Name}, Stop Time: {DateTime.Now}, Duration: {stopwatch.Elapsed}");
    }
}

如您所见,插装方面仅适用于方法,乘以方法的开始和停止时间,并将概要信息记录到Profile.log文件中。如果遇到异常,则将异常记录到Exception.log文件中。

我们现在有了一个功能强大、可重用的横切关注点库。让我们总结一下我们在本章学到的知识。

我们学到了一些有价值的信息。我们首先查看装饰器模式,然后是代理模式。代理模式提供的对象可以替代客户端使用的实际服务对象。代理接收客户端请求,执行必要的工作,然后将请求传递给服务对象。由于代理与它们所替代的服务共享相同的接口,因此它们是可互换的。

在覆盖代理模式之后,我们使用 PostSharp 进入 AOP。我们看到了如何使用方面和属性来修饰代码,以便在编译时,它注入代码来执行所需的操作,例如异常处理、日志记录、审计和安全性。我们通过开发自己的 aspect 扩展了 aspect 框架,并研究了如何使用 PostSharp 和 decorator 模式来解决配置管理、日志记录、审核、安全性、验证、异常处理、检测、事务、资源池、缓存、线程和并发等交叉问题。

在下一章中,我们将介绍如何使用工具来帮助您提高代码质量。但在此之前,先测试你的知识,然后进一步阅读。

  1. 什么是交叉关注点?AOP 代表什么?
  2. 什么是方面?如何应用方面?
  3. 什么是属性?如何应用属性?
  4. 方面和属性如何协同工作?
  5. 构建过程如何处理方面?

教程来源于Github,感谢apachecn大佬的无私奉献,致敬!

技术教程推荐

MySQL实战45讲 -〔林晓斌〕

SQL必知必会 -〔陈旸〕

消息队列高手课 -〔李玥〕

罗剑锋的C++实战笔记 -〔罗剑锋〕

Web漏洞挖掘实战 -〔王昊天〕

深入浅出可观测性 -〔翁一磊〕

高并发系统实战课 -〔徐长龙〕

快速上手C++数据结构与算法 -〔王健伟〕

结构思考力 · 透过结构看思考 -〔李忠秋〕