一、異常處理不簡單
個人覺得,異常處理對於程序員來說,尤其是對於那些初級.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: }
EventHandlerWrapper 通過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顯示出來,如下圖所示:
五、通過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: }