技術交流

與 ASP.Net MVC 5 + EF 6 + DI/IoC 的美麗邂逅

更新於

無論是何種產業,程式設計人員進行系統開發時,皆須謹慎評估與考量適當的程式語言、應用程式所產生的效能,及須注意與防範的問題。資通電腦委外人力主要運用 .Net 跟 java 開發技術。使用物件導向的觀念與方式,透過由『分離組件(Components)』的設置與使用,以降低類別或模組之間的耦合度,不僅能增加靈活度,更能確保未來的擴充性及調修的彈性。

而採用 ASP.Net MVC 5 + Entity Framework 6,佐以 Dependency Injection(DI;相依性注入)以及跟 DI 息息相關的 Inversion of Control (IoC;控制反轉)為架構的購物網站,日前已經陸陸續續通過 Fortify、WebInspect 以及壓力等測試,在程式開發與測試期間遭遇的問題,所幸都能一一克服,以下將與大家分享導入 DI/IoC 的緣由,以及如何避免 EF 重複執行查詢命令。

DI/IoC

首先來看專案背景,與本案的串接網站系統:

  • 會員網站:平行開發,結案時間與本案差不多,會用到註冊、登入、計點等功能。開發前期幾乎無法與之交易,最快也只能在連結測試時進行整合。但可以事先拿到各功能的介面(Interface)。
  • 銀行匯款:動工時,還不知道是哪家銀行及定案時間?不過接觸洽談的銀行會先行提供API(Application Programming Interface;應用程式介面)。
  • 取貨方式:有超商及宅配等二種,但每種方式可搭配數家廠商,分別提供各自的服務 API。

DI/IoC 牽涉物件設計模式範圍廣泛,不妨上網搜尋查閱,在此不再贅述,然而其結果可以歸納出採用 DI/IoC 的目的不外乎寬鬆耦合(loose coupling)及動態繫結(Dynamic Binding),這也是此次導入採用的主因。面對上述不確定因素及複雜的功能進行抽象化、分離介面,逐一針對介面來撰寫具象類別程式。因此,先寫模擬程式(如登入、ATM & 信用卡等),讓整個購物流程更順暢,待相關程式完成後便可進行替換,達成寬鬆耦合的目標。

關於 DI/IoC 的現成簡單範例,可利用 Microsoft Visual Studio 新增 ASP.Net Web Application 專案裡的 Startup 為開端導引的 Startup.Auth,雖然是一個登入授權,其內容以 Manager 結尾命名的類別(Class)都是採用「建構式注入」(Constructor Injection)搭配 Factory、Decorator 等模式,最精彩的是在 ConfigureAuth 中最下方被註解的部分,各協力廠的登入服務通通都是 IAppBuilder 實作。

這案例還有一個 OWIN(Open Web Interface for .NET)的概念,主要是為了讓網站應用程式和網站伺服器解除依賴關係。

另一個值得探討的課題是基於 DI 模式時,考量到物件生成、生命週期等因素需要寫相當多的介面,由於有太多 I 起頭的 Interface,造成閱讀程式碼的難度提高,除非逐步執行,否則往往會不知道究竟是在執行哪個物件。

Entity Framework

查詢就免不了 LINQ(Language Integrated Query)及 Lambda 運算式,支援型別就是 IEnumerable<T> 及衍生介面(例如 IQueryable<T>),通常會使用 var 關鍵字來避免泛型語法,它是「推斷」類型,指示編譯器從陳述式右側的運算式推斷變數的類型:如內建類型、匿名型別、使用者定義型別,或 .NET Framework 類別庫中定義的類型。

Entity Framework
(資料來源:小朱)

上述查詢方法有一特性就是延遲執行,變數本身只會儲存查詢命令,非得等到 foreach 迴圈或是 ToList、ToArray 或彙整等指令時,才會真正去執行。而延遲的結果可能會導致兩個問題:

  • 沒有回傳:查詢尚未執行,連線就被關閉,如圖(一)中的 Index01(),改善方式可以如 Index02()。
  • 重複執行:如圖(一)中的 Index03(),呼叫者分別是 View 中的 Model.Count()、foreach(var item in Model)以及 Controller 的 result.Count(),改善方式可以如 Index04()。利用 SQL Server Profiler 監控命令的執行情形,如圖(二)與(三)。
圖(一):注意!左邊 View 的 Model.Count() 會執行一次指令,但查詢結果不會轉給下面的 foreach 使用。
圖(二):綠框標示是 result.Count() 的執行命令,而 ①② 是相同指令,由 View 中的 Model.Count() 與 froeach 發出。這案例也呈現出雖都是 Count(),但是運用的 SQL 句法卻是不一樣。
圖(三):指令是由 Controller 強制執行,所以 View 中的 Model 就毋須再執行查詢。

千萬不要貪圖便利,一味地要求強制執行,這可能招來反效果。如圖(四),若都加上 ToList,則結果如何?這樣會把 Tuple 內的物件通通執行一次,那麼「有用到時,才執行」的延遲效益就被抹煞了。

圖(四):Model.Item1 如果是 Null 值時,那麼就不會執行 Model.item2 的查詢;留意程式碼的搭配方式,它會決定查詢的時機。

其實程式在開發時,也都提防「延遲效應」,沒想到在進行壓力測試時,「指令重複執行」還是發生了。追究到底,錯誤的根源竟然是最令人想不到的地方:View!

閱讀更多