如何编写没有Try/Catch的程序

在上面一篇文章《谈谈关于MVP模式中V-P交互问题》中,我提到最近一直为一个项目进行Code Review的工作,从中发现了一些问题,同时也有了一些想法。上次谈到如何正确编写服务MVP规范的程序,这次我们来关注一个我们每天都会面对的问题:异常处理。

一、异常处理不简单

个人觉得,异常处理对于程序员来说,尤其是对于那些初级.NET程序员来说,是最为熟悉的同时也是最难掌握的。说它熟悉,因为仅仅就是Try/Catch而已。说它难以掌握,很多开发人员却说不清楚Try/Catch应该置于何处?什么情况下需要对异常进行日志记录?什么情况下需要对异常进行封装?什么情况下需要对异常进行替换?对于捕获的异常,在什么情况下需要将其再次抛出?什么情况下则不需要。总之,异常处理没有我们想象的那么简单。

无论对于何种类型的应用,异常处理都是必不可少的。合理的异常处理应该是场景驱动的,在不同的场景下,采用的异常处理策略往往是不同的。异常处理的策略应该是可配置的,因为应用程序出现怎样的异常往往是不可预测的,现有异常策略的不足往往需要在真正出现某种异常的时候才会体现出来,所以我们需要一种动态可配置的异常处理策略维护方式。目前有一些开源的异常处理框架提供了这种可配置的、场景驱动的异常处理方式,EnterLib的Exception Handling Application Block就是一个不错的选择。

二、异常处理对于最终的开发人员是透明的

“异常处理对于最终的开发人员是透明的”,可能这句话说得有点过头。但是,就我个人的项目经验来讲,这是一种理想的状态。由于异常策略是一般是通过配置动态配置的,不需要反映在代码上面。如果能够通过框架的方式提供异常处理的实现,使开发人员无需编写任何异常处理的代码,只需要关注业务流程的实现就可以了,这不仅能够提高开发的效率,也能够提高系统的可维护性。

我们目前的项目是一个典型的分布式应用,所有的业务流程的处理和数据访问都实现在服务端,最终以WCF服务的形式暴露给客户端(Smart Client)和第三方应用。所有客户端和服务端从逻辑上具有相应的层次划分,但是异常处理仅仅实现在两个地方,一个地方是WCF服务本身,另一个实现UI层。忘了说明一点,我们项目直接将EnterLib的Exception Handling Application Block作为我们的异常处理框架。对于服务端的异常处理来说,我们通过WCF与EHAB的集成来实现的(《WCF与Exception Handling AppBlock集成》),所以不需要开发人员添加任何一句Try/Catch代码。但是客户端来说,对于某个控件的事件来说,由于UI本身就是处于整个调用栈的最顶层,很难通过基于AOP的拦截机制来实现对异常处理的动态注入,所以客户端会出现非常类似于下面代码所示的Try/Catch。

   1: private void buttonCalculate_Click(object sender, EventArgs e)
   2: {
   3:     try
   4:     {
   5:         //
   6:     }
   7:     catch (Exception ex)
   8:     {
   9:         if (ExceptionPolicy.HandleException(ex, "policyName"))
  10:         {
  11:             throw;
  12:         }
  13:     }
  14:  
  15: }    
我是一个对重复代码具有强迫症的人,看到两个相同的代码我都有对代码进行重构的冲动,何况如此众多的相同代码充斥在客户端。

三、通过编写公共方法的形式实现代码的重用

为了避免开发人员编写相同的Try/Catch,很多人首先想到的肯定是将重复代码定义在一个公共的方法上,以实现代码的复用。这个公共方法很简单,只需要如下几句代码即可。
   1: public void Invoke(Action action)
   2: {
   3:     try
   4:     {
   5:         action();
   6:     }
   7:     catch (Exception ex)
   8:     {
   9:         if (ExceptionPolicy.HandleException(ex, "data access policy"))
  10:         {
  11:             throw;
  12:         }
  13:     }
  14: } 

在调用的时候,只需要将相应的操作以Action类型的Delegate的形式传入Invoke方法即可。但是这样,也会在所有控件处理事件中出现重复的Invoke调用,虽然重复的代码行数减少了,但是还是会出现大规模的重复。接下里我来介绍另一种解决方法。

四、对EventHandler进行封装

认真分析上面的需求,我们的根本目的就是让执行事件处理程序的时候在外面人为地套一个Try/Catch,并对捕获的异常进行相应的处理。从这个意义上讲,如果我们能够对EventHandler或者ExventHandler<TEventArgs>进行相应的封装,就能实现我们需要的目的。

