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

AspectF 声明式添加 Aspect 以实现更清晰可维护的代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.97/5 (60投票s)

2009年9月19日

CPOL

7分钟阅读

viewsIcon

161488

AspectF 是一种实现面向切面编程风格编码的简单方法,它能让你的代码更加整洁、更易于维护。

引言

AspectF 是一种为代码添加切面的流畅而简单的方式。如果你熟悉面向切面编程(AOP),你就会知道它能让你的代码更整洁、更易于维护。但 AOP 需要第三方框架或像 IL 操作这样的硬核 .NET 功能才能实现。尽管其好处远大于复杂性,我仍在寻找一种更简单的方式来进行 AOP 风格的编码,以便保持代码的整洁和简单,将切面从核心逻辑中分离出去。这就诞生了 AspectF

代码可在 Google Code 上获取。

背景 - 面向切面编程 (AOP)

切面是你在项目中不同部分时常编写的通用功能。它可能是在代码中处理异常的某种特定方式,或是记录方法调用,或是计时方法执行,或是重试某些方法等等。如果你不使用任何面向切面编程框架来实现这些,那么你就是在整个项目中重复编写大量相似的代码,这会让你的代码难以维护。例如,假设你有一个业务层,其中的方法需要被记录日志、错误需要以特定方式处理、执行需要计时、数据库操作需要重试等等。于是,你写出了这样的代码:

public bool InsertCustomer(string firstName, string lastName, int age, 
    Dictionary<string, string> attributes)
{
    if (string.IsNullOrEmpty(firstName)) 
        throw new ApplicationException("first name cannot be empty");
    if (string.IsNullOrEmpty(lastName))
        throw new ApplicationException("last name cannot be empty");
    if (age < 0)
        throw new ApplicationException("Age must be non-zero");
    if (null == attributes)
        throw new ApplicationException("Attributes must not be null");
    
    // Log customer inserts and time the execution
    Logger.Writer.WriteLine("Inserting customer data...");
    DateTime start = DateTime.Now;
    
    try
    {
        CustomerData data = new CustomerData();
        bool result = data.Insert(firstName, lastName, age, attributes);
        if (result == true)
        {
            Logger.Writer.Write("Successfully inserted customer data in " 
                + (DateTime.Now-start).TotalSeconds + " seconds");
        }
        return result;
    }
    catch (Exception x)
    {
        // Try once more, may be it was a network blip or some temporary downtime
        try
        {
            CustomerData data = new CustomerData();
            if (result == true)
            {
                Logger.Writer.Write("Successfully inserted customer data in " 
                    + (DateTime.Now-start).TotalSeconds + " seconds");
            }
            return result;
        }
        catch 
        {
            // Failed on retry, safe to assume permanent failure.
            // Log the exceptions produced
            Exception current = x;
            int indent = 0;
            while (current != null)
            {
                string message = new string(Enumerable.Repeat('\t', indent).ToArray())
                    + current.Message;
                Debug.WriteLine(message);
                Logger.Writer.WriteLine(message);
                current = current.InnerException;
                indent++;
            }
            Debug.WriteLine(x.StackTrace);
            Logger.Writer.WriteLine(x.StackTrace);
            return false;
        }
    }
} 

在这里你可以看到,真正实现业务逻辑的两行代码——通过调用名为 CustomerData 的类来插入一个客户——几乎被所有你必须在业务层中实现的关注点(日志、重试、异常处理、计时)所淹没。如今的业务层中充斥着验证、错误处理、缓存、日志记录、计时、审计、重试、依赖解析等各种逻辑。一个项目越成熟,进入你代码库的关注点就越多。因此,你不断地复制粘贴样板代码,然后在那堆样板代码的某个角落里写下那一点点真正的业务逻辑。更糟糕的是,你必须为每个业务层方法都这样做。假设现在你想要在业务层中添加一个 UpdateCustomer 方法。你就必须再次复制所有这些关注点,然后把那两行真正的代码放在样板代码的某个地方。

