系統中大量的用到緩存設計模式,對系統登入以後不變的數據進行緩存,不從數據庫中直接讀取。耗費一些內存,相比從SQL Server中再次讀取數據要划算得多。緩存的基本設計模式參考下面代碼:html
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中,用戶修改這些參數值,須要提醒或是強制用戶退出從新登入。session
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)。ide
這是個很容易理解的設計模式,貴在堅持。咱們在讀取數據時,只讀取最少的可用的數據,避免讀取不須要的數據。用查詢語句表達以下,下面是沒有效率的查詢數據:性能
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,讀一筆數據的性能確定優於讀取未知筆數據記錄。
在使用對象時,只有當須要使用對象的方法或屬性,咱們才實例化對象。設計模式的代碼例子以下:
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); }
忽然想到這種模式就是系統緩存的實現方法。在類型中定義一個私有靜態變量,使用這個變量時咱們纔去初始化它的實例。延遲加載避免了系統啓動時建立全部緩存對象耗費的內存和時間,有些對象或許根本不會用到,也就不該該去建立。
好比用戶僅登入進系統,沒有作任何業務單據操做而後退出。若是在登入時就建立貨幣或付款條款的緩存,而用戶又沒有使用這些數據,影響了系統性能。
.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上找到源代碼和講解文章。
主要介紹不可變的數據字典的設計模式,先看一下性別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);
這樣設計模式解決了數據字典的文檔更新的煩惱。編寫源代碼同時就設計好了文檔,想知道數據字典的值,直接打開枚舉類型定義便可。
對業務邏輯的業務操做,遵照校驗-執行-驗證設計約定,來看一段代碼加深印象:
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)動做,最後驗證生成的憑證的借貸方是否一致,是否考慮到小數點進位致使的借貨方不一致,生成的憑證金額是否與原發票上的金額相等。
第六條講解是的業務記賬方法,第七條這裏講解的是公共框架與應用程序互動的方法。繼承的.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 在執行完成後。能夠對執行的結果重寫,也能夠調用派生類中的事件。
框架能完成不少應用程序一句話調用就能完成的功能,元數據的功勞最大。系統中的實體對象的每一個字段都有一張附加屬性表,參考下面的代碼定義:
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框架,考慮給實體和實體的屬性增長元數據(自定義屬性),它會爲系統的可擴展帶來諸多方便。