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


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM