65.9K
CodeProject 正在变化。 阅读更多。
Home

面向方面编程:循序渐进学习并创建自己的实现!

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (128投票s)

2012年10月18日

CPOL

21分钟阅读

viewsIcon

278562

downloadIcon

2074

一次 AOP 之旅,涵盖关注点、切入点、连接点、通知、方面、拦截器、代理、目标、混入、组合……

本文始于我需要一个良好而简单的拦截机制,以解决一些可以使用 AOP 技术实现的需求。市面上有许多拦截器,但大多数(Castle.net、Spring.net、LinFu 等)都需要在运行时通过 IL 代码发出动态子类,因此在可拦截的类方面,它们几乎总是面临相同的限制:它们不能是静态的,必须是非密封的,属性和方法必须是虚拟的,等等……
其他拦截机制需要更改构建过程或购买许可证。我无法负担其中任何一种……

目录

引言

AOP 代表面向方面编程。我猜每位读者都熟悉该缩写中的 OP 部分,因此我们需要阐明“方面”的含义,别担心,我们将在文章后面讨论这个问题。

我将(尝试)将本文保持在初级水平。阅读本文的唯一要求是了解面向对象编程概念!

在我看来,正确理解一个概念会让你更好地使用它的实现。但是,我确实理解这篇文章有点长,所以如果你感到无聊或气馁,我仍然鼓励你跳到实现部分。你稍后仍然可以回来学习理论。

高级开发人员,请阅读此文!

如果您熟悉 AOP,请不要离开!
让我开门见山地说明本文将提供什么……

我将介绍一种拦截技术,它允许拦截

  • 任何类(包括密封类、静态类、值类型)
  • 构造函数
  • 类型初始化器
  • 实例方法、属性和事件(即使它们未标记为虚拟)
  • 静态方法、属性和事件

无需

  • 编织您的代码或程序集
  • 发出 IL 代码
  • 使用任何动态特性
  • 修改您的目标类
  • 强制编织器实现任何东西(例如 MarshalByRef)

基本上,我们谈论的是一种纯托管代码技术,它可以在 .Net 1.1 上运行(实际上我使用了一些 Linq 位,但您可以轻松更改它),它允许您拦截您能想到的一切。

让我们更清楚一些。使用以下技术

您可以拦截诸如 System.DateTime.NowSystem.IO.File 操作之类的东西!
您没有最流行的拦截库的常见限制。

你们怀疑吗?那请继续阅读!!

AOP 的原理

背景

有些人可能认为他们还没有完全掌握面向对象编程,那么他们为什么要从 OOP 转向 AOP 并放弃多年来辛辛苦苦学到的所有概念呢?答案很简单:没有这种转换。没有:OOP 与 AOP 之争! AOP 是一个概念,在我看来,它的名称具有误导性。采用 AOP 原则让您处理类、对象、接口、继承、多态、抽象等等……因此您仍然完全沉浸在 OOP 世界中,不会迷失方向。

在您的代码中应用 AOP 概念时,您试图放宽 OOP 的一个特定原则(并非最不重要的原则)——封装,以解决横切关注点(我们稍后会再讨论)。

在过去,当互联网还只是黄页、公告板和 Usenet 时,如果你想学习任何东西,最好是读书(给 Y 世代的评论:书是一种里面塞满了写有文字的纸张的东西)。所有这些书都会大致提醒你关于 OOP 的主题:

  • 规则 #1:你必须封装所有数据和代码
  • 规则 #2:永远不要打破规则 #1

封装是引入 OOP 概念到第三代语言(3GL)的最终目标。

Wikipedia
封装用于指代两个相关但不同的概念,有时也指它们的组合
  • 一种限制对对象某些组件访问的语言机制。
  • 一种语言结构,便于将数据与对该数据进行操作的方法(或其他函数)捆绑在一起。

AOP 在某种程度上声称,有时应该可以使用
一种语言构造,便于将操作封装数据的方法(或其他函数)与数据捆绑在一起,而无需数据本身。

明白了?那么你就明白了所有关于理论的知识。干得好!

现在我们自然而然地引出以下两个问题

  • “有时”是什么时候?
  • 为什么会这样呢?

关于 AOP 的实用性……一些场景

让我们看看以下情况

场景 A