想象一下这样的场景:你需要对整个项目中处理错误的方式进行修改。你必须逐一检查你编写的数百个业务层函数并进行修改。再比如,你需要改变计时执行的方式,你又得再次检查数百个函数来完成这个任务。

面向切面编程解决了这些挑战。当你使用 AOP 时,你可以用一种更酷的方式来实现:

[EnsureNonNullParameters]
[Log]
[TimeExecution]
[RetryOnceOnFailure]
public void InsertCustomerTheCoolway(string firstName, string lastName, int age,
    Dictionary<string, string> attributes)
{
    CustomerData data = new CustomerData();
    data.Insert(firstName, lastName, age, attributes);
}

在这里,你已经将像日志、计时、重试、验证这些通用性的东西——它们被正式称为“关注点”——完全从你的实际代码中分离出来了。这个方法变得简洁明了,直奔主题。所有的关注点都已从函数代码中移除,并通过特性(Attribute)添加到函数上。这里的每个特性都代表一个切面。例如,你只需添加 Log 特性,就可以为任何函数添加日志记录切面。无论你使用哪个 AOP 框架,该框架都会确保这些切面在构建时或运行时被“织入”到代码中。

有些 AOP 框架允许你在编译时通过后期构建事件和 IL 操作来织入切面,例如 PostSharp;有些则在运行时通过 DynamicProxy 来实现;还有一些需要你的类继承自 ContextBoundObject,以便利用 C# 内置功能来支持切面。所有这些方法都有一定的入门门槛,你需要证明使用某个外部库的合理性,进行足够的性能测试以确保这些库具有良好的伸缩性等等。而你所需要的,或许只是一种非常简单的方式来实现“关注点分离”,而不一定是功能完备的面向切面编程。记住,这里的目的是分离关注点,并保持代码的优美和整洁。

AspectF 如何让这一切变得极其简单

那么,让我来展示一种简单的方法,它能实现关注点分离,使用标准的 C# 代码,没有特性或 IL 操作之类的“黑魔法”,只需简单地调用类和委托,就能以一种可重用和可维护的方式实现优雅的关注点分离。最棒的是,它非常轻量,只有一个小小的类。

 public void InsertCustomerTheEasyWay(string firstName, string lastName, int age,
    Dictionary<string, string> attributes)
{
    AspectF.Define
        .Log(Logger.Writer, "Inserting customer the easy way")
        .HowLong(Logger.Writer, "Starting customer insert", 
		"Inserted customer in {1} seconds")
        .Retry()
        .Do(() =>
            {
                CustomerData data = new CustomerData();
                data.Insert(firstName, lastName, age, attributes);
            });
}

让我们看看它与其他常见 AOP 框架的区别:

  • 你不是在方法外部定义切面,而是在方法内部立即定义它们。
  • 你不是将切面定义为类,而是将它们定义为方法。

现在来看看它的优势:

  • 不需要任何黑魔法(特性、ContextBoundObject、后期构建事件、IL 操作、DynamicProxy)。
  • 没有因黑魔法带来的性能开销。
  • 可以按你喜欢的方式精确排序切面。例如,你可以只记录一次日志但重试多次,或者通过将 Retry 调用在调用链中上移,你也可以让日志记录本身也支持重试。明白了吗?不明白的话,请继续阅读。
  • 你可以向切面传递参数、局部变量等。这是其他任何框架都无法做到的。
  • 它不是一个功能完备的框架,只是一个名为 AspectF 的简单类。
  • 你可以在代码的任何地方定义切面。例如,你可以用切面包裹一个 for 循环。

让我们看看用这种方法构建切面是多么简单。切面被定义为扩展方法。AspectExtensions 类包含所有预置的切面,如 LogRetryTrapLogTrapLogThrow 等。例如,Retry 的工作原理如下:

[DebuggerStepThrough]
public static AspectF Retry(this AspectF aspects)
{
    return aspects.Combine((work) => 
        Retry(1000, 1, (error) => DoNothing(error), DoNothing, work));
}

[DebuggerStepThrough]
public static void Retry(int retryDuration, int retryCount, 
    Action<Exception> errorHandler, Action retryFailed, Action work)
{
    do
    {
        try
        {
            work();
        }
        catch (Exception x)
        {
            errorHandler(x);
            System.Threading.Thread.Sleep(retryDuration);
        }
    } while (retryCount-- > 0);
    retryFailed();
}

