系统中大量的用到缓存设计模式,对系统登入以后不变的数据进行缓存,不从数据库中直接读取。耗费一些内存,相比从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框架,考虑给实体和实体的属性增长元数据(自定义属性),它会为系统的可扩展带来诸多方便。