使用AOP 使C#代碼更清晰

來源:互聯網
上載者:User

簡介

如果你很熟悉面向方面編程(AOP),你就會知道給代碼增加“切面”可以使代碼更清晰並且具有可維護性。但是AOP通常都依賴於第三方類庫或者硬式編碼.net特性來工作。雖然這些實現方式的好處大於它們的複雜程度,但是我仍然在尋找一種實現AOP的更為簡單的方式,來試My Code更為清晰。我將它們單獨移出來,並命名為AspectF。

Aspect Oriented Programming (AOP)的背景

“切面”指的是那些在你寫的代碼中在項目的不同部分且有相同共性的東西。它可能是你代碼中處理異常、記錄方法調用、時間處理、重新執行一些方法等等的一些特殊方式。如果你沒有使用任何面向切面編程的類庫來做這些事情,那麼在你的整個項目中將會遺留一些很簡單而又重複的代碼,它將使你的代碼很難維護。例如,在你的商務邏輯層有些方法需要被記錄,有些異常需要被處理,有些執行需要計時,資料庫操作需要重試等等。所以,也許你會寫出下面這樣的代碼。

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

你會看到上面只有兩行關鍵代碼,它調用了CustomerData執行個體的一個方法插入了一個Customer。但去實現這樣的商務邏輯,你真的很難去照顧所有的細節(日誌記錄、重試、異常處理、操作計時)。項目越成熟,在你的代碼中需要維護的這些“邊邊角角”就更多了。所以你肯定經常會到處拷貝這些“樣板”代碼,但只在這些樣板內寫少了真是的東西。這多不值!你不得不對每個商務邏輯層的方法都這麼做。比如現在你想在你的商務邏輯層中增加一個UpdateCustomer方法。你不得不再次拷貝所有的這些“樣板”,然後將兩行關鍵代碼加入其中。

思考這樣的情境,你需要做出一個項目層級的改變——針對如何處理異常。你不得不處理你寫的這“上百”的方法,然後一個一個地修改它們。如果你想修改計時的邏輯,做法同樣如此。

面向切面編程就可以很好地處理這些問題。當你採用AOP,你會以一種很酷的方式來實現它:

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

這裡你需要區分這些通用的東西,像日誌記錄、計時、重試、驗證等這些通常被稱為“邊邊角角”的東西,最重要的是完全與你的“真實”代碼無關。這可以使方法將會變得美觀而清晰。所有的這些細節都在方法外被處理,並且只是在代碼外加上了一些屬性。這裡,每一個屬性代表一個Aspect(切面)。例如,你可以增加“日誌記錄”切面到任何代碼中,只需要增加一個Log屬性。無論你使用何種AOP的類庫,該類庫都能夠確保這些“切面”被有效地加入到代碼中,當然時機不一,可能是在編譯時間,也可能是在運行時。

有許多AOP類庫通過使用編譯事件和IL操作允許你在編譯時間“處理”這些方面,例如PostSharp;而某些類庫使用DynamicProxy在運行時處理;某些要求你的類繼承自ContextBoundObject使用C#內建特性來supportAspects。所有的這些都有某些“不便”。你不得不使用某些外部庫,做足夠的效能測試來那些類庫可擴充等等。而你需要的只是一個非常簡單的方式來實現“隔離”,可能並不是想要完全實現AOP。記住,你的目的是隔離那些並不重要的核心代碼,來讓一切變得簡單並且清晰!

AspectF如何來讓這一切變得簡單!

讓我展示一種簡答的方式來實現這種隔離,僅僅使用標準的C#代碼,類和代理的簡單調用,沒有用到“特性”或者“IL操作”這些東西。它提供了可重用性和可維護性。最好的一點是它的“輕量級”——僅僅一個很小得類。

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

讓我們看看它與通常的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]<br />public static AspectF Retry(this AspectF aspects)<br />{<br /> return aspects.Combine((work) =><br /> Retry(1000, 1, (error) => DoNothing(error), DoNothing, work));<br />}</p><p>[DebuggerStepThrough]<br />public static void Retry(int retryDuration, int retryCount,<br /> Action<Exception> errorHandler, Action retryFailed, Action work)<br />{<br /> do<br /> {<br /> try<br /> {<br /> work();<br /> }<br /> catch (Exception x)<br /> {<br /> errorHandler(x);<br /> System.Threading.Thread.Sleep(retryDuration);<br /> }<br /> } while (retryCount-- > 0);<br /> retryFailed();<br />}

