使用AOP 使C#代码更清晰

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介:

简介

如果你很熟悉面向方面编程(AOP),你就会知道给代码增加“切面”可以使代码更清晰并且具有可维护性。但是AOP通常都依赖于第三方类库或者硬编码的.net特性来工作。虽然这些实现方式的好处大于它们的复杂程度,但是我仍然在寻找一种实现AOP的更为简单的方式,来试我的代码更为清晰。我将它们单独移出来,并命名为AspectF。

Aspect Oriented Programming (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实例的一个方法插入了一个Customer。但去实现这样的业务逻辑,你真的很难去照顾所有的细节(日志记录、重试、异常处理、操作计时)。项目越成熟,在你的代码中需要维护的这些“边边角角”就更多了。所以你肯定经常会到处拷贝这些“样板”代码,但只在这些样板内写少了真是的东西。这多不值!你不得不对每个业务逻辑层的方法都这么做。比如现在你想在你的业务逻辑层中增加一个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);
}

这里你需要区分这些通用的东西,像日志记录、计时、重试、验证等这些通常被称为“边边角角”的东西,最重要的是完全与你的“真实”代码无关。这可以使方法将会变得美观而清晰。所有的这些细节都在方法外被处理,并且只是在代码外加上了一些属性。这里,每一个属性代表一个Aspect(切面)。例如,你可以增加“日志记录”切面到任何代码中,只需要增加一个Log属性。无论你使用何种AOP的类库,该类库都能够确保这些“切面”被有效地加入到代码中,当然时机不一,可能是在编译时,也可能是在运行时。

有许多AOP类库通过使用编译事件和IL操作允许你在编译时“处理”这些方面,例如PostSharp;而某些类库使用DynamicProxy在运行时处理;某些要求你的类继承自ContextBoundObject使用C#内建特性来supportAspects。所有的这些都有某些“不便”。你不得不使用某些外部库,做足够的性能测试来那些类库可扩展等等。而你需要的只是一个非常简单的方式来实现“隔离”,可能并不是想要完全实现AOP。记住,你的目的是隔离那些并不重要的核心代码,来让一切变得简单并且清晰!

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类库有何不同:

(1) 不在方法的外面定义“切面”,而是在方法的内部直接定义。

(2) 取代将“切面”做成类,而是将其构建成方法

现在,看看它有什么优势:

(1) 没有很“深奥”的要求(Attributes, ContextBoundObject, Post build event, IL Manipulation,DynamicProxy)

(2) 没有对其他依赖的性能担忧

(3) 直接随意组合你要的“切面”。例如,你可以只对日志记录一次,但尝试很多次操作。

(4) 你可以传递参数,局部变量等到“切面”中,而你在使用第三方类库的时候,通常不能这么做

(5) 这不是一个完整的框架或类库,而仅仅是一个叫做AspectF的类

(6) 可能以在代码的任何地方定义方面,例如你可以将一个for 循环包裹成一个“切面”

让我们看看使用这种方案构建一个“切面”有多简单!这个方案中“切面”都是以方法来定义的。AspectExtensions类包含了所有的这些“预构建”的切面,比如:Log、Retry、TrapLog、TrapLogThrow等。例如,这里展示一下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 Service的调用,因为它们经常由于各种基础设施问题而失败,并且有时重试一次就可以解决问题。我有个习惯是总是去尝试数据库插入,更新,删除、web service调用,处理文件等等。而这样的“切面”无疑让我对处理这样的问题时轻松了许多。

下面展示了一下它是如何工作的,它创建了一个代理的组合。而结果就像如下这段代码:

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);
    });
}

你调用AspectF的Combine方法来压缩一个将要被放进委托链的委托。委托链在最后将会被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类还有其他几个方便的“切面”,大致如下(当然你完全可以DIY你自己的‘切面’)。

public static class AspectExtensions
    {
        [DebuggerStepThrough]
        public static void DoNothing()
        {

        }

        [DebuggerStepThrough]
        public static void DoNothing(params object[] whatever)
        {

        }

        [DebuggerStepThrough]
        public static AspectF Delay(this AspectF aspect, int milliseconds)
        {
            return aspect.Combine((work) =>
            {
                System.Threading.Thread.Sleep(milliseconds);
                work();
            });
        }

        [DebuggerStepThrough]
        public static AspectF MustBeNonNull(this AspectF aspect, params object[] args)
        {
            return aspect.Combine((work) =>
            {
                for (int i = 0; i < args.Length; i++)
                {
                    object arg = args[i];
                    if (arg == null)
                    {
                        throw new ArgumentException(string.Format("Parameter at index {0} is null", i));
                    }
                }
                work();
            });
        }

        [DebuggerStepThrough]
        public static AspectF MustBeNonDefault<T>(this AspectF aspect, params T[] args) where T : IComparable
        {
            return aspect.Combine((work) =>
            {
                T defaultvalue = default(T);
                for (int i = 0; i < args.Length; i++)
                {
                    T arg = args[i];
                    if (arg == null || arg.Equals(defaultvalue))
                    {
                        throw new ArgumentException(string.Format("Parameter at index {0} is null", i));
                    }
                }
                work();
            });
        }

        [DebuggerStepThrough]
        public static AspectF WhenTrue(this AspectF aspect, params Func<bool>[] conditions)
        {
            return aspect.Combine((work) =>
            {
                foreach (Func<bool> condition in conditions)
                {
                    if (!condition())
                    {
                        return;
                    }
                }
                work();
            });
        }

        [DebuggerStepThrough]
        public static AspectF RunAsync(this AspectF aspect, Action completeCallback)
        {
            return aspect.Combine((work) => work.BeginInvoke(asyncresult =>
            {
                work.EndInvoke(asyncresult); completeCallback();
            }, null));
        }

        [DebuggerStepThrough]
        public static AspectF RunAsync(this AspectF aspect)
        {
            return aspect.Combine((work) => work.BeginInvoke(asyncresult =>
            {
                work.EndInvoke(asyncresult);
            }, null));
        }
    }

