解析大型.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框架,考虑给实体和实体的属性增加元数据(自定义属性),它会为系统的可扩展带来诸多方便。

时间: 2024-10-13 06:59:51

解析大型.NET ERP系统 高质量.NET代码设计模式的相关文章

解析大型.NET ERP系统架构设计 Framework+ Application 设计模式

我对大型系统的理解,从数量上面来讲,源代码超过百万行以上,系统有超过300个以上的功能,从质量上来讲系统应该具备良好的可扩展性和可维护性,系统中的功能紧密关联.除去业务上的复杂性,如何设计这样的一个协作良好的系统,搭建开发人员基础平台,一直是我研究的方向. SouceCounter(版本3.3.91.79)对源代码的统计信息如下: 下面来详细解析一下这个系统的设计架构,纯.NET技术架构方案,C/S WinForms系统. 系统分为Framework和Application两个部分,前者是框架(

解析大型.NET ERP系统 十三种界面设计模式

成熟的ERP系统的界面应该都是从模板中拷贝出来的,各类功能的界面有规律可遵循.软件界面设计模式化或是艺术性的创作,我认可前者,模式化的界面客户容易举一反三,降低学习门槛.除了一些小部分的功能界面设计特殊一些,ERP绝大部分的功能的界面都相似.以我接触和设计的ERP系统,总结常见的界面设计模式,供读者参考. 模式1 单据 Entry 常用于各种单据的输入界面,也可用于主文件/主档(客户,供应商,部门等)界面,参考下面的图片. 我在图中作了标识,A区是工具条按钮,所有的界面共享工具条按钮,接着是数据

解析大型.NET ERP系统 设计异常处理模块

异常处理模块是大型系统必备的一个组件,精心设计的异常处理模块可提高系统的健壮性.下面从我理解的角度,谈谈异常处理的方方面面.我的设计仅仅限定于Windows Forms,供参考. 1 定义异常类型 .NET 框架定义很多异常类型,ERP系统中根据实际的需要,我们再增加一些自定义的异常类型. 数据库访问异常:LLBL Gen Pro已经定义几种常见的异常类型,常见的异常类型及其作用简介. ORMConcurrencyException     并发异常,更新实体时实体已经被删除,删除时有约束无法删

解析大型.NET ERP系统 业务逻辑设计与实现

根据近几年的制造业软件开发经验,以我开发人员的理解角度,简要说明功能(Feature)是如何设计与实现的,供参考. 因架构的不同,技术实现上会有所差异,我的经验仅限定于Windows Form程序.   总体功能 1  系统支持多用户. 创建一个单实例(Singleton)的会话管理器SessionManager,用.NET Remoting部署在服务器端时,用DataTable保存登入的用户会话(Session:Login Id,User Id,Name,Login Time).客户端登入时,

解析大型.NET ERP系统 通用附件管理功能

大型系统具备一个通用的附件管理功能,对于单据中无法清晰表达的字段,用一个附件图片或附件文档表示是最好的方法了.比如物料清单附加一张CAD图纸,销售订单评审功能中附加客户的各种表格,通用附件功能对系统起到画龙点睛的作用.一图解千言,先来看一下界面设计模式,看起来和一般的数据输入功能相同. 首先是设计附件表,它的定义参考下面的代码. CREATE TABLE [dbo].[Attachment] ( [Index] [int] NOT NULL, [MasterTable] [nvarchar] (

解析大型.NET ERP系统 20条数据库设计规范

数据库设计规范是个技术含量相对低的话题,只需要对标准和规范的坚持即可做到.当系统越来越庞大,严格控制数据库的设计人员,并且有一份规范书供执行参考.在程序框架中,也有一份强制性的约定,当不遵守规范时报错误. 以下20个条款是我从一个超过1000个数据库表的大型ERP系统中提炼出来的设计约定,供参考.   1  所有的表的第一个字段是记录编号Recnum,用于数据维护 [Recnum] [decimal] (8, 0) NOT NULL IDENTITY(1, 1)   在进行数据维护的时候,我们可

解析大型.NET ERP系统 权限模块设计与实现

权限模块是ERP系统的核心模块之一,完善的权限控制机制给系统增色不少.总结我接触过的权限模块,以享读者. 1 权限的简明定义 ERP权限管理用一句简单的话来说就是:谁 能否 做 那些 事. 文句 含义 说明 谁 部门+岗位职责 也可以不与部门岗位绑定,省略角色定义. 能否 能(True) 否(False) 用0或1,true/false表示能否执行 做 增加/删除/修改/查询/统计/打印/过帐 权限对象 哪些 通用的/本人的/本组别的/本部门的/本公司的/其他的/多帐套的 范围:行政部的办公文具

解析大型.NET ERP系统 电子邮件系统帐户集成

为保证ERP系统的信息流准确快速的传递,需要给系统设计一个消息盒子机制.当系统中发生业务操作后,需要提醒下一个环节的操作人员,以保证ERP信息流快速准确传递.比如生产任务单(工作单,加工单,制单)过帐完成后,需要通知仓库准备材料供车间领料生产.消息盒子的界面大致如下所示: 消息盒子包含业务通知(Messages)和工作流审批(Workflow).业务通知比如采购人员下达采购订单PO后,需要通知仓库人员准备收货.工作流审批是以审批为基础的单据流程控制. 在实现消息盒子过程中,遇到一个客户需要将消息

解析大型.NET ERP系统 单据标准(新增,修改,删除,复制,打印)功能程序设计

ERP系统的单据具备标准的功能,这里的单据可翻译为Bill,Document,Entry,具备相似的工具条操作界面.通过设计可复用的基类,子类只需要继承基类窗体即可完成单据功能的程序设计.先看标准的销售合同单据界面: 本篇通过销售合同单据功能,依次讲解编程要点,供参考. 1 新增 Insert 窗体有二种状态,一种是编辑状态,别一种是数据浏览状态,区别在于编辑状态的窗体数据被修改(dirty),在窗体关闭时需要保存数据.点击工具条的新增(Insert)按钮,窗体进入编辑状态.新增状态需要对窗体所