你可以讓“切面”調用你的代碼任意多次。很容易在Retry切面中包裹對資料庫、檔案IO、網路IO、Web Service的調用,因為它們經常由於各種基礎設施問題而失敗,並且有時重試一次就可以解決問題。我有個習慣是總是去嘗試資料庫插入,更新,刪除、web service調用,處理檔案等等。而這樣的“切面”無疑讓我對處理這樣的問題時輕鬆了許多。

下面展示了一下它是如何工作的,它建立了一個代理的組合。而結果就像如下這段代碼:

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

AspectF類除了壓縮這樣的代碼之外,其他什麼都沒有。

下面展示,你怎樣建立你自己的“切面”。首先為AspectF類建立一個擴充方法。比如說,我們建立一個Log:

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

你調用AspectF的Combine方法來壓縮一個將要被放進委託鏈的委託。委託鏈在最後將會被Do方法調用。

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

這裡Combine方法操作的是被“切面”擴充方法傳遞過來的委託,例如Log,然後它將該委託壓入之前加入的一個“切面”的委託中,來保證第一個切面調用第二個,第二個調用第三個,知道最後一個調用真實的(你想要真正執行的)代碼。

Do/Return方法做最後的執行操作。

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

就是這些,現在你有一個非常簡單的方式來分隔那些你不想過度關注的代碼,並使用C#享受AOP風格的編程模式。

AspectF類還有其他幾個方便的“切面”,大致如下(當然你完全可以DIY你自己的‘切面’)。

public static class AspectExtensions<br /> {<br /> [DebuggerStepThrough]<br /> public static void DoNothing()<br /> {</p><p> }</p><p> [DebuggerStepThrough]<br /> public static void DoNothing(params object[] whatever)<br /> {</p><p> }</p><p> [DebuggerStepThrough]<br /> public static AspectF Delay(this AspectF aspect, int milliseconds)<br /> {<br /> return aspect.Combine((work) =><br /> {<br /> System.Threading.Thread.Sleep(milliseconds);<br /> work();<br /> });<br /> }</p><p> [DebuggerStepThrough]<br /> public static AspectF MustBeNonNull(this AspectF aspect, params object[] args)<br /> {<br /> return aspect.Combine((work) =><br /> {<br /> for (int i = 0; i < args.Length; i++)<br /> {<br /> object arg = args[i];<br /> if (arg == null)<br /> {<br /> throw new ArgumentException(string.Format("Parameter at index {0} is null", i));<br /> }<br /> }<br /> work();<br /> });<br /> }</p><p> [DebuggerStepThrough]<br /> public static AspectF MustBeNonDefault<T>(this AspectF aspect, params T[] args) where T : IComparable<br /> {<br /> return aspect.Combine((work) =><br /> {<br /> T defaultvalue = default(T);<br /> for (int i = 0; i < args.Length; i++)<br /> {<br /> T arg = args[i];<br /> if (arg == null || arg.Equals(defaultvalue))<br /> {<br /> throw new ArgumentException(string.Format("Parameter at index {0} is null", i));<br /> }<br /> }<br /> work();<br /> });<br /> }</p><p> [DebuggerStepThrough]<br /> public static AspectF WhenTrue(this AspectF aspect, params Func<bool>[] conditions)<br /> {<br /> return aspect.Combine((work) =><br /> {<br /> foreach (Func<bool> condition in conditions)<br /> {<br /> if (!condition())<br /> {<br /> return;<br /> }<br /> }<br /> work();<br /> });<br /> }</p><p> [DebuggerStepThrough]<br /> public static AspectF RunAsync(this AspectF aspect, Action completeCallback)<br /> {<br /> return aspect.Combine((work) => work.BeginInvoke(asyncresult =><br /> {<br /> work.EndInvoke(asyncresult); completeCallback();<br /> }, null));<br /> }</p><p> [DebuggerStepThrough]<br /> public static AspectF RunAsync(this AspectF aspect)<br /> {<br /> return aspect.Combine((work) => work.BeginInvoke(asyncresult =><br /> {<br /> work.EndInvoke(asyncresult);<br /> }, null));<br /> }<br /> }

現在,你已經擁有了一個簡潔的方式來隔離那些細枝末節的代碼,去享受AOP形式的編程而無需使用任何“笨重”的架構。

源碼下載

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.