这个切面会在出现异常时,按你指定的次数重复调用你的代码。用 Retry 切面来包裹数据库、文件 IO、网络 IO、Web 服务调用非常方便,因为这些操作可能会因为各种暂时的基础设施问题而失败,有时重试一次就能解决问题。我习惯于总是对数据库的插入、更新、删除操作,以及调用 Web 服务方法和处理文件时进行重试。这为我避免了许多生产环境中的问题。

它的工作原理是,它创建了一个委托(Delegate)的组合。最终结果类似于以下代码:

Log(() =>
{
    HowLong(() =>
    {
        Retry(() =>
        {
            Do(() =>
            {
                CustomerData data = new CustomerData();
                data.Insert(firstName, lastName, age, attributes);
            });
        });
    });
});

AspectF 类不过是实现这种代码组合的一种流畅方式。

下面是你如何创建自己的切面。首先,为 AspectF 类创建一个扩展方法。假设我们正在创建 Log 切面。

[DebuggerStepThrough]
public static AspectF Log(this AspectF aspect, TextWriter logWriter, 
            string beforeMessage, string afterMessage)
{
    return aspect.Combine((work) =>
    {
        logWriter.Write(DateTime.Now.ToUniversalTime().ToString());
        logWriter.Write('\t');
        logWriter.Write(beforeMessage);
        logWriter.Write(Environment.NewLine);

        work();

        logWriter.Write(DateTime.Now.ToUniversalTime().ToString());
        logWriter.Write('\t');
        logWriter.Write(afterMessage);
        logWriter.Write(Environment.NewLine);
    });
}

你调用 AspectFCombine 函数来组合一个委托,这个委托随后会被放入委托链中,当你最后调用 Do 方法时,这个链条就会被触发。

