NET 中的面向切面编程 - 第二部分






3.30/5 (21投票s)
一篇关于面向切面编程的文章,包含一个示例实现。
目录
属性编程
大家好,我从未写过分部分文章。但主题要求迫使我将其分成几部分。如果你直接来到这个页面,最好先阅读本文的第一部分,以清除 AOP 基础知识。我们将继续处理第一部分中讨论的客户销售应用程序的相同问题。我们在上一篇文章中看到,代码混乱源于横切关注点。我们还发现,为了解决这个问题,我们将横切关注点与主要关注点分开。
如果你仔细研究 .NET 架构中核心关注点和横切关注点的分离,它已经使用“属性编程”实现了。在 COM 时代,如果我想序列化一个组件,我必须编写大量的代码。序列化代码(横切关注点)和组件业务关注点(核心关注点)总是纠缠在一起。所以你将所有横切关注点代码移动到属性中,你的方法实现主要关注点。在下面的代码中,你可以看到客户类“Add
”方法的核心关注点使用“属性编程”与对象的“序列化”问题分开。
图 2.1 C# 属性编程分离关注点的方式
那么属性编程是面向切面编程吗?好吧,这又是一个争论。但从我的角度来看,答案是“否”,属性编程是分离关注点的一种方式。但 AOP 远不止于此,我们定义了连接点、切入点和建议等。因此,我将得出结论,属性编程只能分离关注点,这是 AOP 的一个特性。
好的,那么让我们尝试使用“属性编程”解决我们之前客户销售应用程序的问题。
定义自定义属性
在本节中,我们将使用自定义属性来实现 AOP 关注点分离。为了使类用作属性,它应该继承自“System.Attribute
”。所以我们将我们的两个横切关注点类都继承自“System.Attribute
”。
让我们快速浏览一下使类成为属性所需的步骤
- 首先,该类应继承自“
System.attribute
”。public sealed class ClsEmail : System.Attribute
- 其次,定义属性用法。属性用法说明该类将在哪个级别上被赋予属性,即方法级别、类级别等。我将其留给用户进一步研究属性用法基础知识。目前,为了简化起见,我们将保留所有这些。
[AttributeUsage(AttributeTargets.All)]
- 为了设置类的局部变量,我们需要修改构造函数。例如,在下面的“
ClsEmail
”代码中,我们有一个属性“strEmail
”。由于这是面向属性的编程,我们不能使用“set
”和“get
”。所以我们想要设置的任何属性,都必须在构造函数中定义。例如,“ClsEmail
”构造函数被修改,以便电子邮件属性可以传递给它。你可以看到我们在对象创建时就设置了电子邮件属性。这是这种方法最大的缺点。“Set”和“Get”的优雅性丢失了。public ClsEmail(string pstrEmail) { // // TODO: Add constructor logic here // strEmail = pstrEmail; Send(); }
- 最后,我将此属性添加到“
ClsCustomer
”类的“Add
”方法中。请注意,我正在使用构造函数设置电子邮件地址,这看起来非常糟糕。[ClsEmail("shiv_koirala@yahoo.com")] public void Add() { Console.WriteLine("Add routine called of the customer"); }
好的,下面是所有三个代码的粘贴:即两个横切关注点(电子邮件和打印)和主要关注点(客户)。希望你能够理解。
横切电子邮件代码
using System;
using System.Reflection;
namespace CustomerSalesClassProject
{
/// <summary>
/// Summary description for ClsEmail.
/// </summary>
[AttributeUsage(AttributeTargets.All)]
public sealed class ClsEmail : System.Attribute
{
private string strEmail;
public ClsEmail(string pstrEmail)
{
//
// TODO: Add constructor logic here
//
strEmail = pstrEmail;
Send();
}
public void Send()
{
// This method sends email
Console.WriteLine("Email sent to admin");
Console.ReadLine();
}
}
}
Email 类继承自“System.Attribute
”。我将简要解释上述代码。横切关注点打印类
using System;
namespace CustomerSalesClassProject
{
/// <summary>
/// Summary description for ClsPrint.
/// </summary>
[AttributeUsage(AttributeTargets.All)]
public class ClsPrint : System.Attribute
{
private string strPrinterName;
public ClsPrint(string pstrPrinterName)
{
//
// TODO: Add constructor logic here
//
strPrinterName = pstrPrinterName;
Print();
}
public bool Print()
{
// This methods prints the sales or the customer data
Console.WriteLine("Printed to printer");
return true;
}
}
}
最后,调用这两个关注点的客户类
using System;
using System.Reflection;
namespace CustomerSalesClassProject
{
/// <summary>
/// Summary description for Class1.
/// </summary>
public class ClsCustomer : ContextBoundObject
{
public ClsCustomer()
{
//
// TODO: Add constructor logic here
//
}
// Previously tangled code
/*
public void Add()
{
/////////////////////////////////////////
// This method adds customer information
//
// Adding code will go here
////////////////////////////////////////
// After adding to database email is sent
ClsEmail pobjClsEmail = new ClsEmail();
pobjClsEmail.Send();
// After sending email its printed
ClsPrint pobjClsPrint = new ClsPrint();
pobjClsPrint.Print();
}
*/
// Attribute applied code
[ClsEmail("shiv_koirala@yahoo.com")]
[ClsPrint("myprinter")]
public void Add()
{
Console.WriteLine("Add routine called of the customer");
}
}
}
属性编程问题
以下是使用属性编程实现 AOP 的问题
- 上述定义方法的主要问题是我无法指定我的 Advice。简而言之,我无法指定横切关注点代码应该在何时运行。你可以看到两个横切关注点都在客户添加到数据库之前触发。正确的方法是先将客户添加到数据库,然后发送电子邮件并将其打印到打印机上。我们将在下一节中看到如何使用“上下文”属性定义横切关注点代码应何时处理。
- 其次,我所有的属性都是通过类的构造函数设置的,这是一种非常丑陋的做法。这意味着我们所有的业务逻辑也应该通过构造函数运行。
- 最后一个也是最主要的问题是我必须通过在方法上指定属性来更改主要核心关注类。如果我只是不同地定义和编码两个类(横切关注点),然后在编译时指定将这两个横切关注点与主要关注点编织在一起并生成那个组合的魔术代码,那不是最好的吗?嗯...听起来像一个梦想,但我们将在下一节中实际做到这一点。
第二个问题无法真正解决,因为这就是 .NET 属性编程的工作方式。因此,为了解决这个问题,要么等待 .NET 变得 AOP 兼容,要么就接受它。
上下文绑定对象
首先,让我们尝试定义什么是上下文。上下文定义了一个环境,它具有对象行为的规则和规定。因此,当一个对象绑定到一个上下文时,它们就受该上下文规则的约束。现在,当对象进入或离开上下文定义的这个环境时,这些规则就会被应用。如果对象未绑定到上下文,则称其为敏捷对象。
上下文在对象激活时创建。如果存在现有上下文,则将对象放置在该上下文中,否则将创建一个新上下文并将对象放置在该上下文中。
所以,例如,我有一个名为“ContextParties”的上下文,它绑定了“Customer”和“Suppliers”等对象。因此,当创建客户或供应商对象时,如果“ContextParties”不存在,它将被创建,并且对象将被放置在该上下文中。
好的,现在最后一个关于上下文绑定对象调用和消息传递的基础知识需要澄清。如果任何外部客户端想要使用上下文绑定对象,它们不能直接调用它们,而是应该使用代理。因此,所有客户端都获取一个代理的引用,该代理向上下文发送消息,上下文与实际对象通信。现在,当方法消息跨越上下文时,你可以拦截这些消息并应用一些预处理或后处理......嗯,这解决了我们在属性编程问题中定义的第一个问题。
上下文绑定对象确实很混乱,投入精力解释如何实现它们不在本文的范围之内。如果你想了解如何操作,可以在 Google 上搜索。有大量优秀的文章展示了这种能力。我不想让你们在理解实现 AOP 的迂回方式上失去耐心,而是想让你们理解正确的方式。我确信在阅读了上下文绑定之后,你会明白我放弃它并直接给出 AOP 的真实情况是正确的。
使用 EOS 编译器
如前所述,我们无法使用当前的 .NET 框架进行编织。因此,目前无法看到使用当前编译器的实时实现。我确信一年后,这篇文章会显得非常愚蠢,到那时微软应该已经使他们的编译器兼容 AOP 了。
为了演示 AOP 的能力,我们将使用 EOS 编译器,你可以从这里下载。
那么 EOS 是什么呢?我只是从上面给出的网站上粘贴的。
EOS 是 Microsoft® .NET Framework™ 上 C# 的面向切面扩展。EOS 旨在从三个维度改进当前的面向切面语言模型。首先,它概括了切面实例化和建议编织模型,以消除当切面用于表达某些横切关注点时不可避免的变通方法的需要。其次,它概括了连接点模型。第三,它旨在消除类和切面构造之间的区别,转而采用单个概念构建块,该构建块结合了当前类和切面的表达能力,显著提高了语言设计的概念完整性和统一性。
所以首先你需要从这里下载编译器。下面的示例代码是使用 0.3.3 版本编译的。你将得到一个包含编译器、文档等的 ZIP 文件。解压后,你将看到三个主要文件夹。
- Bin(这是编译器所在的位置,它是 C# 编译器的扩展。)
- Docs(文档。)
- Examples(示例代码。)
一旦你拥有了所有这些,让我们编写一个 AOP 示例并编译,使用 EOS 编译器查看输出。
使用 EOS 编译器编译示例代码
好的,各位,解压并查看了各个文件夹之后,让我们尝试一下如何使用 EOP 编译一个实现 AOP 基本原理的 C# 程序。我们将继续使用我们首先描述的相同的“客户”和“打印机”问题。让我回顾一下问题是什么:客户类为客户更新客户数据。在客户数据更新之后,我们希望打印客户详细信息。所以我们的切面问题是打印,主要关注点是客户数据更新。
所以我们将定义三个部分
- 客户类(主要关注点)。
- 打印机类(切面)。
- 运行并查看输出的主方法。
所以首先是客户类
public class ClsCustomer
{
public ClsCustomer()
{
//
// TODO: Add constructor logic here
//
}
public void Update()
{
System.Console.WriteLine("Update Method called");
}
}
上面的客户类内容不多,它有一个进行数据库操作的Update
方法。
现在是第二个切面打印机类……这真是太棒了,里面有新的语法。我将所有 AOP 语法都用粗体标出,这样你就可以想象未来可能会出现什么类型的语法。由于打印机是我的切面,我定义了一个名为“ClsCrossCutprinter
”的切面类。
public aspect ClsCrossCutPrinter
{
public ClsCrossCutPrinter()
{
//
// TODO: Add constructor logic here
//
}
第二件事是“introduce
”关键字,它表示将此方法(Print
)织入客户类中。
introduce in Customer.ClsCustomer
{
public static void Print()
{
System.Console.WriteLine("Print cross cut concern called");
Console.ReadLine();
}
}
最后我的建议说它应该在何时执行:即在Update
方法执行之后。
static after execution (public void
Customer.ClsCustomer.Update()): call proxyprint();
public void proxyprint()
{
Customer.ClsCustomer.Print();
}
}
最后是运行所有内容的主静态方法。这里没有发生任何特殊情况:我们正在创建客户类并执行“Add
”方法。
public class NameSpaceRunningtheProgram
{
public static void Main(string[] arguments)
{
ClsCustomer cust = new ClsCustomer();
cust.Update();
}
}
好了,伙计们,我们没有在 Customer 类中写一个字,切面将代码插入到类中。现在是编译这个类的时候了。
所以进入 bin 目录并运行:eos.exe "c:\*.cs"。
如果一切编译正常,你将看到以下输出
Eos Compiler Version 0.3
Copyright (C) The Rector and Visitors of the University of Virginia.
All rights reserved.
好的,现在让我们尝试运行编译后的 EXE。你将收到类似这样的错误
图 2.2 错误,需要将“Eos.Runtime.dll”复制到 EXE 文件夹
好的,错误是因为你需要将“Eos.Runtime.dll”复制到 EXE 所在的文件夹。所以只需进入bin目录并复制文件。现在当你运行 EXE 时,你将看到以下输出。
Update Method called
Print cross cut concern called
在不更改客户类的情况下,调用了更新方法,然后是打印横切方法……我还在本文顶部提供了一个包含完整源代码的 Zip 文件。
好的,伙计们,这是一篇很长的文章,但希望它能对 .NET 社区有所帮助。
请对我的 .NET 面试问题书籍提供反馈。
如需进一步阅读,请观看下面的面试准备视频和分步视频系列。