您是银行的软件开发人员。银行有一个运行良好的运营系统。业务运行顺畅。政府发布了一项政策,强制银行承诺某种形式的透明度。每当有资金进出银行时,都应记录。政府公开表示,这是朝着透明度迈出的第一步,但预计会有更多措施。

场景 B

您的 Web 应用程序已发布给测试团队。所有功能测试都通过了,但应用程序在负载测试中失败了。一个非功能性需求声明,任何页面在服务器上处理时间不得超过 500 毫秒。经过分析,有数十个对数据库的查询可以通过缓存结果来避免。

场景 c

你花了两年时间在一个由 200 多个类组成的完美库中建模你的领域模型。最近有人告诉你将编写一个新的应用程序前端,并且这个人需要将你的对象绑定到 UI。但是为了简化该任务,你所有的类现在都应该实现 INotifyPropertyChanged

这些例子在 AOP 何时以及为何能派上用场方面是有效的。这些场景有以下共同点

横切关注点

“有时”是什么时候?

一些旨在实现给定功能的类(银行类、数据访问服务、领域模型类等)必须进行修改,以处理基本上“不属于它们自己业务”的需求。

  1. 银行类的目的是资金交换。日志记录关注点是政府的利益。
  2. 数据服务类的目的是检索数据。数据缓存关注点是非功能性需求。
  3. 领域模型类的目的是处理您公司的业务。关于属性更改通知的关注点在于 UI 的利益。

无论何时,您都必须在不同的类中编写一些代码,以满足这些类之外的利益。在 AOP 术语中,这被称为横切关注点

横切关注点的概念是 AOP 的核心。没有横切关注点 = 不需要 AOP!

为什么会这样呢?

让我们仔细看看场景 C。
你的问题是你的领域模型中每个类平均暴露了 5 个属性。有 200 多个类,你将不得不实现(复制/粘贴)超过 1,000 次一些样板代码,将看起来像这样的东西转换成

public class Customer
{
    public string Name { get; set; }
}

变成那样

public class Customer : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
        
    private string _Name;
    public string Name 
    { 
        get { return _Name; }
        set
        {
            if (value != _Name)
            {
                _Name = value;
                SignalPropertyChanged("Name");
            }
        }
    }

    void SignalPropertyChanged(string propertyName)
    {
        var pcEvent = this.PropertyChanged;
        if (pcEvent != null) pcEvent(this, new PropertyChangedEventArgs(propertyName));
    }
}

呃!这还只是一个属性!顺便说一下,你知道那个实习生两天前离职了吗?

你肯定不需要进一步解释就能明白原因

应该可以“将操作封装数据的方法与数据捆绑在一起,而无需数据”,换句话说:将横切的 INotifyPropertyChanged 关注点的实现外部化,对 Customer 等领域模型类没有或只有最小的影响。

如果我们能做到这一点,那么我们将

  1. 实现适当的关注点分离
  2. 避免代码重复,从而方便代码维护
  3. 避免将领域类的核心业务隐藏在大量的样板代码之下

是啊……好吧,这一切听起来很棒,很花哨,但是我们怎么做呢?

理论上的答案

我们有一个横切关注点,它需要在几个类(从现在开始称为目标)中执行一些代码。
在 AOP 世界中,实现(实现日志、缓存或其他任何功能的代码)简单地称为关注点

那么我们应该能够将我们的关注点(我重复一遍,因为这很重要:关注点是您横切关注点的实现)附加(注入、引入等……选择您的词)到目标的任何选定位置。
我们应该能够选择目标中的以下任何位置来附加我们的关注点

  • 静态初始化器
  • 构造函数
  • 静态属性获取器和设置器
  • 实例属性获取器和设置器
  • 静态方法
  • 实例方法
  • 析构函数

在一个完美的 AOP 世界中,我们应该能够将我们的关注点附加到目标的任何一行代码上。

很好,但是如果我们想附加一个关注点,我们需要在目标中有一个钩子,不是吗?是的,船长!
在 AOP 中,钩子(您的关注点将在此处附加并执行)有一个名称:它是一个切入点。而您实际附加代码的位置也有一个名称:它是一个连接点

够清楚吗?也许不是……这里有一些伪代码,希望能演示这个想法

