1 快取 Cache
系統中大量的用到快取設計模式,對系統登入之後不變的資料進行快取,不從資料庫中直接讀取。耗費一些記憶體,相比從SQL Server中再次讀取資料要划算得多。快取的基本設計模式參考下面程式碼:
1 2 3 4 5 |
private static ConcurrentDictionary<string, LookupDialogEntity> _cachedLookupDialogEntities = new ConcurrentDictionary<string, LookupDialogEntity>(); if (!_cachedLookupDialogEntities.ContainsKey(key)) lookupDialog = _cachedLookupDialogEntities.GetOrAdd(key, lookupDialog); else _cachedLookupDialogEntities[key] = lookupDialog; |
主要用到的資料結構是字典,字典中的專案不存在時,向其增加,以後再呼叫時,直接從記憶體中取值。
列舉一下,我可以看到的ERP系統中應用快取設計模式的地方,主要分資料快取和物件快取,資源快取:
1) 系統翻譯 ERP系統中的文句翻譯內容儲存在資料庫表中,只需要在系統登入時讀取一次,快取到DataTable中。
2) 系統引數 登入系統之後,當前的財年,會計期間,採購單批核流程,物料編碼長度,是否實施批號和序號,記帳憑證過帳前是否需要稽核,成本核算的來源(物料成本,物料成本+人工成本,物料成本+人工成本+機器成本),這些引數都可以快取在Entity中,使用者修改這些引數值,需要提醒或是強制使用者退出重新登入。
3) 系統查詢 系統中可預定義一組查詢語句,在程式碼中將查詢語句轉化為查詢物件,將查詢物件快取,節省SQL語句到查詢物件的轉化時間。
4) 物件例項 以外掛方式在搜尋程式集中包含的系統功能時,搜尋到後,會將程式功能對應的型別快取,所以第二次執行功能的速度會相當快。參考下面的例子程式碼加深印象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
public void OpenFunctionForm(string functionCode) { functionCode = functionCode.ToUpper().Trim(); Type formBaseType = null; if (!_formBaseType.TryGetValue(functionCode, out formBaseType)) { Assembly assembly = Assembly.GetExecutingAssembly(); foreach (Type type in assembly.GetTypes()) { try { object[] attributes = type.GetCustomAttributes(typeof(FunctionCode), true); foreach (object obj in attributes) { FunctionCode attribute = (FunctionCode)obj; if (!string.IsNullOrEmpty(attribute.Value)) { if (!_formBaseType.ContainsKey(attribute.Value)) _formBaseType.Add(attribute.Value, type); if (formBaseType == null && attribute.Value.Equals(functionCode,StringComparison.InvariantCultureIgnoreCase)) formBaseType = type; } if (formBaseType != null) { goto Found; } } } catch { } } } Found: if (formBaseType != null) { object entryForm = Activator.CreateInstance(formBaseType) as Form; Form functionForm = (Form)entryForm; OpenFunctionForm(functionForm); } } |
在我的通用應用程式開源框架中,有上面這個例子的完整程式碼。
5) 資源快取 系統中會用到一些以嵌入方式編譯到程式集中的資原始檔,在搜尋到資原始檔後,也是以字典的方式快取資源(圖示Icon,圖片Image,文字Text,查詢語句Query)。
2 查詢優化 Query Optimize
這是個很容易理解的設計模式,貴在堅持。我們在讀取資料時,只讀取最少的可用的資料,避免讀取不需要的資料。用查詢語句表達如下,下面是沒有效率的查詢資料:
1 |
SELECT * FROM Company |
經過改善之後的語句,改成只讀需要使用的資料,改善後的查詢如下:
1 |
SELECT CompanyCode, CompanyName FROM Company |
後者的效能會好很多。對於我使用的LLBL Gen Pro,把上面的程式碼轉化為程式程式碼,也就是下面的例子程式所示:
1 2 3 4 5 6 7 8 9 |
IncludeFieldsList fieldList = new IncludeFieldsList(); fieldList.Add(FiscalPeriodFields.Period); fieldList.Add(FiscalPeriodFields.FiscalYear); fieldList.Add(FiscalPeriodFields.PeriodNo); IFiscalPeriodManager fiscalPeriodManager = ClientProxyFactory.CreateProxyInstance<IFiscalPeriodManager>(); FiscalPeriodEntity fiscalPeriodEntity = fiscalPeriodManager.GetFiscalPeriod(Shared.CurrentUserSessionId, this.VoucherDate, null, fieldList); this.Period = fiscalPeriodEntity.Period; this.FiscalYear = fiscalPeriodEntity.FiscalYear; this.PeriodNo = fiscalPeriodEntity.PeriodNo; |
即使沒有接觸過LLBL Gen Pro,也可感受到型別IncludeFieldsList 的作用是為了挑選要讀取的資料列,也就是要使用什麼欄位,就讀什麼欄位,避免讀取不需要的欄位。
對於上面的程式,它的效能開銷主要在讀取資料和建立物件方面,為了效能再快一點,考慮讀取資料轉化為DataTable,可讀性上有所降低但效能又提升了一些。
1 2 3 4 5 6 7 8 9 10 11 |
IRelationPredicateBucket filterBucket = new RelationPredicateBucket(); filterBucket.PredicateExpression.Add(ShipmentFields.CustomerNo == this.CustomerNo); filterBucket.PredicateExpression.Add(ShipmentFields.Posted == true); filterBucket.Relations.Add(new EntityRelation(ShipmentDetailFields.OrderNo, SalesOrderDetailFields.OrderNo, RelationType.ManyToMany)); filterBucket.PredicateExpression.Add(ShipmentDetailFields.QtyShipped == SalesOrderDetailFields.Qty); ResultsetFields fields = new ResultsetFields(4); fields.DefineField(ShipmentFields.RefNo, 0); fields.DefineField(ShipmentFields.PayTerms, 1); fields.DefineField(ShipmentFields.Ccy, 2); fields.DefineField(ShipmentFields.ShipmentDate, 3); System.Data.DataTable shipments = userDefinedQueryManager.GetQueryResult(Shared.CurrentUserSessionId, fields, filterBucket, null, null, false, false); |
繼續改善查詢的效能,假設場景是銷售訂單表要讀取客戶編號和客戶名稱,我們直接在銷售訂單表中增加客戶名稱欄位,這樣每次載入銷售訂單時,可直接讀取到銷售訂單表自身的客戶名稱欄位,而不用左連線關聯到客戶表讀取客戶名稱。
Entity Framework或是第三方的ORM 查詢介面,應該都具備上面列舉的特性。
ORM查詢不推薦使用LINQ,效能是主要考慮的方面。ORM框架將查詢轉化為實體物件時,因為不能預料到後面會用到實體的哪些屬性,預先讀取所有的欄位繫結到屬性中,效能難以接受,這跟前面提到的SELECT * 讀取所有欄位是同樣的意思,延遲繫結屬性,用到屬性時再讀取相應的資料庫欄位,每用一個屬性都去讀取一次資料庫,對資料庫的連線次數過於頻繁,也不可接受。
下面的寫法是我最不能忍受的查詢寫法,參考程式碼中的例子:
1 2 |
EntityCollection<AccountsReceivableJournalEntity> journalCollection = adapter.FetchEntityCollection<AccountsReceivableJournalEntity>(filterBucket, 1, sorter, null, fieldList); AccountsReceivableJournalEntity lastJournal = journalCollection[journalCollection.Count-1]; |
為了取一個表中的最後一筆記錄,居然將整個表都讀取到記憶體中,再取最後一條記錄。
這種查詢可以改善成SELECT TOP 1 + ORDER BY,讀一筆資料的效能肯定優於讀取未知筆資料記錄。
3 延遲載入 Delay Load
在使用物件時,只有當需要使用物件的方法或屬性,我們才例項化物件。設計模式的程式碼例子如下:
1 2 3 4 5 6 7 |
PayTermEntity payTerm = null; payTerms.TryGetValue(dataRow["PayTerms"].ToString(), out payTerm); if (payTerm == null) { payTerm = payTermManager.GetPayTerm(Shared.CurrentUserSessionId, dataRow["PayTerms"].ToString()); payTerms.Add(payTerm.PayTerms, payTerm); } |
突然想到這種模式就是系統快取的實現方法。在型別中定義一個私有靜態變數,使用這個變數時我們才去初始化它的例項。延遲載入避免了系統啟動時建立所有快取物件耗費的記憶體和時間,有些物件或許根本不會用到,也就不應該去建立。
比如使用者僅登入進系統,沒有做任何業務單據操作然後退出。如果在登入時就建立貨幣或付款條款的快取,而使用者又沒有使用這些資料,影響了系統效能。
4 後臺執行緒與多執行緒 BackgroundWorker/WorkerThreadBase
.NET 提供了後臺執行緒控制元件,解決了長時間操作避免主介面卡死的問題。在系統中,凡是涉及到資料庫操作,不能在很短時間內完成的,都放到BackgroundWorker後臺執行緒中執行。系統中大量使用BackgroundWorker的地方:
1) 單據增刪查改 所有單據對資料的Insert,Delete,Update都用BackgroundWorker操作。
2) 查詢 所有關於資料的查詢封裝到BackgroundWorker中執行。
3) 資料操作類功能:資料初始化,資料再開始,核算供應商帳,核算客戶帳,資料存檔,資料備份,資料還原。
4) 業務單據過帳,業務單據完成,業務單據取消,業務單據修改。
當沒有介面時,無法使用BackgroundWorker,可以用多執行緒元件改善效能。參考下面的例子程式碼:
1 2 3 4 5 6 7 8 |
private sealed class LoadItemsWorker : WorkerThreadBase { private MrpEntity _mrp; private ConcurrentBag<DataRow> _itemMasterRows; protected override void Work() { //long time operation } |
呼叫上面的多執行緒元件,參看下面的例子程式碼:
1 2 3 4 5 6 7 |
List<LoadItemsWorker> workers = new List<LoadItemsWorker>(); for (int i = 0; i < MAX_RUNNING_THREAD; i++) { LoadItemsWorker worker = new LoadItemsWorker(sessionId, this, mrp); workers.Add(worker); } WorkerThreadBase.StartAndWaitAll(workers.ToArray()); |
多執行緒元件WorkerThreadBase可以在Code Project上找到原始碼和講解文章。
5 資料字典 Data Dictionary
主要介紹不可變的資料字典的設計模式,先看一下性別Gender的資料字典設計:
1 2 3 4 5 6 7 8 9 |
public enum Gender { [StringValue("M")] [DisplayValue("Male")] Male, [StringValue("F")] [DisplayValue("Female")] Female } |
為列舉型別增加了二個特性,StringValue用於儲存,DisplayValue用於介面控制元件中顯示,這跟資料繫結中的介紹的資料來源的ValueMember和DisplayMember是一樣的原理。再來看使用程式碼:
1 2 |
Employee employee=... employee.Gender=StringEnum<Gender>.GetStringValue(Gender.Male); |
也可以這樣呼叫獲取顯示的值DisplayValue:
1 |
string displayValue=StringEnum<Gender>.GetDisplayValue(Gender.Male); |
這樣設計模式解決了資料字典的文件更新的煩惱。編寫原始碼同時就設計好了文件,想知道資料字典的值,直接開啟列舉型別定義即可。
6 校驗-執行-驗證 Validate-Post-Verify
對業務邏輯的業務操作,遵守校驗-執行-驗證設計約定,來看一段程式碼加深印象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
try { adapter.StartTransaction(IsolationLevel.ReadCommitted, "PostInvoice"); this.ValidateBeforePost(sessionId, accountsReceivableAllocation); this.Post(sessionId, accountsReceivableAllocation); this.VerifyGeneratedVoucher(sessionId, accountsReceivableAllocation); adapter.Commit(); } catch { adapter.Rollback(); throw; } |
先校對要執行操作的資料,再對資料進行操作,操作完成之後,再對期望的資料進行驗證。
比如發票生成憑證,先要驗證發票上的金額是否大於零,開發票的時間是否是當前期間等業務邏輯,再執行憑證生成(Voucher)動作,最後驗證生成的憑證的借貸方是否一致,是否考慮到小數點進位導致的借貨方不一致,生成的憑證金額是否與原發票上的金額相等。
7 執行前-執行-執行後 OnBefore-Perform-OnAfter
第六條講解是的業務記帳方法,第七條這裡講解的是公共框架與應用程式互動的方法。繼承的.NET窗體或派生類要能改變基類的行為,需要設計一種方法來達到此目的。先看一段程式碼熟悉這種設計模式:
1 2 3 4 5 6 7 8 9 10 11 12 |
CancelableRecordEventArgs e = new CancelableRecordEventArgs(this.CurrentEntity); this.OnBeforeCancelEdit(e); if (this._beforeCancelEdit != null) this._beforeCancelEdit(this, e); if (e.Cancel) return false; bool flag = this.DoPerformCancelEdit(this.CurrentEntity); RecordEventArgs args2 = new RecordEventArgs(this.CurrentEntity); this.OnAfterCancelEdit(args2); if (this._afterCancelEdit != null) this._afterCancelEdit(this, args2); |
為了加深瞭解這種設計模式,我對上面的程式碼段用兩行空格分開成三個部分,下面詳細講解這三個部分:
OnBefore 在執行操作前,派生類可以設定引數到基類中,影響基類的行為。比如可以執行一個事件,也可以向基類傳遞取消條件,派生類向基類傳遞Cancel=true的標誌位,完全取消當前的操作。這是派生類影響基類行為的一種設計方式。另一種方法是丟擲異常,異常會導致整個堆疊回滾。
Perform 執行要做的操作,這個命名是按照.NET的規範。比如我們想在程式碼中直接執行按鈕的點選事件,可以這樣寫呼叫程式碼的方法:btnOK.PerformClick();
OnAfter 在執行完成後。可以對執行的結果重寫,也可以呼叫派生類中的事件。
8 後設資料 Metadata
框架能完成很多應用程式一句話呼叫就能完成的功能,後設資料的功勞最大。系統中的實體物件的每個欄位都有一張附加屬性表,參考下面的程式碼定義:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
private static void SetupCustomPropertyHashtables() { _customProperties = new Dictionary<string, string>(); _fieldsCustomProperties = new Dictionary<string, Dictionary<string, string>>(); _customProperties.Add("SupportDocumentApproval", @""); _customProperties.Add("SupportExternalAttachment", @""); Dictionary<string, string> fieldHashtable; fieldHashtable = new Dictionary<string, string>(); _fieldsCustomProperties.Add("Recnum", fieldHashtable); fieldHashtable = new Dictionary<string, string>(); fieldHashtable.Add("AllowEditForNewOnly", @""); fieldHashtable.Add("CapsLock", @""); _fieldsCustomProperties.Add("RefNo", fieldHashtable); fieldHashtable = new Dictionary<string, string>(); fieldHashtable.Add("ReadOnly", @""); |
看到上面的程式碼,當前實體的每一個屬性都可以繫結一個Dictionary物件,這段程式碼是用程式碼生成器完成。於是發揮想象力,將欄位的特殊屬性放到實體屬性的附加屬性中,框架可完成很多基礎功能。
看到上面的RefNo屬性中增加了AllowEditForNewOnly和CapsLock兩條後設資料。在系統框架部分,程式碼參考如下:
1 2 3 4 5 6 7 8 9 10 11 |
Dictionary<string, string> fieldsCustomProperties = GetFieldsCustomProperties(boundEntity, bindingMemberInfo.BindingField); if (fieldsCustomProperties != null) { if (fieldsCustomProperties.ContainsKey("CapsLock")) { base.CharacterCasing = CharacterCasing.Upper; } else if (!(this.AlwaysReadOnly || !fieldsCustomProperties.ContainsKey("AllowEditForNewOnly"))) { this._allowEditForNewOnly = true; } |
後設資料通過程式碼生成器的實體設計完成,框架獲取實體程式碼的後設資料,做一些控制元件屬性上的公共設定,節省了大量的重複的程式碼。以上是屬性上的後設資料,也可以增加實體層級上的後設資料,後設資料的存在給框架設計帶來了便利。
如果正在設計一套ORM框架,考慮給實體和實體的屬性增加後設資料(自定義屬性),它會為系統的可擴充套件帶來諸多方便。