使用AOP 使C#代码更清晰

Posted 在西天取经的路上……

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用AOP 使C#代码更清晰相关的知识,希望对你有一定的参考价值。

简介

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

 

Aspect Oriented Programming (AOP)的背景

“切面”指的是那些在你写的代码中在项目的不同部分且有相同共性的东西。它可能是你代码中处理异常、记录方法调用、时间处理、重新执行一些方法等等的一些特殊方式。如果你没有使用任何面向切面编程的类库来做这些事情,那么在你的整个项目中将会遗留一些很简单而又重复的代码,它将使你的代码很难维护。例如,在你的业务逻辑层有些方法需要被记录,有些异常需要被处理,有些执行需要计时,数据库操作需要重试等等。所以,也许你会写出下面这样的代码。

 

[csharp] view plain copy
 
 print?
  1. public bool InsertCustomer(string firstName, string lastName, int age,   
  2.     Dictionary<string, string> attributes)  
  3. {  
  4.     if (string.IsNullOrEmpty(firstName))   
  5.         throw new ApplicationException("first name cannot be empty");  
  6.     if (string.IsNullOrEmpty(lastName))  
  7.         throw new ApplicationException("last name cannot be empty");  
  8.     if (age < 0)  
  9.         throw new ApplicationException("Age must be non-zero");  
  10.     if (null == attributes)  
  11.         throw new ApplicationException("Attributes must not be null");  
  12.       
  13.     // Log customer inserts and time the execution  
  14.     Logger.Writer.WriteLine("Inserting customer data...");  
  15.     DateTime start = DateTime.Now;  
  16.       
  17.     try  
  18.     {  
  19.         CustomerData data = new CustomerData();  
  20.         bool result = data.Insert(firstName, lastName, age, attributes);  
  21.         if (result == true)  
  22.         {  
  23.             Logger.Writer.Write("Successfully inserted customer data in "   
  24.                 + (DateTime.Now-start).TotalSeconds + " seconds");  
  25.         }  
  26.         return result;  
  27.     }  
  28.     catch (Exception x)  
  29.     {  
  30.         // Try once more, may be it was a network blip or some temporary downtime  
  31.         try  
  32.         {  
  33.             CustomerData data = new CustomerData();  
  34.             if (result == true)  
  35.             {  
  36.                 Logger.Writer.Write("Successfully inserted customer data in "   
  37.                     + (DateTime.Now-start).TotalSeconds + " seconds");  
  38.             }  
  39.             return result;  
  40.         }  
  41.         catch   
  42.         {  
  43.             // Failed on retry, safe to assume permanent failure.  
  44.             // Log the exceptions produced  
  45.             Exception current = x;  
  46.             int indent = 0;  
  47.             while (current != null)  
  48.             {  
  49.                 string message = new string(Enumerable.Repeat(‘\t‘, indent).ToArray())  
  50.                     + current.Message;  
  51.                 Debug.WriteLine(message);  
  52.                 Logger.Writer.WriteLine(message);  
  53.                 current = current.InnerException;  
  54.                 indent++;  
  55.             }  
  56.             Debug.WriteLine(x.StackTrace);  
  57.             Logger.Writer.WriteLine(x.StackTrace);  
  58.             return false;  
  59.         }  
  60.     }  
  61. }   

 

 

你会看到上面只有两行关键代码,它调用了CustomerData实例的一个方法插入了一个Customer。但去实现这样的业务逻辑,你真的很难去照顾所有的细节(日志记录、重试、异常处理、操作计时)。项目越成熟,在你的代码中需要维护的这些“边边角角”就更多了。所以你肯定经常会到处拷贝这些“样板”代码,但只在这些样板内写少了真是的东西。这多不值!你不得不对每个业务逻辑层的方法都这么做。比如现在你想在你的业务逻辑层中增加一个UpdateCustomer方法。你不得不再次拷贝所有的这些“样板”,然后将两行关键代码加入其中。

思考这样的场景,你需要做出一个项目级别的改变——针对如何处理异常。你不得不处理你写的这“上百”的方法,然后一个一个地修改它们。如果你想修改计时的逻辑,做法同样如此。

面向切面编程就可以很好地处理这些问题。当你采用AOP,你会以一种很酷的方式来实现它:

[csharp] view plain copy
 
 print?
  1. [EnsureNonNullParameters]  
  2. [Log]  
  3. [TimeExecution]  
  4. [RetryOnceOnFailure]  
  5. public void InsertCustomerTheCoolway(string firstName, string lastName, int age,  
  6.     Dictionary<string, string> attributes)  
  7. {  
  8.     CustomerData data = new CustomerData();  
  9.     data.Insert(firstName, lastName, age, attributes);  
  10. }  

 

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

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

AspectF如何来让这一切变得简单!

