1 緩存 Cache
系統中大量的用到緩存設計模式,對系統登入之后不變的數據進行緩存,不從數據庫中直接讀取。耗費一些內存,相比從SQL Server中再次讀取數據要划算得多。緩存的基本設計模式參考下面代碼:
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) 對象實例 以插件方式在搜索程序集中包含的系統功能時,搜索到后,會將程序功能對應的類型緩存,所以第二次執行功能的速度會相當快。參考下面的例子代碼加深印象:
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
這是個很容易理解的設計模式,貴在堅持。我們在讀取數據時,只讀取最少的可用的數據,避免讀取不需要的數據。用查詢語句表達如下,下面是沒有效率的查詢數據:
SELECT * FROM Company
經過改善之后的語句,改成只讀需要使用的數據,改善后的查詢如下:
SELECT CompanyCode, CompanyName FROM Company
后者的性能會好很多。對於我使用的LLBL Gen Pro,把上面的代碼轉化為程序代碼,也就是下面的例子程序所示:
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,可讀性上有所降低但性能又提升了一些。
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 * 讀取所有字段是同樣的意思,延遲綁定屬性,用到屬性時再讀取相應的數據庫字段,每用一個屬性都去讀取一次數據庫,對數據庫的連接次數過於頻繁,也不可接受。
下面的寫法是我最不能忍受的查詢寫法,參考代碼中的例子:
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
在使用對象時,只有當需要使用對象的方法或屬性,我們才實例化對象。設計模式的代碼例子如下:
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,可以用多線程組件改善性能。參考下面的例子代碼:
private sealed class LoadItemsWorker : WorkerThreadBase { private MrpEntity _mrp; private ConcurrentBag<DataRow> _itemMasterRows; protected override void Work() { //long time operation }
調用上面的多線程組件,參看下面的例子代碼:
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的數據字典設計:
public enum Gender { [StringValue("M")] [DisplayValue("Male")] Male, [StringValue("F")] [DisplayValue("Female")] Female }
為枚舉類型增加了二個特性,StringValue用於存儲,DisplayValue用於界面控件中顯示,這跟數據綁定中的介紹的數據源的ValueMember和DisplayMember是一樣的原理。再來看使用代碼:
Employee employee=... employee.Gender=StringEnum<Gender>.GetStringValue(Gender.Male);
也可以這樣調用獲取顯示的值DisplayValue:
string displayValue=StringEnum<Gender>.GetDisplayValue(Gender.Male);
這樣設計模式解決了數據字典的文檔更新的煩惱。編寫源代碼同時就設計好了文檔,想知道數據字典的值,直接打開枚舉類型定義即可。
6 校驗-執行-驗證 Validate-Post-Verify
對業務邏輯的業務操作,遵守校驗-執行-驗證設計約定,來看一段代碼加深印象:
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窗體或派生類要能改變基類的行為,需要設計一種方法來達到此目的。先看一段代碼熟悉這種設計模式:
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
框架能完成很多應用程序一句話調用就能完成的功能,元數據的功勞最大。系統中的實體對象的每個字段都有一張附加屬性表,參考下面的代碼定義:
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兩條元數據。在系統框架部分,代碼參考如下:
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框架,考慮給實體和實體的屬性增加元數據(自定義屬性),它會為系統的可擴展帶來諸多方便。