public class AspectF
{
    /// <summary>
    /// Chain of aspects to invoke
    /// </summary>
    public Action<Action> Chain = null;
    /// <summary>
    /// Create a composition of function e.g. f(g(x))
    /// </summary>
    /// <param name="newAspectDelegate">A delegate that offers an aspect's behavior. 
    /// It's added into the aspect chain</param>
    /// <returns></returns>
    [DebuggerStepThrough]
    public AspectF Combine(Action<Action> newAspectDelegate)
    {
        if (this.Chain == null)
        {
            this.Chain = newAspectDelegate;
        }
        else
        {
            Action<Action> existingChain = this.Chain;
            Action<Action> callAnother = (work) => 
                existingChain(() => newAspectDelegate(work));
            this.Chain = callAnother;
        }
        return this;
    }

在这里,Combine 函数接收由切面扩展方法(如 Log)传递的委托,然后将其包裹在先前添加的切面之内,这样第一个切面就会调用第二个,第二个调用第三个,最后一个切面则调用你希望在切面内执行的真实代码。

Do/Return 函数负责执行最终的调用。

/// <summary>
/// Execute your real code applying the aspects over it
/// </summary>
/// <param name="work">The actual code that needs to be run</param>
[DebuggerStepThrough]
public void Do(Action work)
{
    if (this.Chain == null)
    {
        work();
    }
    else
    {
        this.Chain(work);
    }
}

就是这样,你现在有了一种极其简单的方式来分离关注点,并能使用基本的 C# 语言特性来享受 AOP 风格的编程。

AspectF 类还附带了其他几个方便的切面。例如,MustBeNonNull 切面可以用来确保传入的参数不为 null。下面是一个单元测试,展示了它的用法:

[Fact]
public void TestMustBeNonNullWithValidParameters()
{
    bool result = false;
    Assert.DoesNotThrow(delegate
    {
      AspectF.Define
        .MustBeNonNull(1, DateTime.Now, string.Empty, "Hello", new object())
        .Do(delegate
        {
            result = true;
        });
    });
    Assert.True(result, "Assert.MustBeNonNull did not call the function
        although all parameters were non-null");
}

类似地,还有一个 MustBeNonDefault,它检查传入的参数值是否为默认值,例如,对于数字来说是 0。

[Fact]
public void TestMustBeNonDefaultWithInvalidParameters()
{
    bool result = false;

    Assert.Throws(typeof(ArgumentException), delegate
    {
        AspectF.Define
            .MustBeNonDefault<int>(default(int))
	   .MustBeNonDefault<DateTime>(default(DateTime))
            .MustBeNonDefault<string>(default(string), "Hello")
            .Do(() =>
            {
                result = true;
            });

        Assert.True(result, "Assert.MustBeNonDefault must 
		not call the function when there's a null parameter");
    });
}

还有一个 Delay 切面,它会在一定的延迟后执行代码。这对于限制对某些 Web 服务的调用频率非常有用。

[Fact]
public void TestDelay()
{
    DateTime start = DateTime.Now;
    DateTime end = DateTime.Now;
    AspectF.Define.Delay(5000).Do(() => { end = DateTime.Now; });
    TimeSpan delay = end - start;
    Assert.InRange<double>(delay.TotalSeconds, 4.9d, 5.1d);
}

另一个有用的切面是 RunAsync,它会异步调用代码。有时你的 Web 服务方法需要立即返回,并在后台异步执行主要任务。你只需用 RunAsync 包裹方法的代码,其中的代码就会异步运行。

[Fact]
public void TestAspectAsync()
{
    bool callExecutedImmediately = false;
    bool callbackFired = false;
        AspectF.Define.RunAsync().Do(() =>
    {
        callbackFired = true;
        Assert.True(callExecutedImmediately, 
            "Aspect.RunAsync Call did not execute asynchronously");
    });
    callExecutedImmediately = true;
    // wait until the async function completes
    while (!callbackFired) Thread.Sleep(100);
    bool callCompleted = false;
    bool callReturnedImmediately = false;
    AspectF.Define.RunAsync(() => 
        Assert.True(callCompleted, 
            "Aspect.RunAsync Callback did not fire
             after the call has completed properly"))
        .Do(() =>
        {
            callCompleted = true;
            Assert.True(callReturnedImmediately,
               "Aspect.RunAsync call did not run asynchronously");
        });
    callReturnedImmediately = true;
    while (!callCompleted) Thread.Sleep(100);
}

另一个方便的切面是 TrapLog,它会捕获并记录抛出的异常,但不会让异常继续传播。如果你希望异常在被记录后再次抛出以便继续传播,你可以使用 TrapLogThrow

[Fact]
public void TestTrapLog()
{
    StringBuilder buffer = new StringBuilder();
    StringWriter writer = new StringWriter(buffer);
    Assert.DoesNotThrow(() =>
    {
        AspectF.Define.TrapLog(writer).Do(() =>
        {
            throw new ApplicationException("Parent Exception",
                new ApplicationException("Child Exception",
                    new ApplicationException("Grandchild Exception")));
        });
    });
    string logOutput = buffer.ToString();
    Assert.True(logOutput.Contains("Parent Exception"));
    Assert.True(logOutput.Contains("Child Exception"));
    Assert.True(logOutput.Contains("Grandchild Exception"));
}

所以,你现在拥有了一种优美简洁的方式来实现关注点分离,并且无需任何重量级的库就能体验到面向切面编程的乐趣!

结论

AOP 的纯粹主义者看到这里肯定要火冒三丈了——“你这个笨蛋,这根本就不是 AOP!” 嗯,AOP 的核心在于关注点分离和横切。确实,AspectF 并没有展示一种实现横切的方法,但它确实展示了一种实现关注点分离的方式。仅仅因为 AOP 展示了一种在方法边界之外编写代码的方式,并不意味着你不能在方法边界之内做同样的事情。例如,如果在 C# 中你必须在方法体内声明特性(Attribute),那么使用特性来实现 AOP 与使用 AspectF 的差别就很小了。AspectF 的主要目标是——不依赖外部库或非标准的 .NET 功能,以最小的努力创建新的切面,并利用强类型在应用切面时提供构建时检查等特性。

© . All rights reserved.