让我展示一种简答的方式来实现这种隔离,仅仅使用标准的C#代码,类和代理的简单调用,没有用到“特性”或者“IL操作”这些东西。它提供了可重用性和可维护性。最好的一点是它的“轻量级”——仅仅一个很小得类。

[csharp] view plain copy
 
 print?
  1. public void InsertCustomerTheEasyWay(string firstName, string lastName, int age,  
  2.     Dictionary<string, string> attributes)  
  3. {  
  4.     AspectF.Define  
  5.         .Log(Logger.Writer, "Inserting customer the easy way")  
  6.         .HowLong(Logger.Writer, "Starting customer insert",   
  7.         "Inserted customer in {1} seconds")  
  8.         .Retry()  
  9.         .Do(() =>  
  10.             {  
  11.                 CustomerData data = new CustomerData();  
  12.                 data.Insert(firstName, lastName, age, attributes);  
  13.             });  
  14. }  

 

让我们看看它与通常的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是如何工作的:

[csharp] view plain copy
 
 print?
  1. [DebuggerStepThrough]  
  2. public static AspectF Retry(this AspectF aspects)  
  3. {  
  4.     return aspects.Combine((work) =>   
  5.         Retry(1000, 1, (error) => DoNothing(error), DoNothing, work));  
  6. }  
  7.   
  8. [DebuggerStepThrough]  
  9. public static void Retry(int retryDuration, int retryCount,   
  10.     Action<Exception> errorHandler, Action retryFailed, Action work)  
  11. {  
  12.     do  
  13.     {  
  14.         try  
  15.         {  
  16.             work();  
  17.         }  
  18.         catch (Exception x)  
  19.         {  
  20.             errorHandler(x);  
  21.             System.Threading.Thread.Sleep(retryDuration);  
  22.         }  
  23.     } while (retryCount-- > 0);  
  24.     retryFailed();  
  25. }  

 

你可以让“切面”调用你的代码任意多次。很容易在Retry切面中包裹对数据库、文件IO、网络IO、Web Service的调用,因为它们经常由于各种基础设施问题而失败,并且有时重试一次就可以解决问题。我有个习惯是总是去尝试数据库插入,更新,删除、web service调用,处理文件等等。而这样的“切面”无疑让我对处理这样的问题时轻松了许多。

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

[csharp] view plain copy
 
 print?
  1. Log(() =>  
  2. {  
  3.     HowLong(() =>  
  4.     {  
  5.         Retry(() =>  
  6.         {  
  7.             Do(() =>  
  8.             {  
  9.                 CustomerData data = new CustomerData();  
  10.                 data.Insert(firstName, lastName, age, attributes);  
  11.             });  
  12.         });  
  13.     });  
  14. });  

 

AspectF类除了压缩这样的代码之外,其他什么都没有。

下面展示,你怎样创建你自己的“切面”。首先为AspectF类创建一个扩展方法。比如说,我们创建一个Log:

[csharp] view plain copy
 
 print?
  1. [DebuggerStepThrough]  
  2. public static AspectF Log(this AspectF aspect, TextWriter logWriter,   
  3.             string beforeMessage, string afterMessage)  
  4. {  
  5.     return aspect.Combine((work) =>  
  6.     {  
  7.         logWriter.Write(DateTime.Now.ToUniversalTime().ToString());  
  8.         logWriter.Write(‘\t‘);  
  9.         logWriter.Write(beforeMessage);  
  10.         logWriter.Write(Environment.NewLine);  
  11.   
  12.         work();  
  13.   
  14.         logWriter.Write(DateTime.Now.ToUniversalTime().ToString());  
  15.         logWriter.Write(‘\t‘);  
  16.         logWriter.Write(afterMessage);  
  17.         logWriter.Write(Environment.NewLine);  
  18.     });  
  19. }  

 

你调用AspectF的Combine方法来压缩一个将要被放进委托链的委托。委托链在最后将会被Do方法调用。