// Target class
class BankAccount
{
    public string AccountNumber {get;}
    public int Balance {get; set;}
    void Withdraw(int AmountToWithdraw)
    {
       
:public pointcut1;   // a pointcut (imagine something like a label for goto statement in BASIC or T-SQL etc...)
        Balance -= AmountToWithdraw;
    }
}

// Concern 
concern LoggingConcern
{
    void LogWithdraw(int AmountToWithdraw)
    { 
        // Here you have to imagine that some kind of 'magic' has happened
        // and 'this' is an instance of the BankAccount class.
        Console.WriteLine(this.AccountNumber + " withdrawal on-going...");
    }
}

class Program
{
    void Main()
    {
        // get a reference to the pointcut marker  through reflection
        pointcut = typeof(Bank).GetPointcut("pointcut1");

        // this is the joinpoint 
        LoggingConcern.Join(cutpoint,
    LogWithdraw); 

        // After joinpoint the runtime should have (in a sort of registry)
        // a record which tells to execute our LoggingConcern at pointcut1 of Bank class
    }
}

C# 开箱即用地提供这种机制岂不是很好?

更多概念

在我们进入实际实现之前,让我们介绍几个更多的定义……

什么是方面
它是关注点切入点连接点的组合。
思考一秒钟,我希望它会非常清晰:我有一个日志机制(关注点),我注册它的日志方法在我的应用程序代码的给定位置执行(连接点),这就是我的应用程序的一个方面

但是等一下……一个关注点一旦被注入,它能够/应该做什么?

关注点分为两类

  • 副作用:
    副作用是一种关注点,它不会改变切入点处的代码行为。副作用只是引入一个额外的操作来执行。
    日志记录关注点副作用的一个很好的例子:每当运行时执行目标方法(例如:Bank.Withdraw(int Amount))时,LoggingConcern.LogWithdraw(int Amount) 方法将被执行,并且 Bank.Withdraw(int Amount) 方法将继续执行。

  • 建议:
    通知是一个关注点,它可能会改变方法的输入/输出。
    缓存关注点是一个完美的例子:每当运行时执行目标方法(例如:CustomerService.GetById(int Id))时,CachingConcern.TryGetCustomerById(int Id) 将被执行,如果缓存中找到值,则强制 CustomerService.GetById(int Id) 方法返回该值,否则让执行继续。

    通知应该允许
    • 检查目标切入点处的输入参数,并在需要时修改它们
    • 取消目标方法的执行并用不同的实现替换它
    • 检查目标方法的输出结果并修改或替换它

到目前为止,如果你还在阅读,那么恭喜你!太棒了!因为你值得拥有……

我们已经完成了 AOP 的通用概念和思想。让我们继续前进,看看如何用 C# 实现它……

我们的实现

给我看代码,否则我就杀了狗!

关注点

关注点应该具有一个神奇的 this 行为,它是我们的目标类型。
这没问题!

public interface IConcern<T>
{
    T This { get; } // ok, that's a bit of cheating but who cares?
}

切入点参考

并没有简单的方法可以获取每一行代码的切入点。但是我们可以在每次方法调用时获取一个,这通过使用 System.Reflection.MethodBase 类是相当容易的。MSDN 对此的描述并不详尽:“提供有关方法和构造函数的信息。”[原文如此]。

在你我之间,使用 MethodBase 获取切入点的引用是你可以使用的最强大的可能性。

您可以获取构造函数、方法、属性和事件的切入点引用,因为对于 .Net 而言,您在代码中声明的几乎所有内容(除了字段)最终都是一个方法……
自己看

            
public class Customer 
{
    public event EventHandler<EventArgs> NameChanged;
    public string Name { get; private set; }
        
    public void ChangeName(string newName) 
    {
        Name = newName;
        NameChanged(this, EventArgs.Empty);
    }
}

class Program
{
    static void Main(string[] args)
    {
        var t = typeof(Customer);

        // Constructor (not limited to parameterless)
        var pointcut1 = t.GetConstructor(new Type[] { });

        // The ChangeName method
        var pointcut2 = t.GetMethod("ChangeName");

        // the Name property
        var nameProperty = t.GetProperty("Name");
        var pointcut3 = nameProperty.GetGetMethod();
        var pointcut4 = nameProperty.GetSetMethod();

        // Everything about the NameChanged event
        var NameChangedEvent = t.GetEvent("NameChanged");
        var pointcut5 = NameChangedEvent.GetRaiseMethod();
        var pointcut6 = NameChangedEvent.GetAddMethod();
        var pointcut7 = NameChangedEvent.GetRemoveMethod();
    }
}

