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

1 緩存 Cache

系統中大量的用到緩存設計模式,對系統登入以後不變的數據進行緩存,不從數據庫中直接讀取。耗費一些內存,相比從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

 

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框架,考慮給實體和實體的屬性增長元數據(自定義屬性),它會爲系統的可擴展帶來諸多方便。

相關文章
相關標籤/搜索