[csharp] view plain copy
 
 print?
  1. public class AspectF  
  2. {  
  3.     /// <summary>  
  4.     /// Chain of aspects to invoke  
  5.     /// </summary>  
  6.     public Action<Action> Chain = null;  
  7.     /// <summary>  
  8.     /// Create a composition of function e.g. f(g(x))  
  9.     /// </summary>  
  10.     /// <param name="newAspectDelegate">A delegate that offers an aspect‘s behavior.   
  11.     /// It‘s added into the aspect chain</param>  
  12.     /// <returns></returns>  
  13.     [DebuggerStepThrough]  
  14.     public AspectF Combine(Action<Action> newAspectDelegate)  
  15.     {  
  16.         if (this.Chain == null)  
  17.         {  
  18.             this.Chain = newAspectDelegate;  
  19.         }  
  20.         else  
  21.         {  
  22.             Action<Action> existingChain = this.Chain;  
  23.             Action<Action> callAnother = (work) =>   
  24.                 existingChain(() => newAspectDelegate(work));  
  25.             this.Chain = callAnother;  
  26.         }  
  27.         return this;  
  28.     }  

 

这里Combine方法操作的是被“切面”扩展方法传递过来的委托,例如Log,然后它将该委托压入之前加入的一个“切面”的委托中,来保证第一个切面调用第二个,第二个调用第三个,知道最后一个调用真实的(你想要真正执行的)代码。

Do/Return方法做最后的执行操作。

[csharp] view plain copy
 
 print?
  1. /// <summary>  
  2. /// Execute your real code applying the aspects over it  
  3. /// </summary>  
  4. /// <param name="work">The actual code that needs to be run</param>  
  5. [DebuggerStepThrough]  
  6. public void Do(Action work)  
  7. {  
  8.     if (this.Chain == null)  
  9.     {  
  10.         work();  
  11.     }  
  12.     else  
  13.     {  
  14.         this.Chain(work);  
  15.     }  
  16. }  

 

就是这些,现在你有一个非常简单的方式来分隔那些你不想过度关注的代码,并使用C#享受AOP风格的编程模式。

AspectF类还有其他几个方便的“切面”,大致如下(当然你完全可以DIY你自己的‘切面’)。

 

[csharp] view plain copy
 
 print?
  1. public static class AspectExtensions  
  2.     {  
  3.         [DebuggerStepThrough]  
  4.         public static void DoNothing()  
  5.         {  
  6.   
  7.         }  
  8.   
  9.         [DebuggerStepThrough]  
  10.         public static void DoNothing(params object[] whatever)  
  11.         {  
  12.   
  13.         }  
  14.   
  15.         [DebuggerStepThrough]  
  16.         public static AspectF Delay(this AspectF aspect, int milliseconds)  
  17.         {  
  18.             return aspect.Combine((work) =>  
  19.             {  
  20.                 System.Threading.Thread.Sleep(milliseconds);  
  21.                 work();  
  22.             });  
  23.         }  
  24.   
  25.         [DebuggerStepThrough]  
  26.         public static AspectF MustBeNonNull(this AspectF aspect, params object[] args)  
  27.         {  
  28.             return aspect.Combine((work) =>  
  29.             {  
  30.                 for (int i = 0; i < args.Length; i++)  
  31.                 {  
  32.                     object arg = args[i];  
  33.                     if (arg == null)  
  34.                     {  
  35.                         throw new ArgumentException(string.Format("Parameter at index {0} is null", i));  
  36.                     }  
  37.                 }  
  38.                 work();  
  39.             });  
  40.         }  
  41.   
  42.         [DebuggerStepThrough]  
  43.         public static AspectF MustBeNonDefault<T>(this AspectF aspect, params T[] args) where T : IComparable  
  44.         {  
  45.             return aspect.Combine((work) =>  
  46.             {  
  47.                 T defaultvalue = default(T);  
  48.                 for (int i = 0; i < args.Length; i++)  
  49.                 {  
  50.                     T arg = args[i];  
  51.                     if (arg == null || arg.Equals(defaultvalue))  
  52.                     {  
  53.                         throw new ArgumentException(string.Format("Parameter at index {0} is null", i));  
  54.                     }  
  55.                 }  
  56.                 work();  
  57.             });  
  58.         }  
  59.   
  60.         [DebuggerStepThrough]  
  61.         public static AspectF WhenTrue(this AspectF aspect, params Func<bool>[] conditions)  
  62.         {  
  63.             return aspect.Combine((work) =>  
  64.             {  
  65.                 foreach (Func<bool> condition in conditions)  
  66.                 {  
  67.                     if (!condition())  
  68.                     {  
  69.                         return;  
  70.                     }  
  71.                 }  
  72.                 work();  
  73.             });  
  74.         }  
  75.   
  76.         [DebuggerStepThrough]  
  77.         public static AspectF RunAsync(this AspectF aspect, Action completeCallback)  
  78.         {  
  79.             return aspect.Combine((work) => work.BeginInvoke(asyncresult =>  
  80.             {  
  81.                 work.EndInvoke(asyncresult); completeCallback();  
  82.             }, null));  
  83.         }  
  84.   
  85.         [DebuggerStepThrough]  
  86.         public static AspectF RunAsync(this AspectF aspect)  
  87.         {  
  88.             return aspect.Combine((work) => work.BeginInvoke(asyncresult =>  
  89.             {  
  90.                 work.EndInvoke(asyncresult);  
  91.             }, null));  
  92.         }  
  93.     }  

 

 

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

 

源码下载

以上是关于使用AOP 使C#代码更清晰的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 Nadio c# 使声音更大?

C#知识点 枚举

C#中使用面向切面编程(AOP)中实践代码整洁

编写高质量c#代码的10个建议

C# 命令绑定

C# 与 Go 的互通代码笔记