连接点

编写连接代码也相当容易。看看下面的方法签名

void Join(System.Reflection.MethodBase pointcutMethod, System.Reflection.MethodBase concernMethod);

我们可以将这个签名添加到我们稍后将提供的某种注册表中,并且我们已经可以想象编写这样的代码了!!!

public class Customer
{
    public string Name { get; set;}
    public void DoYourOwnBusiness() 
    {
        System.Diagnostics.Trace.WriteLine(Name + " is doing is own business");
    }
}

public class LoggingConcern : IConcern<Customer>
{
    public Customer This { get; set; }

    public void DoSomething() 
    {
        System.Diagnostics.Trace.WriteLine(This.Name + " is going to do is own business");

        This.DoYourOwnBusiness();
            
        System.Diagnostics.Trace.WriteLine(This.Name + " has finished doing its own business");
    }
}

class Program
{
    static void Main(string[] args)h
    {
        // Get a pointcut for Customer.DoSomething();
        var pointcut1 = typeof(Customer).GetMethod("DoSomething");
        var concernMethod = typeof(LoggingConcern).GetMethod("DoSomething");

        // Join them
        AOP.Registry.Join(pointcut1, concernMethod);
    }
}

我们离我们的伪代码还有多远?我个人认为不多……
那接下来呢?

将一切连接起来……

这正是问题和乐趣同时开始的地方!

但让我们从简单开始

注册表

注册表将保留有关连接点的记录。它是一个连接点项的单例列表。
一个连接点是一个简单的结构体

public struct Joinpoint
{
    internal MethodBase PointcutMethod;
    internal MethodBase ConcernMethod;
    
    private Joinpoint(MethodBase pointcutMethod, MethodBase concernMethod)
    {
        PointcutMethod = pointcutMethod;
        ConcernMethod = concernMethod;
    }

    // Utility method to create joinpoints 
    public static Joinpoint Create(MethodBase pointcutMethod, MethodBase concernMethod)
    {
        return new Joinpoint (pointcutMethod, concernMethod);
    }
}

没什么特别的……它也应该实现 IEquatable,但为了使代码更短,我在这里故意去掉了它

以及注册表:该类名为 AOP 并实现了单例模式。它通过一个名为 Registry 的公共静态属性公开其唯一实例。

public class AOP : List<Joinpoint>
{
    static readonly AOP _registry;
    static AOP() { _registry = new AOP(); }
    private AOP() { }
    public static AOP Registry { get { return _registry; } }

    [MethodImpl(MethodImplOptions.Synchronized)]
    public void Join(MethodBase pointcutMethod, MethodBase concernMethod)
    {
        var joinPoint = Joinpoint.Create(pointcutMethod, concernMethod);
        if (!this.Contains(joinPoint)) this.Add(joinPoint);
    }
}

有了 AOP 类,我们现在可以编写类似这样的构造

AOP.Registry.Join(pointcut, concernMethod);

休斯顿,我们有麻烦了

我们现在有一个明显而严重的问题需要解决。如果一个开发人员写出这样的代码……

var customer = new Customer {Name="test"};
customer.DoYourOwnBusiness();

...我们的注册表根本就没有被查阅的理由,所以我们的 LoggingConcern.DoSomething() 方法也就无法执行了……

我们的问题是 .Net 没有为我们提供一种开箱即用的简单方法来拦截此类调用。

既然没有原生方法,就必须实现一些变通方法。你的变通方法的能力将决定你的 AOP 实现的能力。
本文的目的不是讨论所有可能的拦截技术,但请注意,拦截模型是所有 AOP 实现之间的关键区别
SharpCrafters 网站(PostSharp 的所有者)提供了关于两种主要技术的清晰信息

我们的拦截技术:代理