可能我这样说,你不会太明白,我们还是通过代码来说话好了。在下面我创建了一个用于封装EventHandler对象的EventHandlerWrapper类型。我们知道EventHandler是一个Delegate,而Delegate由两部分组成:表示操作本身的MethodInfo和操作执行的目标对象,分别通过属性Method和Target表示。在执行EventHandler的时候,就是通过反射的方式调用MethodInfo的Invoke方法,并将目标对象和相应的参数传入该方法而已。

   1: using System;
   2: using System.Diagnostics;
   3: using System.Reflection;
   4: using System.Text;
   5: using System.Windows.Forms;
   6: namespace ProgramingWithoutTryCatch
   7: {
   8:     public class EventHandlerWrapper
   9:     {
  10:         public object Target
  11:         { get; private set; }
  12:  
  13:         public MethodInfo Method
  14:         { get; private set; }
  15:  
  16:         public EventHandler Hander
  17:         { get; private set; }
  18:  
  19:         public EventHandlerWrapper(EventHandler eventHandler)
  20:         {
  21:             if (null == eventHandler)
  22:             {
  23:                 throw new ArgumentNullException("eventHandler");
  24:             }
  25:  
  26:             this.Target = eventHandler.Target;
  27:             this.Method = eventHandler.Method;
  28:             this.Hander += Invoke;
  29:         }
  30:  
  31:         public static implicit operator EventHandler (EventHandlerWrapper eventHandlerWrapper)
  32:         {
  33:             return eventHandlerWrapper.Hander;
  34:         }
  35:  
  36:         private void Invoke(object sender, EventArgs args)
  37:         {
  38:             try
  39:             {
  40:                 this.Method.Invoke(this.Target, new object[] { sender, args });
  41:             }
  42:             catch (TargetInvocationException ex)
  43:             {
  44:                 StringBuilder message = new StringBuilder();
  45:                 message.AppendLine(string.Format("Message: {0}", ex.InnerException.Message));
  46:                 message.AppendLine(string.Format("Exception Type: {0}", ex.InnerException.GetType().AssemblyQualifiedName));
  47:                 message.AppendLine(string.Format("Stack Trace: {0}", ex.InnerException.StackTrace));
  48:                 EventLog.WriteEntry("Application", message.ToString());
  49:                 MessageBox.Show(ex.InnerException.Message + Environment.NewLine + "For detailed information, please view event log", string.Empty, MessageBoxButtons.OK, MessageBoxIcon.Error);
  50:             }
  51:         }
  52:     }
  53: }

imageEventHandlerWrapper通过EventHandler对象创建,并将EventHandler的Target和Method赋值给EventHandlerWrapper的同名属性。此外,EventHandlerWrapper得Invoke方法中,将对Method的调用放在一个Try/Catch中,并对捕获的异常进行简单的处理:记录到EventLog中在通过MessageBox将相关异常信息显示出来。而EventHandlerWrapper的Handler属性就是对该Invoke方法的直接反映。最后定义了一个隐式类型转换将EventHandlerWrapper直接转换成EventHandler。转化后返回的就是反映Invoke方法的Handler属性。为了演示,我写了一个简单的计算器的应用。该应用运行后的界面如右图所示,这是一个进行简单除法运算的计算器。 下面是相关的代码:

   1: using System;
   2: using System.Windows.Forms;
   3: namespace ProgramingWithoutTryCatch
   4: {
   5:     public partial class Form1 : Form
   6:     {
   7:         public Form1()
   8:         {
   9:             InitializeComponent();
  10:             this.buttonCalculate.Click += new EventHandlerWrapper(buttonCalculate_Click);
  11:         }
  12:  
  13:         private void buttonCalculate_Click(object sender, EventArgs e)
  14:         {
  15:             int op1 = int.Parse(this.textBoxOp1.Text);
  16:             int op2 = int.Parse(this.textBoxOp2.Text);
  17:             int result = op1 / op2;
  18:             this.textBoxResult.Text = result.ToString();
  19:         }     
  20:     }
  21: }

代码非常简单,需要注意的是在对Button的Click事件进行注册的时候,我们直接使用的时我们上面创建的EventHandlerWrapper,这和真正进行事件注册的方式几乎一致。当你输入非数字或者被除数设置为的时候,会抛出异常,异常的相关信息会直接写入EventLog,并将异常消息通过MessageBox显示出来,如下图所示:

image image

五、通过EventHandlerWrapper的写法实现其他的功能

EventHandlerWrapper实际上为了展示了对EventHandler进行封装的方式,异常处理并非其独有的应用场景。如果你看过我的文章《事件 (Event),绝大多数内存泄漏(Memory Leak)的元凶(上篇)(下篇)》,你会发现我通过相同的方式解决了事件注册导致的内存泄露的问题。在这里我在介绍另外一种有趣的应用。

在进行Windows Forms开发中,相信你会经常要求实现这样的功能:如果点击某个按钮后,需要较长的反映时间,需要在点击之后将Form的光标设置成沙漏的形状(Wait Cursor),当整个处理结束后再将其回复。我们可以对EventHandlerWrapper的Invoke方法略加修改就能够实现这个功能:

   1: private void Invoke(object sender, EventArgs args)
   2: {
   3:     if(null !=  Form.ActiveForm)
   4:     {
   5:         Form.ActiveForm.Cursor = Cursors.WaitCursor;
   6:     }
   7:     try
   8:     {
   9:         this.Method.Invoke(this.Target, new object[] { sender, args });
  10:     }
  11:     finally
  12:     {
  13:         if (null != Form.ActiveForm)
  14:         {
  15:             Form.ActiveForm.Cursor = Cursors.Default;
  16:         }
  17:     }
  18: }
作者:Artech
出处:http://artech.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
posted @ 2010-03-26 21:33  Artech  阅读(19504)  评论(92编辑  收藏  举报