现在,你已经拥有了一个简洁的方式来隔离那些细枝末节的代码,去享受AOP形式的编程而无需使用任何“笨重”的框架。



原文发布时间为:2011-10-30


本文作者:vinoYang


本文来自云栖社区合作伙伴CSDN博客,了解相关信息可以关注CSDN博客。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
相关文章
|
3月前
|
缓存 C# Windows
C#程序如何编译成Native代码
【10月更文挑战第15天】在C#中,可以通过.NET Native和第三方工具(如Ngen.exe)将程序编译成Native代码,以提升性能和启动速度。.NET Native适用于UWP应用,而Ngen.exe则通过预编译托管程序集为本地机器代码来加速启动。不过,这些方法也可能增加编译时间和部署复杂度。
183 2
|
5月前
|
C# 开发者 Windows
在VB.NET项目中使用C#编写的代码
在VB.NET项目中使用C#编写的代码
65 0
|
3月前
|
C#
C# 图形验证码实现登录校验代码
C# 图形验证码实现登录校验代码
113 2
|
3月前
|
中间件 数据库连接 API
C#数据分表核心代码
C#数据分表核心代码
49 0
|
5月前
|
XML Java 数据格式
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
这篇文章是Spring5框架的AOP切面编程教程,通过XML配置方式,详细讲解了如何创建被增强类和增强类,如何在Spring配置文件中定义切入点和切面,以及如何将增强逻辑应用到具体方法上。文章通过具体的代码示例和测试结果,展示了使用XML配置实现AOP的过程,并强调了虽然注解开发更为便捷,但掌握XML配置也是非常重要的。
Spring5入门到实战------11、使用XML方式实现AOP切面编程。具体代码+讲解
|
5月前
|
物联网 C# Windows
看看如何使用 C# 代码让 MQTT 进行完美通信
看看如何使用 C# 代码让 MQTT 进行完美通信
684 0
|
5月前
|
Java Spring XML
掌握面向切面编程的秘密武器:Spring AOP 让你的代码优雅转身,横切关注点再也不是难题!
【8月更文挑战第31天】面向切面编程(AOP)通过切面封装横切关注点,如日志记录、事务管理等,使业务逻辑更清晰。Spring AOP提供强大工具,无需在业务代码中硬编码这些功能。本文将深入探讨Spring AOP的概念、工作原理及实际应用,展示如何通过基于注解的配置创建切面,优化代码结构并提高可维护性。通过示例说明如何定义切面类、通知方法及其应用时机,实现方法调用前后的日志记录,展示AOP在分离关注点和添加新功能方面的优势。
74 0
|
5月前
|
数据安全/隐私保护 C# UED
利用 Xamarin 开展企业级移动应用开发:从用户登录到客户管理,全面演示C#与Xamarin.Forms构建跨平台CRM应用的实战技巧与代码示例
【8月更文挑战第31天】利用 Xamarin 进行企业级移动应用开发能显著提升效率并确保高质量和高性能。Xamarin 的跨平台特性使得开发者可以通过单一的 C# 代码库构建 iOS、Android 和 Windows 应用,帮助企业快速推出产品并保持一致的用户体验。本文通过一个简单的 CRM 示例应用演示 Xamarin 的使用方法,并提供了具体的代码示例。该应用包括用户登录、客户列表显示和添加新客户等功能。此外,还介绍了如何增强应用的安全性、数据持久化、性能优化及可扩展性,从而构建出功能全面且体验良好的移动应用。
61 0
|
5月前
|
前端开发 开发者 Apache
揭秘Apache Wicket项目结构:如何打造Web应用的钢铁长城,告别混乱代码!
【8月更文挑战第31天】Apache Wicket凭借其组件化设计深受Java Web开发者青睐。本文详细解析了Wicket项目结构,帮助你构建可维护的大型Web应用。通过示例展示了如何使用Maven管理依赖,并组织页面、组件及业务逻辑,确保代码清晰易懂。Wicket提供的页面继承、组件重用等功能进一步增强了项目的可维护性和扩展性。掌握这些技巧,能够显著提升开发效率,构建更稳定的Web应用。
125 0
|
5月前
|
前端开发 程序员 API
从后端到前端的无缝切换:一名C#程序员如何借助Blazor技术实现全栈开发的梦想——深入解析Blazor框架下的Web应用构建之旅,附带实战代码示例与项目配置技巧揭露
【8月更文挑战第31天】本文通过详细步骤和代码示例,介绍了如何利用 Blazor 构建全栈 Web 应用。从创建新的 Blazor WebAssembly 项目开始,逐步演示了前后端分离的服务架构设计,包括 REST API 的设置及 Blazor 组件的数据展示。通过整合前后端逻辑,C# 开发者能够在统一环境中实现高效且一致的全栈开发。Blazor 的引入不仅简化了 Web 应用开发流程,还为习惯于后端开发的程序员提供了进入前端世界的桥梁。
537 0