如果你想拦截对一个类的所有调用,其实并没有什么秘密,你有三种选择

  1. 创建您自己的语言和编译器来生成 .net 程序集:在编译时,您可以在任何地方注入您想要的任何内容。
  2. 实现一个修改程序集运行时行为的解决方案
  3. 向您的消费者提供一个代理,并使用拦截器类拦截调用,同时编组真实主体(目标

对于高级用户:我故意不提及 Debugger API 和 Profiler API 的可能性,它们不适用于生产场景。
对于非常高级的用户:使用 Roslyn API 的解决方案 1 和 2 的混合实现应该是可行的,据我所知,它仍有待发明。有识之士……

除非您需要在每一行代码上提供切入点,否则前两种解决方案似乎有点过度设计。
我们将选择第三种解决方案。请注意,代理技术的使用伴随着好消息和坏消息

坏消息是你的目标对象必须在运行时与代理对象实例进行交换。这意味着如果你想拦截构造函数之类的东西,你将不得不将你的目标类实例的构造委托给一个工厂(这是一个这个实现无法解决的横切关注点。如果你已经有一个目标类实例,那么你将不得不明确地要求进行交换。对于 IOC 和依赖注入的高手来说,对象创建的委托将不是问题。对于其他人来说,这意味着如果他们想充分利用我们的拦截技术,他们将不得不使用一个工厂。但是别担心,我们将实现那个工厂。

好消息是,我们无需做任何事情即可实现代理。System.Runtime.Remoting.Proxies.RealProxy 类将以高度优化的方式为我们构建它。

在我看来,这个类的名称并不能反映它的用途。这个类不是代理,它是一个拦截器。但无论如何,这个类将通过调用其 GetTransparentProxy() 方法为我们提供一个代理,而这正是我们唯一需要的。

所以我们拦截器的骨架是

public class Interceptor : RealProxy, IRemotingTypeInfo
{
    object theTarget { get; set; }

    public Interceptor(object target) : base(typeof(MarshalByRefObject))
    {
        theTarget = target;
    }

    public override System.Runtime.Remoting.Messaging.IMessage Invoke(System.Runtime.Remoting.Messaging.IMessage msg)
    {
        IMethodCallMessage methodMessage = (IMethodCallMessage) msg;
        MethodBase method = methodMessage.MethodBase;
        object[] arguments = methodMessage.Args;

        object returnValue = null;

        // TODO:
        // here goes the implementation details for method swapping in case the AOP.Registry 
        // has an existing joinpoint for the MethodBase which is hold in the "method" variable...
        // if the Registry has no joinpoint then simply search for the corresponding method 
        // on the "theTarget" object and simply invoke it... ;-)

        return new ReturnMessage(returnValue, methodMessage.Args, methodMessage.ArgCount, methodMessage.LogicalCallContext, methodMessage);
    }

    #region IRemotingTypeInfo
    public string TypeName { get; set; }
    public bool CanCastTo(Type fromType, object o) { return true; }
    #endregion
}

这里需要一些解释,因为我们现在正在触及实现的核心……

RealProxy 类的存在是为了拦截来自远程对象的调用并编组目标对象。这里的远程必须理解为真正的远程,比如:生活在另一个应用程序、另一个 AppDomain、另一个服务器等等中的对象。我不会过多地深入细节,但在 .net Remoting 基础设施中有两种编组对象的方式:按引用或按值。基本上,这意味着只有当远程对象继承 MarshalByRef 或实现 ISerializable 时,您才能编组它们。我们的计划根本不使用 remoting 功能,但我们仍然需要让 RealProxy 类认为我们的目标对于 remoting 是可接受的。这就是为什么我们向 RealProxy 基构造函数传递 typeof(MarshalByRef)

RealProxy 类通过 System.Runtime.Remoting.Messaging.IMessage Invoke(System.Runtime.Remoting.Messaging.IMessage msg) 方法接收所有对透明代理的调用。我们将在那里实现方法交换的细节。请阅读上面代码中的注释。

关于 IRemotingTypeInfo 的实现:在真正的远程处理环境中,客户端会向服务器请求一个对象。客户端应用程序运行时可能对已编组的远程对象的类型一无所知。因此,当客户端应用程序调用 public object GetTransparentProxy() 方法时,运行时必须决定返回的对象(透明代理)是否可以装箱到客户端应用程序预期的契约中。通过实现 IRemotingTypeInfo,您可以向客户端运行时提供提示,告知是否允许转换为指定类型。
你猜怎么着,诀窍就在这里,就在你惊讶的目光前,就在这里……

public bool CanCastTo(Type fromType, object o) { return true; }    

我们所有的 AOP 实现都只能通过远程处理提供的可能性来实现这两个词:return true; 过了这一点,我们就可以将 GetTransparentProxy() 返回的对象转换为任何接口,无需任何运行时检查!!!

运行时只是纯粹简单地给了我们一张可以玩的“是”牌!

我们可能希望重新审视这段代码,以返回更适合任何类型的东西,而不是 true……但我们也可以想象利用这种行为来提供一个缺失方法实现或一个捕获所有接口……这里有很多发挥你创造力的空间!

此时,我们为目标实例提供了一个像样的拦截机制。我们仍然缺少对构造函数的拦截和透明代理的创建。这是工厂的工作……

工厂

关于这个,没什么好说的。这是类的骨架。

public static class Factory
{
    public static object Create<T>(params object[] constructorArgs)
    {
        T target;

        // TODO:
        // Base on typeof(T) and the list of constructorArgs (count and their Type)
        // we can ask our Registry if it has a constructor method joinpoint and invoke it
        // if the Registry has no constructor joinpoint then simply search for the corresponding one 
        // on typeof(T) and invoke it...
            

        // Assign the result of construction to the "target" variable
        // and pass it to the GetProxy method.
        return GetProxyFor<T>(target);
    }

    public static object GetProxyFor<T>(object target = null)
    {
        // Here we are asked to intercept calls on an existing object instance (Maybe we constructed it but not necessarily)
        // Simply create the interceptor and return the transparent proxy
        return new Interceptor(target).GetTransparentProxy();
    }
}
        
        

请注意,Factory 类总是返回一个 object 类型的对象。我们不能返回 T 类型的对象,因为透明代理根本不是 T 类型,它是 System.Runtime.Remoting.Proxies.__TransparentProxy 类型。但是,请记住“通行证”,我们可以将返回的对象转换为任何接口,而无需任何运行时检查!

我们会将 Factory 类嵌套在 AOP 类中,希望能为我们的消费者提供简洁的编程体验。但您会在下面的“使用”部分看到。

实现最终说明

如果你已经读到这里,我必须承认你几乎是一个英雄!太棒了!赞!

为了本文的简洁和清晰(该死……你为什么笑?),我不会讨论方法检索和切换的无聊实现细节。实际上,其中并没有太多乐趣。但如果你对此感兴趣,可以下载代码并浏览:它是完全可用的!。类和方法签名可能略有不同,因为我边写边编码,但预计不会有重大变化。

警告在决定在您的项目中使用此代码之前,请仔细阅读倒数第二部分。如果您不知道“paenultimus”这个词,那么我想您得先点击链接!

用法

我写了很多,但还没有给您一个关于我们如何实际使用所有这些的恰当提示。现在,我们终于来到了:真相时刻!

下载后得到什么

附件的 zip 文件包含一个项目,其中有 5 个示例用于演示。因此,您将获得通过以下方式注入方面的示例:

  • 拦截构造函数
  • 拦截方法和属性
  • 拦截事件
  • 拦截类型初始化
  • 拦截 File.ReadAllText(string path)

现在我将展示这五个例子中的两个:最明显和最不明显的。

拦截方法和属性

首先我们需要一个领域模型……没什么特别的

            
public interface IActor
{
    string Name { get; set; }
    void Act();
}

public class Actor : IActor
{
    public string Name { get; set; }

    public void Act()
    {
        Console.WriteLine("My name is '{0}'. I am such a good actor!", Name);
    }
}

    

然后我们需要一个关注点

public class TheConcern : IConcern<Actor>
{
    public Actor This { get; set; }

    public string Name 
    {
        set
        {
            This.Name = value + ". Hi, " + value + " you've been hacked";
        }
    }

    public void Act()
    {
        This.Act();
        Console.WriteLine("You think so...!");
    }
}
    

在应用程序初始化时,我们通过注册表告知我们的连接点

// Weave the Name property setter
AOP.Registry.Join
    (
        typeof(Actor).GetProperty("Name").GetSetMethod(),
        typeof(TheConcern).GetProperty("Name").GetSetMethod()
    );

// Weave the Act method
AOP.Registry.Join
    (
        typeof(Actor).GetMethod("Act"),
        typeof(TheConcern).GetMethod("Act")
    );    
    
    

最后,我们通过工厂创建一个对象

var actor1 = (IActor) AOP.Factory.Create<Actor>();
actor1.Name = "the Dude";
actor1.Act();    

请注意,我们请求创建了一个 Actor 类,但我们可以将结果转换为接口,所以让我们使用 IActor,因为该类实现了它。

如果您在控制台应用程序中运行它,输出将是以下内容

    
            
My name is 'the Dude. Hi, the Dude you've been hacked'. I am such a good actor!
You think so...!    

拦截 File.ReadAllText(string path)

这里我们有2个问题

  1. File 类是静态的
  2. 并且不实现任何接口

这就是我们受益于“通行证”的地方!还记得吗?返回的代理和接口之间没有运行时类型检查。
这意味着我们可以创建任何类型的接口……反正没人需要实现它:无论是目标还是关注点。基本上,我们只是将接口用作契约……

让我们通过创建一个模拟静态 File 类的假接口来演示这一点

public interface IFile
{
    string[] ReadAllLines(string path);
}

我们的关注点

public class TheConcern
{
    public static string[] ReadAllLines(string path)
    {
        return File.ReadAllLines(path).Select(x => x + " hacked...").ToArray();
    }
}

连接点的注册

AOP.Registry.Join
    (
        typeof(File).GetMethods().Where(x => x.Name == "ReadAllLines" && x.GetParameters().Count() == 1).First(),
        typeof(TheConcern).GetMethod("ReadAllLines")
    );

最后是程序的执行

var path = Path.Combine(Environment.CurrentDirectory, "Examples", "data.txt");
var file = (IFile) AOP.Factory.Create(typeof(File));
foreach (string s in file.ReadAllLines(path)) Console.WriteLine(s);

在这种情况下,请注意我们不能使用 Factory.Create 方法,因为静态类型不能用作泛型参数。

参考文献

不分先后顺序

接下来做什么? 

到目前为止,我们已经能够实现 AOP 的主要目标:实现一个方面并注册它以执行。TinyAOP 诞生了。但你在 AOP 领域的旅程还没有结束,你可能想深入挖掘:

  • 原因 #1:谁愿意像我们现在这样注册连接点?我肯定不愿意!通过一点自省,我们可以让事情变得更加实用,并构建一个看起来像真正的 AOP 库。AOP 的目的是为了简化你的生活,而不是增加更多的痛苦。
  • 原因 #2:混入组合等问题完全未被涵盖,我们可以在那里期待大量的好处。
  • 原因 #3:我们需要性能和稳定性。现在代码只是一个概念验证。当我们能让它非常快的时候,它却太慢了。一点错误检查也不会是坏事。
  • 原因 #4:我们正在拦截几乎所有类型的类,但接口拦截呢??
  • 原因 #5:我们真的需要更多理由吗?

结论:我们有一个精巧的小型原型,它证明了纯粹使用托管的、非动态的代码,无需织入等方式实现 AOP 的技术可行性。

你已经学会了 AOP:你可以从这里开始,自己实现一个版本!

最后几句话,题外话……

关于法语表达 

我是法国人,这已经不是秘密了……没有人是完美的!在写这篇文章的时候,我在谷歌上搜索“A boire, ou je tue le chien!”这个表达的解释,我找到了一个名为“你在学校学不到的法语表达”的页面。我敢肯定你会觉得其中一些表达很有趣,所以我分享链接:http://www.globule.org/~gpierre/french.php

声明

CodeProject 这样的网站之所以能运作,是因为有一些人在撰写和发表文章。无论这些人这样做的原因是什么,他们都在做!这需要大量的时间和精力

不要忽视时间和工作:
如果您不喜欢这篇文章,请不要在没有进一步解释的情况下给出一分评价……我可能写了一个错误或不准确的陈述,我的英语肯定需要重写,也许您期待更多或更少的解释,我不知道……就是这么简单:如果您不说,我不知道! 欢迎您给出有理由的差评,我不会受伤(好吧,也许会一点点),它会让我重新审视我的判断,对文章进行必要的调整或更正,并提高自己以便将来写出更好的文章。

如果您喜欢这篇文章,或者正在使用这些代码,或者今天学到了一些东西,那么也请告诉我:留下评论,给我投票(投5分),给我发电子邮件,在领英上联系……任何形式的反馈都非常感谢!!!

感谢阅读!

© . All rights reserved.