解析大型.NET ERP系統:高質量.NET程式碼設計模式

發表於2015-08-13

1 快取 Cache

系統中大量的用到快取設計模式,對系統登入之後不變的資料進行快取,不從資料庫中直接讀取。耗費一些記憶體,相比從SQL Server中再次讀取資料要划算得多。快取的基本設計模式參考下面程式碼:

主要用到的資料結構是字典,字典中的專案不存在時,向其增加,以後再呼叫時,直接從記憶體中取值。

列舉一下,我可以看到的ERP系統中應用快取設計模式的地方,主要分資料快取和物件快取,資源快取:

1) 系統翻譯 ERP系統中的文句翻譯內容儲存在資料庫表中,只需要在系統登入時讀取一次,快取到DataTable中。

2) 系統引數 登入系統之後,當前的財年,會計期間,採購單批核流程,物料編碼長度,是否實施批號和序號,記帳憑證過帳前是否需要稽核,成本核算的來源(物料成本,物料成本+人工成本,物料成本+人工成本+機器成本),這些引數都可以快取在Entity中,使用者修改這些引數值,需要提醒或是強制使用者退出重新登入。

3) 系統查詢 系統中可預定義一組查詢語句,在程式碼中將查詢語句轉化為查詢物件,將查詢物件快取,節省SQL語句到查詢物件的轉化時間。

4) 物件例項 以外掛方式在搜尋程式集中包含的系統功能時,搜尋到後,會將程式功能對應的型別快取,所以第二次執行功能的速度會相當快。參考下面的例子程式碼加深印象:

在我的通用應用程式開源框架中,有上面這個例子的完整程式碼。

5) 資源快取 系統中會用到一些以嵌入方式編譯到程式集中的資原始檔,在搜尋到資原始檔後,也是以字典的方式快取資源(圖示Icon,圖片Image,文字Text,查詢語句Query)。

 

2 查詢優化 Query Optimize

這是個很容易理解的設計模式,貴在堅持。我們在讀取資料時,只讀取最少的可用的資料,避免讀取不需要的資料。用查詢語句表達如下,下面是沒有效率的查詢資料:

經過改善之後的語句,改成只讀需要使用的資料,改善後的查詢如下:

後者的效能會好很多。對於我使用的LLBL Gen Pro,把上面的程式碼轉化為程式程式碼,也就是下面的例子程式所示:

即使沒有接觸過LLBL Gen Pro,也可感受到型別IncludeFieldsList 的作用是為了挑選要讀取的資料列,也就是要使用什麼欄位,就讀什麼欄位,避免讀取不需要的欄位。

對於上面的程式,它的效能開銷主要在讀取資料和建立物件方面,為了效能再快一點,考慮讀取資料轉化為DataTable,可讀性上有所降低但效能又提升了一些。

繼續改善查詢的效能,假設場景是銷售訂單表要讀取客戶編號和客戶名稱,我們直接在銷售訂單表中增加客戶名稱欄位,這樣每次載入銷售訂單時,可直接讀取到銷售訂單表自身的客戶名稱欄位,而不用左連線關聯到客戶表讀取客戶名稱。

Entity Framework或是第三方的ORM 查詢介面,應該都具備上面列舉的特性。

ORM查詢不推薦使用LINQ,效能是主要考慮的方面。ORM框架將查詢轉化為實體物件時,因為不能預料到後面會用到實體的哪些屬性,預先讀取所有的欄位繫結到屬性中,效能難以接受,這跟前面提到的SELECT * 讀取所有欄位是同樣的意思,延遲繫結屬性,用到屬性時再讀取相應的資料庫欄位,每用一個屬性都去讀取一次資料庫,對資料庫的連線次數過於頻繁,也不可接受。

下面的寫法是我最不能忍受的查詢寫法,參考程式碼中的例子:

為了取一個表中的最後一筆記錄,居然將整個表都讀取到記憶體中,再取最後一條記錄。

這種查詢可以改善成SELECT TOP 1 + ORDER BY,讀一筆資料的效能肯定優於讀取未知筆資料記錄。

 

3 延遲載入 Delay Load

在使用物件時,只有當需要使用物件的方法或屬性,我們才例項化物件。設計模式的程式碼例子如下:

突然想到這種模式就是系統快取的實現方法。在型別中定義一個私有靜態變數,使用這個變數時我們才去初始化它的例項。延遲載入避免了系統啟動時建立所有快取物件耗費的記憶體和時間,有些物件或許根本不會用到,也就不應該去建立。

比如使用者僅登入進系統,沒有做任何業務單據操作然後退出。如果在登入時就建立貨幣或付款條款的快取,而使用者又沒有使用這些資料,影響了系統效能。

 

4 後臺執行緒與多執行緒 BackgroundWorker/WorkerThreadBase

.NET 提供了後臺執行緒控制元件,解決了長時間操作避免主介面卡死的問題。在系統中,凡是涉及到資料庫操作,不能在很短時間內完成的,都放到BackgroundWorker後臺執行緒中執行。系統中大量使用BackgroundWorker的地方:

1) 單據增刪查改 所有單據對資料的Insert,Delete,Update都用BackgroundWorker操作。

2) 查詢 所有關於資料的查詢封裝到BackgroundWorker中執行。

3) 資料操作類功能:資料初始化,資料再開始,核算供應商帳,核算客戶帳,資料存檔,資料備份,資料還原。

4) 業務單據過帳,業務單據完成,業務單據取消,業務單據修改。

當沒有介面時,無法使用BackgroundWorker,可以用多執行緒元件改善效能。參考下面的例子程式碼:

呼叫上面的多執行緒元件,參看下面的例子程式碼:

多執行緒元件WorkerThreadBase可以在Code Project上找到原始碼和講解文章。

 

5 資料字典 Data Dictionary

主要介紹不可變的資料字典的設計模式,先看一下性別Gender的資料字典設計:

為列舉型別增加了二個特性,StringValue用於儲存,DisplayValue用於介面控制元件中顯示,這跟資料繫結中的介紹的資料來源的ValueMember和DisplayMember是一樣的原理。再來看使用程式碼:

也可以這樣呼叫獲取顯示的值DisplayValue:

這樣設計模式解決了資料字典的文件更新的煩惱。編寫原始碼同時就設計好了文件,想知道資料字典的值,直接開啟列舉型別定義即可。

 

6 校驗-執行-驗證 Validate-Post-Verify

對業務邏輯的業務操作,遵守校驗-執行-驗證設計約定,來看一段程式碼加深印象:

先校對要執行操作的資料,再對資料進行操作,操作完成之後,再對期望的資料進行驗證。

比如發票生成憑證,先要驗證發票上的金額是否大於零,開發票的時間是否是當前期間等業務邏輯,再執行憑證生成(Voucher)動作,最後驗證生成的憑證的借貸方是否一致,是否考慮到小數點進位導致的借貨方不一致,生成的憑證金額是否與原發票上的金額相等。

 

7 執行前-執行-執行後 OnBefore-Perform-OnAfter

第六條講解是的業務記帳方法,第七條這裡講解的是公共框架與應用程式互動的方法。繼承的.NET窗體或派生類要能改變基類的行為,需要設計一種方法來達到此目的。先看一段程式碼熟悉這種設計模式:

為了加深瞭解這種設計模式,我對上面的程式碼段用兩行空格分開成三個部分,下面詳細講解這三個部分:

OnBefore 在執行操作前,派生類可以設定引數到基類中,影響基類的行為。比如可以執行一個事件,也可以向基類傳遞取消條件,派生類向基類傳遞Cancel=true的標誌位,完全取消當前的操作。這是派生類影響基類行為的一種設計方式。另一種方法是丟擲異常,異常會導致整個堆疊回滾。

Perform 執行要做的操作,這個命名是按照.NET的規範。比如我們想在程式碼中直接執行按鈕的點選事件,可以這樣寫呼叫程式碼的方法:btnOK.PerformClick();

OnAfter 在執行完成後。可以對執行的結果重寫,也可以呼叫派生類中的事件。

 

8 後設資料 Metadata

框架能完成很多應用程式一句話呼叫就能完成的功能,後設資料的功勞最大。系統中的實體物件的每個欄位都有一張附加屬性表,參考下面的程式碼定義:

看到上面的程式碼,當前實體的每一個屬性都可以繫結一個Dictionary物件,這段程式碼是用程式碼生成器完成。於是發揮想象力,將欄位的特殊屬性放到實體屬性的附加屬性中,框架可完成很多基礎功能。

看到上面的RefNo屬性中增加了AllowEditForNewOnly和CapsLock兩條後設資料。在系統框架部分,程式碼參考如下:

後設資料通過程式碼生成器的實體設計完成,框架獲取實體程式碼的後設資料,做一些控制元件屬性上的公共設定,節省了大量的重複的程式碼。以上是屬性上的後設資料,也可以增加實體層級上的後設資料,後設資料的存在給框架設計帶來了便利。

如果正在設計一套ORM框架,考慮給實體和實體的屬性增加後設資料(自定義屬性),它會為系統的可擴充套件帶來諸多方便。

相關文章