实体扩展属性系统 - 设计方案说明书
2012-02-17 16:16
519 查看
这篇设计文档是 12 月份写来参加公司的研发峰会的,自己倒是信心满满,不过最后还是没有入围。现在想想也没啥大用,所以贴出来,期待与园友交流。
文档有点长,没全部贴在博客中,有兴趣的可以下载附件中的 PDF。
附件:《实体扩展属性系统-系统设计说明书.pdf》
================= 分隔线 ======================
目录
前言... 4
1 背景与需求... 5
1.1 产品 721 客户化开发的需要... 5
1.2 实体动态列... 6
1.3 分离只读/视图属性... 6
1.4 提升框架性能... 6
1.5 支持 WPF 绑定... 6
1.6 其它需求... 7
2 分析... 8
2.1 主要功能需求... 8
2.2 非功能需求分析... 8
2.3 约束... 9
2.4 风险... 9
3 设计方案... 10
3.1 一些决策... 10
3.2 风险点验证... 10
3.2.1 支持 WPF 绑定... 10
3.2.2 性能关键点... 12
3.3 方案描述... 12
3.3.1 结构说明... 13
3.3.2 相关UML图... 14
3.3.3 如何支撑需求... 18
3.4 重点实现细节... 18
4 设计验证... 24
4.1 功能需求验证... 24
4.2 WPF绑定验证... 24
4.3 性能验证... 25
5 使用手册... 25
5.1 使用场景介绍(单元测试)... 25
5.1.1 属性默认值... 26
5.1.2 强制替换属性值... 27
5.1.3 属性值设置时的取消与强制替换... 27
5.1.4 引用实体属性的设置取消... 28
5.1.5 属性变更事件... 29
5.1.6 产品721扩展属性... 30
5.1.7 所有扩展属性的界面生成... 31
5.1.8 只读属性的使用方法... 31
5.1.9 只读扩展属性的使用方法... 32
5.1.10 运行时动态属性(动态列)... 33
5.1.11 序列化... 34
5.1.12 WPF绑定验证... 35
5.1.13 扩展属性ORM验证... 35
5.1.14 实体状态的相关属性... 35
5.2 代码生成 – CodeSnippets. 36
5.3 其它问题... 37
5.3.1 扩展属性的CLR属性编写注意点... 37
5.3.2 何时使用属性扩展,何时使用继承扩展?... 38
l 其中最重要一个问题是有时候无法给一个客户两个可选的的功能包,而为了解决这个问题,开发人员又不得不做大量的代码移植,把可选包的代码都移植到主干版本中,导致了临时代码过多,维护成本太大。
l 另外,我们的产品基于实体开发,为实现动态列的需求绕了许多路,最终决定使用数据表的模式来编写,同样造成大量重复代码,开发人员开发效率低下。
基于历史遗留的这些问题,我们设计了全新的属性系统。本系统设计完成之后,解决了许多历史遗留问题,也带来了许多意想不到的价值。例如:
l 支持简单地完成客户化开发中属性的扩展。
l 支持更简单地实现领域实体的动态属性(界面中的动态列,原来要100行代码,现在只要20行。)
l 视图属性分离(更好的可维护性)
l 属性性能提升(性能提升)
l 减少了序列化数据(传输效率提升,性能提升)
l 统一的属性接口(平台可提供更加强大的功能支持)
本次设计是在历史代码上进行重构,但是本质上是设计一个完全独立功能的子系统。本文从需求、分析、方案、实现、验证等角度说明了整个设计是如何完成的。并在最后,给出了系统的使用手册以帮助开发人员日常应用。
备注:
本文档中,为了方便起见,将会把“实体扩展属性系统”简称为 EMPS。(Entity Managed Property System,意为实体托管属性系统)
另外,文中说到的版本号:历史的OEA版本是2.5,升级到EMPS之后,OEA版本是2.6。
(关于产品开发中的721概念及OEA中的客户化设计,参见:《基于OEA框架的客户化设计(三) “插件式”DLL》。关于 OEA 的了解,参见:《OEA 框架演示 - 快过原型的产品开发》。)
客户化开发中,主要解决的问题是如何在客户化版本中对主干版本中的产品进行扩展。各种扩展一般都依托于底层的元数据,这些元数据描述整个系统。当我 们对元数据进行修改时,整个应用程序也就发生了相应的变化。这些产品的扩展可以简单分为:模块级别的扩展、实体级别的扩展、属性级别的扩展。模块的扩展在 此不进行讨论。
先说属性扩展:我们一般会对产品中定义好的类的属性进行以下扩展:添加一个属性、删除一个属性、修改一个属性。(所以,扩展并不只是意味着添加。) 添加属性意味着我们需要为已经定义完成的类添加一个额外的属性,这个属性可以映射到数据库,可以在产品界面中显示,行为和直接定义的属性是一致的。删除属 性则意味着,数据库中不再有对应的字段,界面不再显示。修改属性一般只会修改属性的各种元数据,例如,修改它映射数据库的字段元数据,修改它在界面中显示 的列的元数据等;这些修改其实已经在元数据的设计方案中解决,相关内容可以查看:《基于OEA框架的客户化设计(一) 总体设计》、《基于OEA框架的客户化设计(二) 元数据设计》以及《基于OEA框架的客户化设计(三) “插件式”DLL》。
实体的扩展一般可以通过继承的方法实现,当继承出新的子类后,在元数据中用它将原来的父类进行覆盖即可。有些时候,我们还会为某个类扩展一些聚合父 子关系,例如:我们可以为某一个建设项目扩展出其相关的合同列表,这样,原来只显示项目的界面中,就能紧接着显示每一个项目相应的合同列表。而这种聚合父 子关系的扩展,虽然是实体级别的添加,但是实质上是对实体添加新的一对多关系。也就是说,这种实体的扩展,可以转换为属性扩展,即在原有实体的基础上扩展 一个一对多关系的属性。
基于以上分析,我们知道,一个可扩展的属性系统,几乎是客户化软件产品运行时的最基础设施。
在 2.6 版本之前的 OEA,属性扩展主要使用继承的方式来实现。简单地说,就是继承需要扩展的实体,添加新的属性,然后使用这个实体替换掉原来的类。该方案主要是为了实现属 性的添加,但是属性的删除以及修改都是通过修改属性的元描述来实现的。这样的方式导致了许多问题:属性的删除只是删除了界面,而数据库、运行时实体也都还 存在该属性;属性的修改不能修改属性中的行为代码;重点说下属性的添加造成的缺点:
经常需要对某个类扩展一两个属性,而现在只能继承出子类,同时把父类隐藏起来,或者直接覆盖父类,用进来比较复杂; 同时,类型变多,开发人员的学习成本,维护成本都随之变大。
更重要的是,.NET 中 CLR 单继承体系的限制,使得通过继承无法实现这样的扩展: 两个独立的扩展包“2”以可选的形式对主包“7”进行扩展,也就是说,产品 721 客户化开发中,两个“2”的扩展包是两个单独的程序集,但是单继承的限制,我们不能同时使用它们。对于这种情况,我们目前的处理方式是把两个“2”的包都 放到了主包中,而使用元数据的方式对不需要的功能来进行隐藏,这种实现方式是临时的、错误的。
如果有了扩展属性,我们则可以在任意实体上扩展各种新的属性,界面也就相应地成了“动态”列。
如果有了扩展属性,我们则可以把这个只读属性都放到一个单独的类中去为这个实体做扩展,这样,就可以得到更简洁、结构更清晰的代码。
在产品 721 开发中,常常在 “1” 的客户化版本中需要删除 “2”版本中为“7”扩展的属性,这时,需要支持属性的反扩展(或叫反注册)。
l 获取属性值来源
由于目前 OEA 框架中的实体是分布式对象,我们常常需要在实体属性改变时分辨属性值的来源:是数据库,还是UI界面,还是来自程序中的其它代码。
l 定制序列化的数据
实体属性被框架管理后,可以很轻易地实现各种数据格式的序列化。
l 需要支持属性值的验证、强制、更改通知等事件通知。
l 元数据重载
属性的一切行为都将以回调的形式存放在元数据中。而元数据是可以被重载的。这样,子类就才重写这些行为。同时,我们就可以在进行产品客户化的时候,为属性重新定制这些行为。
最后,可以看一下在《实体扩展属性方案分析脑图》脑图文档中整理出来的需求概况图,这些需求都是历史版本中所不能支持的:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/d50a1270e430d2a6d6818e8fb627e03a.png)
图1. 实体扩展属性需求列表
l 721客户化开发中的属性扩展
l 属性托管(受框架管理)
意思是需要为上层框架提供统一维护属性值的功能。
l 动态列
实体属性可以说是实体设计中最重要的部分。而它的性能好坏则关系到系统中每一个实体的每一个属性,这些属性都直接关系到应用的性能。简单地说,如果属性系统慢,上层应用的性能必然会慢。换句话说,属性系统的代码开发是对性能十分敏感的,在核心代码上需要十分谨慎。
2.5 版本的OEA框架使用的属性主要还是 .NET 中的原生 CLR属性系统 + CSLA 开源框架中的属性系统。主要是为了支持属性的统一管理。而本次设计,可以对系统带来许多的新功能和支持,加之原有系统的属性性能并没有构成应用层开发的性 能问题,所以,一定的性能消耗是可以接受的。
对这项的要求是:
使用同样的代码,和历史属性系统进行属性测试对比,耗时不能超过原有的120%。
比较简单,也比较严格。一旦不满足此项,整个设计不可以被使用。
l 独立性
虽然实体扩展属性系统是作为 OEA 框架的一个重要组成部分,但是托管属性、扩展属性的需求在开发过程中常常会碰到。所以我们需要把实体扩展属性系统设计为一个独立的 DLL,这样,它就可以在非 OEA平台的环境中使用。
l 可扩展性
EMPS的可扩展性并不是指该系统带来的属性的可扩展性(这其实是EMPS的功能需求),而是指属性系统本身需要进行一些扩展。
当前,OEA框架中以产品元数据为整个框架的基础设施。也就是说,OEA 框架中有管理应用中所有元数据的功能。而由图1中的需求列表可以看到,EMPS也需要元数据的支持,例如属性的默认值。但是,独立性中已经要求EMPS被 设计为一个完全独立的模块,也就是说EMPS完全不依赖 OEA。那么,这些属性的元数据如何支持使用 OEA 来进行保存呢?这,同样是EMPS 设计过程中需要特殊考虑的一个扩展点。
l 易用性
此项为框架设计必须考虑的一个非功能需求。
原来的OEA的ORM中支持使用OEAORM及EntityFramework4.1(CodeFirst)两种模式,但是这两种ORM当前无疑都只支持对CLR属性的映射。而扩展属性是没有CLR属性包装器的,但是这些扩展属性同样需要映射数据库。
也就是说:如果EMPS开发完成,要映射新的扩展属性,必须要修改当前OEAORM模块。同时,无法再支持EntityFramework4.1了(EFCodeFirst基于CLR属性来进行映射)。
l 原有属性功能的兼容
2.5 版本的OEA使用的属性主要还是 .NET 中的原生 CLR属性系统 + CSLA 开源框架中的属性系统。这些属性中已经写了非常多的代码。属性的 Get 获取器及 Set 设置器中的代码,可谓五花八门。这些都必须在新的属性系统中被完全兼容,否则,必须导致业务功能出现问题。
l 大量历史代码的修改
由于本次设计本质上是一次在历史版本上的重构,而产品开发截止到目前,已经产生了几万行的历史代码,其中的实体属性也是几千个。重构如此底层的设 计,在尽量保证应用层 API 不变的前提下,也必然会造成较多的修改,同时,很可能会引起比较多的BUG。这是一个必须考虑的约束条件。
由非功能需求的描述中知道,性能是至关重要的。关系到整个设计是否可用。但是,最终开发出来的模块性能,在设计时很难测量的。对于这个风险的规避使 用以下方案:分析历史属性系统的关键性能影响点,在设计稿完成后,理论上检查这些关键点是否能在新设计出来的属性系统下运行良好。
l 支持WPF绑定
这是一个技术难关。
当前我们只是使用了 WPF 中直接绑定CLR属性的方案。如何能让我们在客户化版本的程序集中扩展的扩展属性也支持WPF绑定,成为了一个技术上的难题。
对这点的规避很简单,在整个设计开始之前,先分析WPF绑定中的内部机制,解决这个问题后,才能开始其它的设计。
在设计过程中尽量考虑功能上与原属性系统保持兼容,接口上保持一致。但是当无法兼容或者无法保持一致的接口时,可以不兼容。但是这些不兼容的设计点,都需要记录下来,当设计完成后,逐个修改。如果改动较大,则使用组内的重构工具完成。
经过查阅MSDN及搜索出的网络资源,发现WPF中的绑定机制支持绑定DataTable数据表类型,而表中的字段则是动态的,根据结果数据的变化而变化。所以只要搞清楚DataTable是如何被WPF绑定支持的,那么EMPS也可以使用同样的机制进行绑定。
以下是WPF中DataTable的绑定机制分析:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/9e4816e51683633485089f6f71822727.png)
图2. WPF中DataTable支持绑定的核心类型分析
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/c15f29d9c09205f8ae08d72702bc36e0.png)
图3. WPF中为DataTable生成视图模型的流程图
重点在于DataTable 实现 IListSource接口,并构造动态的视图动态类型 DataRowView并使其实现ICustomTypeDescriptor。(详细过程参见这篇文章:《OEA 扩展属性系统 - 任意适配 WPF Binding 的设计分析》,以及本系列中的文档:《任意适配 WPF Binding 的设计分析》。)
搞清楚了整个设计及创建流程,那么其实在设计EMPS时,支持这个机制就可以了。
3.2.2 性能关键点
需要分析,历史框架中的属性系统(CSLA托管属性系统)在做到托管属性的同时,是如何保证性能的呢?
其实,它其中属性的核心重点在于使用强类型的FieldData<T>来存储每一个属性,并使用定长的属性值的数组来存放:
private IFieldData[] _fieldData;
这样的好处在于强类型保证了没有装箱拆箱操作,同时定长的数组支持了以O(1)的复杂度来查找指定属性:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/bf79c3a6725551151e81df56dabcf08b.png)
但是,这样搜索属性的前提是属性值数组定长,而一个实体类型到底有多少个属性,是在编译期已经完全确定下来的。换句话说,在这个数组初始化时必须知道固定的属性个数,这违背了属性可扩展的需求,这也是为什么使用这个属性系统很难做到扩展的原因。
当然,在对其进行较大改动的前提下,也不是不可能。但是考虑到CSLA是个开源框架,其满足需求与我们的需求有较大的区别,代码比较臃肿,也无法实现我们所需要的一些功能,对它做大型的改动不如重新做一个完全符合需求的托管属性框架。
经过之前的分析,可以想到,要得到较高性能的托管属性系统,最好也是使用“强类型存储属性值”加“定长数组”的方案。但是如何支持属性的扩展呢?“划分属性定义期”是个较好的解决方案。之后的主体设计中会对这个方案进行详细的描述。
3.3.1 结构说明
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/6bf61fce9b72c6e1e12bc993ecd6f4f3.png)
图4. EMPS结构说明
脑图比较简单,其中的具体内容可以参考脑图《扩展属性方案》。这里只做简要说明:
l 静态结构
总体上,静态结构比较简单,主要分为两个层次。底层是抽象的属性元数据提供子系统,而另一层则是依赖于前者而构建的EMPS核心:运行时扩展属性子系统。
提取抽象的属性元数据提供系统是为了使元数据的存储、提供都抽象化,后面可以和 OEA 中的元数据存储模块进行适配。
而核心的EMPS则实现了整个的托管属性。后面将会对其以类图的形式重点说明。
l 动态结构
在这里比较特殊地提出了属性生命周期的概念。属性的生命周期规定了属性被定义(或者被反注册)的时期(可能叫定义期会比较正确。),这里主要有编译期、启动期、运行期。
l 编译期
此阶段中定义的属性主要包括使用代码编写的一般属性、扩展属性。当然,也包括“2”和“1”的扩展包中编写的一些对“7”的包中实体类进行扩展的扩展属性。
定义属性时,一同指定它对应的元数据。
l 启动期
此阶段主要以客户化定义的方式来对编译期属性及其相应的元数据进行修改。
l 运行期
该阶段主要用于附加运行时动态属性。
这些动态属性一般只用于显示,它们会影响界面的生成。属性的扩展和删除,要在生成控件之前就能确定,否则,界面没有对应的列。
由于影响界面生成,所以需要为其指定OEA框架中对应的界面元数据。如果不指定,则使用默认元数据。不过这些元数据的设计会在OEA框架中完成,与EMPS的设计无关。
在这个阶段中扩展的附加属性,不会与服务端程序有任何关系。也就是说,不需要为这些扩展属性定义 ORM 等服务端元数据。当然了,这些属性的数据也不需要序列化后在网络上进行传输。
划分出这几个周期的主要原因:使得可以判断出某个实体的编译期、启动期属性列表长度。这是因为,编译期和启动期 已经定义、修改或者客户化的属性,当程序进入运行时后是不会再发生改变的。而这些属性占据了应用开发的95%以上。所以我们只要知道了编译期启动期属性的 长度,也就意味着可以使用O(1)检索的数组来存放,而不是更慢的List/HashTable,保证了这些属性的性能。而对于运行时属性来说,虽然它的 长度不能固定,根据业务场景而变化,但是使用情况较少,可以不考虑性能。
3.3.2 相关UML图
完整的UML图,参见:《实体扩展属性UML设计图》。下面将挑选重点进行说明。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/c58443164d9796472a662edef96df362.png)
图5.扩展属性核心类结构概要设计图
这张是实现扩展属性的核心类结构概要设计图,其中主要包含 ManagedPropertyObject、ManagedProperty、ManagedPropertyField、ManagedPropertyMeta等。由于是概要设计图,其中的方法、属性等相对实现完成后的系统来比,肯定不完整,但是它的作用主要是说明整个设计的核心思想。其中:
ManagedProperty 表示托管属性,每定义一个托管属性,系统都会生成一个此类型的对象用于标记。获取、设置属性的值时,都需要提供此标记来进行检索。
ManagedPropertyMeta 表示托管属性元数据,其中提供了许多信息,例如:默认值、是否只读、属性变更逻辑回调等;这些元数据对属性值的获取、设置的逻辑都有着比较大的影响。
ManagedPropertyField 表示某个对象中某个托管属性对应的值。其实这个类后期在实现时会被定义为泛型类,这样,值的存储就不是object而是强类型的,不需要装箱拆箱操作。
ManagedPropertyObject 表示拥有托管属性的对象基类(实体),其中定义了根据ManagedProperty来 获取、设置值的接口,这使得该对象能够象一般对象一样获取、存储各种值。同时,它也提供了统一处理所有托管属性值的接口,此类统一处理的接口在应用开发时 很少用到,主要给上层的框架使用。上层框架可以应用这些接口完成以下的框架任务:统一的对象值拷贝、统一的序列化、检索特定类型的值等,这样的值的获取、 设置速度,远比反射要快。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/0df78b951ca90631410b076fd77b3081.png)
图6. 扩展属性仓储概要设计图
这张图说明整个系统中的托管属性都是被系统中的单例对象 ManagedPropertyRepository 给管理起来的,为了给上层提供更方便的查询功能,也方便存储,它使用 TypeIndicators类来存储某个实体类型的属性列表。TypeIndicators这个类也负责为上层提供查询:某一个类型已经定义好的属性列表、某一类型及其所有父类定义的所有属性的联合属性列表。同时,这个类中的属性都会生成在类型中的属性的索引,这样,在获取属性值时就可以使用这个索引在属性值数组中进行属性值的查找。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/7e288e5efae529d8044e5688abfa93b7.png)
图7.扩展属性元数据概要设计图
前面提到过,为了保证 EMPS的独立性,我们需要把托管属性元数据的数据获取方案抽象化,这里的 IPropertyMetaProvider就是抽象的元数据提供器接口。而上层的OEA框架则会实现自己的提供器 OEAPropertyMetaProvider,并通过自己的元数据模块中的信息(例如图中的OEAPropertyMeta)来为托管属性提供元数据。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/49dfcc1fd449dcaf86b3369a9463b9c7.png)
图8. 扩展属性实体实现WPF绑定相关概要设计图
这张图看上去会比较眼熟?没错,它和图2中的WPF支持DataTable绑定的类图比较相似。主要也是让 EntityList 实现 IListSource接口,并添加 EntityView 类实现 ICustomTypeDescriptor 接口,这样,就可以实现动态属性的WPF绑定了。
3.3.3 如何支撑需求
主要的设计方案及类图看完之后,我们需要考虑,整个方案是否能支撑起前面说到的所有需求。下图是对整个需求的可支撑性进行分析。相关内容可以参见:《实体扩展属性方案分析脑图》,在此不再赘述。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/f0f7a385a58dcf19eab56e1d4856a90e.png)
图9. 设计方案对需求的支持度分析。
首先,先来看看最终完成的代码中,最核心部分的代码结构图:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/7325bf48b154158978a3c90beaf29999.png)
图10. 核心代码结构图
整个结构的实现与设计相差无几。接下来,说明一些相对重要的代码:
l 先是ManagedPropertyObject中的属性值获取、设置相关代码:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/911760fbe20447767303f46ca4582af6.png)
前面的设计方案中提到,这个类主要作为所有实体类的基类,提供值的获取、设置等。而这个类其实是把属性值的管理都放到了内部的一个类ManagedPropertyObjectFieldsManager 中:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/09a58094f3046847951a15b887510d3d.png)
并把相关的操作都代理到这个类上:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/6c33ee91628401f10861c9e942e68e24.png)
而 ManagedPropertyObjectFieldsManager则实现了这些逻辑的核心代码。其中,它的私有字段定义如下:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/036f1a1692c002ff8cefceea5d2beb5a.png)
可以看到,编译期、启动期属性值与运行期属性值被分开存放。前者使用数组,构造函数直接初始化,而后者则在需要时才会被序列化。还注意到,它继承自CustomSerializationObject,使得整个属性值列表是可以被自定义序列化的。下面,是泛型的属性值获取与设置逻辑:
internal TPropertyType GetProperty<TPropertyType>(ManagedProperty<TPropertyType> property)
{
var useDefault = true;
TPropertyType result = default(TPropertyType);
if (property.IsReadOnly)
{
result = (property as ManagedProperty<TPropertyType>).ProvideReadOnlyValue(this._owner);
useDefault = false;
}
else
{
if (property.LifeCycle == ManagedPropertyLifeCycle.CompileOrSetup)
{
var field = this._compiledFields[property.TypeCompiledIndex] as ManagedPropertyField<TPropertyType>;
if (field != null)
{
result = field.Value;
useDefault = false;
}
}
else
{
if (this._runtimeFields != null)
{
IManagedPropertyField f;
if (this._runtimeFields.TryGetValue(property, out f))
{
var field = f as ManagedPropertyField<TPropertyType>;
result = field.Value;
useDefault = false;
}
}
}
}
var meta = property.GetMeta(this);
if (useDefault) result = meta.DefaultValue;
result = meta.CoerceGetValue(this._owner, result);
return result;
}
internal void SetProperty<TPropertyType>(ManagedProperty<TPropertyType> property, TPropertyType value, ManagedPropertyChangedSource source)
{
ForceNotReadOnly(property);
var meta = property.GetMeta(this);
bool cancel = meta.RaisePropertyChanging(this._owner, ref value, source);
if (cancel) return;
var hasOldValue = false;
TPropertyType oldValue = default(TPropertyType);
ManagedPropertyField<TPropertyType> field = null;
//这个 if 块中的代码:查找或创建对应 property 的 field,同时记录可能存在的历史值。
if (property.LifeCycle == ManagedPropertyLifeCycle.CompileOrSetup)
{
field = this._compiledFields[property.TypeCompiledIndex] as ManagedPropertyField<TPropertyType>;
if (field == null)
{
//不管是不是默认值,都进行存储。
//不需要检测默认值更加快速,但是浪费了一些小的空间。
//默认值的检测,在 GetNonDefaultPropertyValues 方法中进行实现。
field = property.CreateField();
this._compiledFields[property.TypeCompiledIndex] = field;
}
else
{
oldValue = field.Value;
hasOldValue = true;
}
}
else
{
if (this._runtimeFields == null)
{
this._runtimeFields = new Dictionary<IManagedProperty, IManagedPropertyField>();
}
else
{
IManagedPropertyField f;
if (this._runtimeFields.TryGetValue(property, out f))
{
field = f as ManagedPropertyField<TPropertyType>;
oldValue = field.Value;
hasOldValue = true;
}
}
if (field == null)
{
field = property.CreateField();
this._runtimeFields.Add(property, field);
}
}
field.Value = value;
if (!hasOldValue) { oldValue = meta.DefaultValue; }
if (!object.Equals(oldValue, value))
{
//发生 Meta 中的事件
var args = meta.RaisePropertyChanged(
this._owner, oldValue, value, source
);
//发生事件
this._owner.RaisePropertyChanged(args);
}
}
可以看到,编译期属性主要通过一维数组进行存放,数组中每一个元素都是强类型的泛型对象 ManagedPropertyField<TPropertyType>。
另外,要注意的是,该类提供了同样的非泛型接口:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/1d97f063e9231268f4b14cc939867212.png)
非泛型方法主要是为上次框架提供,其中主要考虑装箱拆箱操作的性能消耗。(关于接口加泛型类的底层框架设计方案,参见:《重构实践:体验interface的威力(一)》、《重构实践:体验interface的威力(二)》。)
GetProperty、SetProperty 方法是对性能最敏感的两个方法,其实现必须特别小心,其内部调用的每一个方法,如 ManagedProperty.GetMeta(ManagedPropertyObject owner)、ManagedPropertyMetadata.RaisePropertyChanged等,也都必须要做特别的优化,需要考虑到装箱 拆箱、属性检索、不构造多余对象等。具体代码设计参考实际工程中的代码,在此不做过多描述。
l 注册属性(及反注册属性)的方法位于 ManagedPropertyRepository类中。
其主要的职责是构造ManagedProperty对象,并计算它的一些重要属性,例如:用于属性快速Hash的GlobalIndex、用于编译期属性检索值时使用的数组下标TypeIndex等。
不过,我们还是为它加了相应的单元测试,这个在后面会有描述。
具体内容可以参见:《实体扩展属性系统性能测试报告》。这里说明一下最终结论:
“
性能结论:
新的 OEA 托管属性系统,在带来众多新功能的同时,不但没有降低原有性能,反而因为优化掉无用的代码,使得速度提升。故可以放心使用该属性系统。
”
单元测试所使用的实体类包含下图中的这些类:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/1af4d8ceaf6b86e103052529cacd7df6.png)
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/c49a68f3c0483ade99edff04df01014a.png)
右图是所涉及到的所有单元测试。
“……………………内容较多,省略,有兴趣的可以看附件中的文档………………”
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/1fe4ec4502670552d225ea7db3eed3a5.png)
导入VS后,只要输入OEAP……,VS就支持这些代码片段的生成,如:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/bdbc9f9a0d212fd5874c117105a90144.png)
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/d730c48afe6cd4e08471a3d2c0783718.png)
使用EMPS定义的属性,如果不是扩展属性,都会定义一个对应的CLR属性包装器,如:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/32fa931918c8526ef2c4ee292cd396ab.png)
注意,CLR属性内,不能添加任何代码,所有需要对Code属性的Get、Set的定制代码,都需要以回调的形式编写在EMPS中,如:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/1430cb4bcfc78fdec4de8b1ee328c999.png)
原因是界面框架、ORM框架、WPF绑定等框架内容都不会调用CLR属性,而是直接调用GetProperty、SetProperty方法,而CLR中的代码只是为了方便类库的使用。
5.3.2 何时使用属性扩展,何时使用继承扩展?
EMPS虽然可以直接对某个实体类型进行属性的扩展,但是我们依然老的方案,即使用CLR类继承机制扩展旧的实体。那么,我们需要特别注意两种方案的区别:
1. 属性扩展是直接对指定的领域实体进行扩展,一旦扩展,该领域实体类在整个应用程序中的属性都被扩展。
2. 而继承扩展则需要用于不同的领域实体中。
简单地说,当你想在应用程序中扩展出一个新的领域实体类或者做一个全新的界面时,则使用继承扩展。而当在做客户化时,希望对现有的领域实体类进行完全扩展时,则应该使用EMPS来进行属性扩展。
文档有点长,没全部贴在博客中,有兴趣的可以下载附件中的 PDF。
附件:《实体扩展属性系统-系统设计说明书.pdf》
================= 分隔线 ======================
目录
前言... 4
1 背景与需求... 5
1.1 产品 721 客户化开发的需要... 5
1.2 实体动态列... 6
1.3 分离只读/视图属性... 6
1.4 提升框架性能... 6
1.5 支持 WPF 绑定... 6
1.6 其它需求... 7
2 分析... 8
2.1 主要功能需求... 8
2.2 非功能需求分析... 8
2.3 约束... 9
2.4 风险... 9
3 设计方案... 10
3.1 一些决策... 10
3.2 风险点验证... 10
3.2.1 支持 WPF 绑定... 10
3.2.2 性能关键点... 12
3.3 方案描述... 12
3.3.1 结构说明... 13
3.3.2 相关UML图... 14
3.3.3 如何支撑需求... 18
3.4 重点实现细节... 18
4 设计验证... 24
4.1 功能需求验证... 24
4.2 WPF绑定验证... 24
4.3 性能验证... 25
5 使用手册... 25
5.1 使用场景介绍(单元测试)... 25
5.1.1 属性默认值... 26
5.1.2 强制替换属性值... 27
5.1.3 属性值设置时的取消与强制替换... 27
5.1.4 引用实体属性的设置取消... 28
5.1.5 属性变更事件... 29
5.1.6 产品721扩展属性... 30
5.1.7 所有扩展属性的界面生成... 31
5.1.8 只读属性的使用方法... 31
5.1.9 只读扩展属性的使用方法... 32
5.1.10 运行时动态属性(动态列)... 33
5.1.11 序列化... 34
5.1.12 WPF绑定验证... 35
5.1.13 扩展属性ORM验证... 35
5.1.14 实体状态的相关属性... 35
5.2 代码生成 – CodeSnippets. 36
5.3 其它问题... 37
5.3.1 扩展属性的CLR属性编写注意点... 37
5.3.2 何时使用属性扩展,何时使用继承扩展?... 38
前言
在产品线开发中,支持产品的客户化在产品规模化开发中是非常重要的一部分。而客户化中的非常重要的一部分则是属性值的客户化,包括属性值的添加、删 除、修改及属性对应的界面的客户化。由于产品对属性值的扩展方案一直是使用类继承的方案来完成,导致了产品出现了许多的问题:l 其中最重要一个问题是有时候无法给一个客户两个可选的的功能包,而为了解决这个问题,开发人员又不得不做大量的代码移植,把可选包的代码都移植到主干版本中,导致了临时代码过多,维护成本太大。
l 另外,我们的产品基于实体开发,为实现动态列的需求绕了许多路,最终决定使用数据表的模式来编写,同样造成大量重复代码,开发人员开发效率低下。
基于历史遗留的这些问题,我们设计了全新的属性系统。本系统设计完成之后,解决了许多历史遗留问题,也带来了许多意想不到的价值。例如:
l 支持简单地完成客户化开发中属性的扩展。
l 支持更简单地实现领域实体的动态属性(界面中的动态列,原来要100行代码,现在只要20行。)
l 视图属性分离(更好的可维护性)
l 属性性能提升(性能提升)
l 减少了序列化数据(传输效率提升,性能提升)
l 统一的属性接口(平台可提供更加强大的功能支持)
本次设计是在历史代码上进行重构,但是本质上是设计一个完全独立功能的子系统。本文从需求、分析、方案、实现、验证等角度说明了整个设计是如何完成的。并在最后,给出了系统的使用手册以帮助开发人员日常应用。
备注:
本文档中,为了方便起见,将会把“实体扩展属性系统”简称为 EMPS。(Entity Managed Property System,意为实体托管属性系统)
另外,文中说到的版本号:历史的OEA版本是2.5,升级到EMPS之后,OEA版本是2.6。
1 背景与需求
本节主要说明整个系统设计之初,设计的背景及最终整理出来的需求列表。这些需求是前期不断收集、累积的结果。接下来,将会详细说明一些主要的需求:1.1 产品 721 客户化开发的需要
部门的几个产品都是基于 OEA 平台开发的。OEA 平台主要解决产品开发模式下客户化开发、以及在产品开发过程中如何提高开发效率两大问题。(关于产品开发中的721概念及OEA中的客户化设计,参见:《基于OEA框架的客户化设计(三) “插件式”DLL》。关于 OEA 的了解,参见:《OEA 框架演示 - 快过原型的产品开发》。)
客户化开发中,主要解决的问题是如何在客户化版本中对主干版本中的产品进行扩展。各种扩展一般都依托于底层的元数据,这些元数据描述整个系统。当我 们对元数据进行修改时,整个应用程序也就发生了相应的变化。这些产品的扩展可以简单分为:模块级别的扩展、实体级别的扩展、属性级别的扩展。模块的扩展在 此不进行讨论。
先说属性扩展:我们一般会对产品中定义好的类的属性进行以下扩展:添加一个属性、删除一个属性、修改一个属性。(所以,扩展并不只是意味着添加。) 添加属性意味着我们需要为已经定义完成的类添加一个额外的属性,这个属性可以映射到数据库,可以在产品界面中显示,行为和直接定义的属性是一致的。删除属 性则意味着,数据库中不再有对应的字段,界面不再显示。修改属性一般只会修改属性的各种元数据,例如,修改它映射数据库的字段元数据,修改它在界面中显示 的列的元数据等;这些修改其实已经在元数据的设计方案中解决,相关内容可以查看:《基于OEA框架的客户化设计(一) 总体设计》、《基于OEA框架的客户化设计(二) 元数据设计》以及《基于OEA框架的客户化设计(三) “插件式”DLL》。
实体的扩展一般可以通过继承的方法实现,当继承出新的子类后,在元数据中用它将原来的父类进行覆盖即可。有些时候,我们还会为某个类扩展一些聚合父 子关系,例如:我们可以为某一个建设项目扩展出其相关的合同列表,这样,原来只显示项目的界面中,就能紧接着显示每一个项目相应的合同列表。而这种聚合父 子关系的扩展,虽然是实体级别的添加,但是实质上是对实体添加新的一对多关系。也就是说,这种实体的扩展,可以转换为属性扩展,即在原有实体的基础上扩展 一个一对多关系的属性。
基于以上分析,我们知道,一个可扩展的属性系统,几乎是客户化软件产品运行时的最基础设施。
在 2.6 版本之前的 OEA,属性扩展主要使用继承的方式来实现。简单地说,就是继承需要扩展的实体,添加新的属性,然后使用这个实体替换掉原来的类。该方案主要是为了实现属 性的添加,但是属性的删除以及修改都是通过修改属性的元描述来实现的。这样的方式导致了许多问题:属性的删除只是删除了界面,而数据库、运行时实体也都还 存在该属性;属性的修改不能修改属性中的行为代码;重点说下属性的添加造成的缺点:
经常需要对某个类扩展一两个属性,而现在只能继承出子类,同时把父类隐藏起来,或者直接覆盖父类,用进来比较复杂; 同时,类型变多,开发人员的学习成本,维护成本都随之变大。
更重要的是,.NET 中 CLR 单继承体系的限制,使得通过继承无法实现这样的扩展: 两个独立的扩展包“2”以可选的形式对主包“7”进行扩展,也就是说,产品 721 客户化开发中,两个“2”的扩展包是两个单独的程序集,但是单继承的限制,我们不能同时使用它们。对于这种情况,我们目前的处理方式是把两个“2”的包都 放到了主包中,而使用元数据的方式对不需要的功能来进行隐藏,这种实现方式是临时的、错误的。
1.2 实体动态列
软件开发中常常遇到动态列的需求:表格中的数据的列是根据数据本身自动生成的,这对于基于领域实体类型、基于非动态类型的技术框架来开发的系统来 说,要实现动态列基本上不可能。所以往往应用程序会另辟捷径,使用 DataTable 来重新组装数据后再显示。这导致两种模式同时存在于一个系统中,同样的代码会重复出现,增加维护成本。界面的代码不一致,也加大了界面自动生成的困难。如果有了扩展属性,我们则可以在任意实体上扩展各种新的属性,界面也就相应地成了“动态”列。
1.3 分离只读/视图属性
实体设计中常常会添加一些只读的属性,它的值是使用实体当前的值经过计算后得出。在 OEA 中,实体被设计为分布式对象(简单地说,就是客户端和服务端重用一套实体代码。可以参见CSLA框架设计书籍《Expert C# 2008 Business Objects》。),这些分布式对象被直接绑定到界面上。为了界面显示的需要,常常会为它们添加许多只读的视图属性,这样就导致了视图属性过多,混杂在领域实体的代码中,污染了代码,加大维护难度。如果有了扩展属性,我们则可以把这个只读属性都放到一个单独的类中去为这个实体做扩展,这样,就可以得到更简洁、结构更清晰的代码。
1.4 提升框架性能
对于框架开发来说,常常需要在框架中对实体的属性做统一的处理,来向应用层提供强大的功能支持。如果使用一般的实体设计,那么属性值的获取、设置都 不可避免地要使用到反射。而大量的属性值操作将会意味着较差的性能。如果有了托管属性,则在框架层面能够使用和应用一致的属性 API 来操作属性,不再使用反射,速度可以有不少提升。1.5 支持 WPF 绑定
一般情况下,我们使用 WPF 绑定时,都是直接绑定到 CLR 托管属性上。但是,如果使用扩展属性的话,并不是所有属性都会有一个 CLR 属性封装器。所以,这些扩展属性必须支持 WPF 绑定也是我们的需求之一。1.6 其它需求
l 支持属性反扩展在产品 721 开发中,常常在 “1” 的客户化版本中需要删除 “2”版本中为“7”扩展的属性,这时,需要支持属性的反扩展(或叫反注册)。
l 获取属性值来源
由于目前 OEA 框架中的实体是分布式对象,我们常常需要在实体属性改变时分辨属性值的来源:是数据库,还是UI界面,还是来自程序中的其它代码。
l 定制序列化的数据
实体属性被框架管理后,可以很轻易地实现各种数据格式的序列化。
l 需要支持属性值的验证、强制、更改通知等事件通知。
l 元数据重载
属性的一切行为都将以回调的形式存放在元数据中。而元数据是可以被重载的。这样,子类就才重写这些行为。同时,我们就可以在进行产品客户化的时候,为属性重新定制这些行为。
最后,可以看一下在《实体扩展属性方案分析脑图》脑图文档中整理出来的需求概况图,这些需求都是历史版本中所不能支持的:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/d50a1270e430d2a6d6818e8fb627e03a.png)
图1. 实体扩展属性需求列表
2 分析
由于前面已经把需求整理得比较明朗了。那么这里,我们首先要分析出主要需求、约束及相关的风险等。(关于框架设计的整个过程,可以参考这篇文章:《框架模块设计经验总结》。)2.1 主要功能需求
其实在图一中已经把需求按照优先级别进行了划分,后面的整个设计将会围绕这些需求进行。其中,最主要的功能性需求是以下三个。而设计目标则是至少实现以下三个需求,其它需求则按优先级尽可能实现。l 721客户化开发中的属性扩展
l 属性托管(受框架管理)
意思是需要为上层框架提供统一维护属性值的功能。
l 动态列
2.2 非功能需求分析
l 运行时性能实体属性可以说是实体设计中最重要的部分。而它的性能好坏则关系到系统中每一个实体的每一个属性,这些属性都直接关系到应用的性能。简单地说,如果属性系统慢,上层应用的性能必然会慢。换句话说,属性系统的代码开发是对性能十分敏感的,在核心代码上需要十分谨慎。
2.5 版本的OEA框架使用的属性主要还是 .NET 中的原生 CLR属性系统 + CSLA 开源框架中的属性系统。主要是为了支持属性的统一管理。而本次设计,可以对系统带来许多的新功能和支持,加之原有系统的属性性能并没有构成应用层开发的性 能问题,所以,一定的性能消耗是可以接受的。
对这项的要求是:
使用同样的代码,和历史属性系统进行属性测试对比,耗时不能超过原有的120%。
比较简单,也比较严格。一旦不满足此项,整个设计不可以被使用。
l 独立性
虽然实体扩展属性系统是作为 OEA 框架的一个重要组成部分,但是托管属性、扩展属性的需求在开发过程中常常会碰到。所以我们需要把实体扩展属性系统设计为一个独立的 DLL,这样,它就可以在非 OEA平台的环境中使用。
l 可扩展性
EMPS的可扩展性并不是指该系统带来的属性的可扩展性(这其实是EMPS的功能需求),而是指属性系统本身需要进行一些扩展。
当前,OEA框架中以产品元数据为整个框架的基础设施。也就是说,OEA 框架中有管理应用中所有元数据的功能。而由图1中的需求列表可以看到,EMPS也需要元数据的支持,例如属性的默认值。但是,独立性中已经要求EMPS被 设计为一个完全独立的模块,也就是说EMPS完全不依赖 OEA。那么,这些属性的元数据如何支持使用 OEA 来进行保存呢?这,同样是EMPS 设计过程中需要特殊考虑的一个扩展点。
l 易用性
此项为框架设计必须考虑的一个非功能需求。
2.3 约束
l ORM功能的修改原来的OEA的ORM中支持使用OEAORM及EntityFramework4.1(CodeFirst)两种模式,但是这两种ORM当前无疑都只支持对CLR属性的映射。而扩展属性是没有CLR属性包装器的,但是这些扩展属性同样需要映射数据库。
也就是说:如果EMPS开发完成,要映射新的扩展属性,必须要修改当前OEAORM模块。同时,无法再支持EntityFramework4.1了(EFCodeFirst基于CLR属性来进行映射)。
l 原有属性功能的兼容
2.5 版本的OEA使用的属性主要还是 .NET 中的原生 CLR属性系统 + CSLA 开源框架中的属性系统。这些属性中已经写了非常多的代码。属性的 Get 获取器及 Set 设置器中的代码,可谓五花八门。这些都必须在新的属性系统中被完全兼容,否则,必须导致业务功能出现问题。
l 大量历史代码的修改
由于本次设计本质上是一次在历史版本上的重构,而产品开发截止到目前,已经产生了几万行的历史代码,其中的实体属性也是几千个。重构如此底层的设 计,在尽量保证应用层 API 不变的前提下,也必然会造成较多的修改,同时,很可能会引起比较多的BUG。这是一个必须考虑的约束条件。
2.4 风险
l 属性性能由非功能需求的描述中知道,性能是至关重要的。关系到整个设计是否可用。但是,最终开发出来的模块性能,在设计时很难测量的。对于这个风险的规避使 用以下方案:分析历史属性系统的关键性能影响点,在设计稿完成后,理论上检查这些关键点是否能在新设计出来的属性系统下运行良好。
l 支持WPF绑定
这是一个技术难关。
当前我们只是使用了 WPF 中直接绑定CLR属性的方案。如何能让我们在客户化版本的程序集中扩展的扩展属性也支持WPF绑定,成为了一个技术上的难题。
对这点的规避很简单,在整个设计开始之前,先分析WPF绑定中的内部机制,解决这个问题后,才能开始其它的设计。
3 设计方案
3.1 一些决策
由于本系统的设计比较复杂。所以先对兼容性约束做了一个决策:在设计过程中尽量考虑功能上与原属性系统保持兼容,接口上保持一致。但是当无法兼容或者无法保持一致的接口时,可以不兼容。但是这些不兼容的设计点,都需要记录下来,当设计完成后,逐个修改。如果改动较大,则使用组内的重构工具完成。
3.2 风险点验证
3.2.1 支持 WPF 绑定经过查阅MSDN及搜索出的网络资源,发现WPF中的绑定机制支持绑定DataTable数据表类型,而表中的字段则是动态的,根据结果数据的变化而变化。所以只要搞清楚DataTable是如何被WPF绑定支持的,那么EMPS也可以使用同样的机制进行绑定。
以下是WPF中DataTable的绑定机制分析:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/9e4816e51683633485089f6f71822727.png)
图2. WPF中DataTable支持绑定的核心类型分析
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/c15f29d9c09205f8ae08d72702bc36e0.png)
图3. WPF中为DataTable生成视图模型的流程图
重点在于DataTable 实现 IListSource接口,并构造动态的视图动态类型 DataRowView并使其实现ICustomTypeDescriptor。(详细过程参见这篇文章:《OEA 扩展属性系统 - 任意适配 WPF Binding 的设计分析》,以及本系列中的文档:《任意适配 WPF Binding 的设计分析》。)
搞清楚了整个设计及创建流程,那么其实在设计EMPS时,支持这个机制就可以了。
3.2.2 性能关键点
需要分析,历史框架中的属性系统(CSLA托管属性系统)在做到托管属性的同时,是如何保证性能的呢?
其实,它其中属性的核心重点在于使用强类型的FieldData<T>来存储每一个属性,并使用定长的属性值的数组来存放:
private IFieldData[] _fieldData;
这样的好处在于强类型保证了没有装箱拆箱操作,同时定长的数组支持了以O(1)的复杂度来查找指定属性:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/bf79c3a6725551151e81df56dabcf08b.png)
但是,这样搜索属性的前提是属性值数组定长,而一个实体类型到底有多少个属性,是在编译期已经完全确定下来的。换句话说,在这个数组初始化时必须知道固定的属性个数,这违背了属性可扩展的需求,这也是为什么使用这个属性系统很难做到扩展的原因。
当然,在对其进行较大改动的前提下,也不是不可能。但是考虑到CSLA是个开源框架,其满足需求与我们的需求有较大的区别,代码比较臃肿,也无法实现我们所需要的一些功能,对它做大型的改动不如重新做一个完全符合需求的托管属性框架。
经过之前的分析,可以想到,要得到较高性能的托管属性系统,最好也是使用“强类型存储属性值”加“定长数组”的方案。但是如何支持属性的扩展呢?“划分属性定义期”是个较好的解决方案。之后的主体设计中会对这个方案进行详细的描述。
3.3 方案描述
整个设计中,借鉴了CSLA托管属性以及WPF依赖属性的设计,然后再构建出我们自己的属性系统:3.3.1 结构说明
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/6bf61fce9b72c6e1e12bc993ecd6f4f3.png)
图4. EMPS结构说明
脑图比较简单,其中的具体内容可以参考脑图《扩展属性方案》。这里只做简要说明:
l 静态结构
总体上,静态结构比较简单,主要分为两个层次。底层是抽象的属性元数据提供子系统,而另一层则是依赖于前者而构建的EMPS核心:运行时扩展属性子系统。
提取抽象的属性元数据提供系统是为了使元数据的存储、提供都抽象化,后面可以和 OEA 中的元数据存储模块进行适配。
而核心的EMPS则实现了整个的托管属性。后面将会对其以类图的形式重点说明。
l 动态结构
在这里比较特殊地提出了属性生命周期的概念。属性的生命周期规定了属性被定义(或者被反注册)的时期(可能叫定义期会比较正确。),这里主要有编译期、启动期、运行期。
l 编译期
此阶段中定义的属性主要包括使用代码编写的一般属性、扩展属性。当然,也包括“2”和“1”的扩展包中编写的一些对“7”的包中实体类进行扩展的扩展属性。
定义属性时,一同指定它对应的元数据。
l 启动期
此阶段主要以客户化定义的方式来对编译期属性及其相应的元数据进行修改。
l 运行期
该阶段主要用于附加运行时动态属性。
这些动态属性一般只用于显示,它们会影响界面的生成。属性的扩展和删除,要在生成控件之前就能确定,否则,界面没有对应的列。
由于影响界面生成,所以需要为其指定OEA框架中对应的界面元数据。如果不指定,则使用默认元数据。不过这些元数据的设计会在OEA框架中完成,与EMPS的设计无关。
在这个阶段中扩展的附加属性,不会与服务端程序有任何关系。也就是说,不需要为这些扩展属性定义 ORM 等服务端元数据。当然了,这些属性的数据也不需要序列化后在网络上进行传输。
划分出这几个周期的主要原因:使得可以判断出某个实体的编译期、启动期属性列表长度。这是因为,编译期和启动期 已经定义、修改或者客户化的属性,当程序进入运行时后是不会再发生改变的。而这些属性占据了应用开发的95%以上。所以我们只要知道了编译期启动期属性的 长度,也就意味着可以使用O(1)检索的数组来存放,而不是更慢的List/HashTable,保证了这些属性的性能。而对于运行时属性来说,虽然它的 长度不能固定,根据业务场景而变化,但是使用情况较少,可以不考虑性能。
3.3.2 相关UML图
完整的UML图,参见:《实体扩展属性UML设计图》。下面将挑选重点进行说明。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/c58443164d9796472a662edef96df362.png)
图5.扩展属性核心类结构概要设计图
这张是实现扩展属性的核心类结构概要设计图,其中主要包含 ManagedPropertyObject、ManagedProperty、ManagedPropertyField、ManagedPropertyMeta等。由于是概要设计图,其中的方法、属性等相对实现完成后的系统来比,肯定不完整,但是它的作用主要是说明整个设计的核心思想。其中:
ManagedProperty 表示托管属性,每定义一个托管属性,系统都会生成一个此类型的对象用于标记。获取、设置属性的值时,都需要提供此标记来进行检索。
ManagedPropertyMeta 表示托管属性元数据,其中提供了许多信息,例如:默认值、是否只读、属性变更逻辑回调等;这些元数据对属性值的获取、设置的逻辑都有着比较大的影响。
ManagedPropertyField 表示某个对象中某个托管属性对应的值。其实这个类后期在实现时会被定义为泛型类,这样,值的存储就不是object而是强类型的,不需要装箱拆箱操作。
ManagedPropertyObject 表示拥有托管属性的对象基类(实体),其中定义了根据ManagedProperty来 获取、设置值的接口,这使得该对象能够象一般对象一样获取、存储各种值。同时,它也提供了统一处理所有托管属性值的接口,此类统一处理的接口在应用开发时 很少用到,主要给上层的框架使用。上层框架可以应用这些接口完成以下的框架任务:统一的对象值拷贝、统一的序列化、检索特定类型的值等,这样的值的获取、 设置速度,远比反射要快。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/0df78b951ca90631410b076fd77b3081.png)
图6. 扩展属性仓储概要设计图
这张图说明整个系统中的托管属性都是被系统中的单例对象 ManagedPropertyRepository 给管理起来的,为了给上层提供更方便的查询功能,也方便存储,它使用 TypeIndicators类来存储某个实体类型的属性列表。TypeIndicators这个类也负责为上层提供查询:某一个类型已经定义好的属性列表、某一类型及其所有父类定义的所有属性的联合属性列表。同时,这个类中的属性都会生成在类型中的属性的索引,这样,在获取属性值时就可以使用这个索引在属性值数组中进行属性值的查找。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/7e288e5efae529d8044e5688abfa93b7.png)
图7.扩展属性元数据概要设计图
前面提到过,为了保证 EMPS的独立性,我们需要把托管属性元数据的数据获取方案抽象化,这里的 IPropertyMetaProvider就是抽象的元数据提供器接口。而上层的OEA框架则会实现自己的提供器 OEAPropertyMetaProvider,并通过自己的元数据模块中的信息(例如图中的OEAPropertyMeta)来为托管属性提供元数据。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/49dfcc1fd449dcaf86b3369a9463b9c7.png)
图8. 扩展属性实体实现WPF绑定相关概要设计图
这张图看上去会比较眼熟?没错,它和图2中的WPF支持DataTable绑定的类图比较相似。主要也是让 EntityList 实现 IListSource接口,并添加 EntityView 类实现 ICustomTypeDescriptor 接口,这样,就可以实现动态属性的WPF绑定了。
3.3.3 如何支撑需求
主要的设计方案及类图看完之后,我们需要考虑,整个方案是否能支撑起前面说到的所有需求。下图是对整个需求的可支撑性进行分析。相关内容可以参见:《实体扩展属性方案分析脑图》,在此不再赘述。
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/f0f7a385a58dcf19eab56e1d4856a90e.png)
图9. 设计方案对需求的支持度分析。
3.4 重点实现细节
在对需求支持分析之后,再经过召开的设计评审会议,发现这样的方案可以实现基本全部需求。这样,就进入实现环节了。实现环节就关注更多了细节性设计。本文档将会挑选一部分重点进行说明。首先,先来看看最终完成的代码中,最核心部分的代码结构图:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/7325bf48b154158978a3c90beaf29999.png)
图10. 核心代码结构图
整个结构的实现与设计相差无几。接下来,说明一些相对重要的代码:
l 先是ManagedPropertyObject中的属性值获取、设置相关代码:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/911760fbe20447767303f46ca4582af6.png)
前面的设计方案中提到,这个类主要作为所有实体类的基类,提供值的获取、设置等。而这个类其实是把属性值的管理都放到了内部的一个类ManagedPropertyObjectFieldsManager 中:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/09a58094f3046847951a15b887510d3d.png)
并把相关的操作都代理到这个类上:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/6c33ee91628401f10861c9e942e68e24.png)
而 ManagedPropertyObjectFieldsManager则实现了这些逻辑的核心代码。其中,它的私有字段定义如下:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/036f1a1692c002ff8cefceea5d2beb5a.png)
可以看到,编译期、启动期属性值与运行期属性值被分开存放。前者使用数组,构造函数直接初始化,而后者则在需要时才会被序列化。还注意到,它继承自CustomSerializationObject,使得整个属性值列表是可以被自定义序列化的。下面,是泛型的属性值获取与设置逻辑:
internal TPropertyType GetProperty<TPropertyType>(ManagedProperty<TPropertyType> property)
{
var useDefault = true;
TPropertyType result = default(TPropertyType);
if (property.IsReadOnly)
{
result = (property as ManagedProperty<TPropertyType>).ProvideReadOnlyValue(this._owner);
useDefault = false;
}
else
{
if (property.LifeCycle == ManagedPropertyLifeCycle.CompileOrSetup)
{
var field = this._compiledFields[property.TypeCompiledIndex] as ManagedPropertyField<TPropertyType>;
if (field != null)
{
result = field.Value;
useDefault = false;
}
}
else
{
if (this._runtimeFields != null)
{
IManagedPropertyField f;
if (this._runtimeFields.TryGetValue(property, out f))
{
var field = f as ManagedPropertyField<TPropertyType>;
result = field.Value;
useDefault = false;
}
}
}
}
var meta = property.GetMeta(this);
if (useDefault) result = meta.DefaultValue;
result = meta.CoerceGetValue(this._owner, result);
return result;
}
internal void SetProperty<TPropertyType>(ManagedProperty<TPropertyType> property, TPropertyType value, ManagedPropertyChangedSource source)
{
ForceNotReadOnly(property);
var meta = property.GetMeta(this);
bool cancel = meta.RaisePropertyChanging(this._owner, ref value, source);
if (cancel) return;
var hasOldValue = false;
TPropertyType oldValue = default(TPropertyType);
ManagedPropertyField<TPropertyType> field = null;
//这个 if 块中的代码:查找或创建对应 property 的 field,同时记录可能存在的历史值。
if (property.LifeCycle == ManagedPropertyLifeCycle.CompileOrSetup)
{
field = this._compiledFields[property.TypeCompiledIndex] as ManagedPropertyField<TPropertyType>;
if (field == null)
{
//不管是不是默认值,都进行存储。
//不需要检测默认值更加快速,但是浪费了一些小的空间。
//默认值的检测,在 GetNonDefaultPropertyValues 方法中进行实现。
field = property.CreateField();
this._compiledFields[property.TypeCompiledIndex] = field;
}
else
{
oldValue = field.Value;
hasOldValue = true;
}
}
else
{
if (this._runtimeFields == null)
{
this._runtimeFields = new Dictionary<IManagedProperty, IManagedPropertyField>();
}
else
{
IManagedPropertyField f;
if (this._runtimeFields.TryGetValue(property, out f))
{
field = f as ManagedPropertyField<TPropertyType>;
oldValue = field.Value;
hasOldValue = true;
}
}
if (field == null)
{
field = property.CreateField();
this._runtimeFields.Add(property, field);
}
}
field.Value = value;
if (!hasOldValue) { oldValue = meta.DefaultValue; }
if (!object.Equals(oldValue, value))
{
//发生 Meta 中的事件
var args = meta.RaisePropertyChanged(
this._owner, oldValue, value, source
);
//发生事件
this._owner.RaisePropertyChanged(args);
}
}
可以看到,编译期属性主要通过一维数组进行存放,数组中每一个元素都是强类型的泛型对象 ManagedPropertyField<TPropertyType>。
另外,要注意的是,该类提供了同样的非泛型接口:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/1d97f063e9231268f4b14cc939867212.png)
非泛型方法主要是为上次框架提供,其中主要考虑装箱拆箱操作的性能消耗。(关于接口加泛型类的底层框架设计方案,参见:《重构实践:体验interface的威力(一)》、《重构实践:体验interface的威力(二)》。)
GetProperty、SetProperty 方法是对性能最敏感的两个方法,其实现必须特别小心,其内部调用的每一个方法,如 ManagedProperty.GetMeta(ManagedPropertyObject owner)、ManagedPropertyMetadata.RaisePropertyChanged等,也都必须要做特别的优化,需要考虑到装箱 拆箱、属性检索、不构造多余对象等。具体代码设计参考实际工程中的代码,在此不做过多描述。
l 注册属性(及反注册属性)的方法位于 ManagedPropertyRepository类中。
其主要的职责是构造ManagedProperty对象,并计算它的一些重要属性,例如:用于属性快速Hash的GlobalIndex、用于编译期属性检索值时使用的数组下标TypeIndex等。
4 设计验证
4.1 功能需求验证
已经为EMPS添加了丰富的单元测试,该项验证的内容被整合到第5项中,参见:《5.使用手册》。4.2 WPF绑定验证
验证这个比较简单,只要基于它的应用程序运行起来之后,界面上的值都能正常获取、设置即可。不过,我们还是为它加了相应的单元测试,这个在后面会有描述。
4.3 性能验证
之前说到如果EMPS相比原来的属性系统,如果耗时超过120%,则该系统不可用。按照之前关键性能点的设计,应该是可以达到,但是,还是需要做出相应的验证工作及最终的数据。具体内容可以参见:《实体扩展属性系统性能测试报告》。这里说明一下最终结论:
“
性能结论:
新的 OEA 托管属性系统,在带来众多新功能的同时,不但没有降低原有性能,反而因为优化掉无用的代码,使得速度提升。故可以放心使用该属性系统。
”
5 使用手册
5.1 使用场景介绍(单元测试)
由于已经为EMPS添加了比较丰富的单元测试,所以本使用手册将主要以介绍单元测试的形式,覆盖所有可能的使用场景,并介绍每一个场景其对应的使用方法。单元测试所使用的实体类包含下图中的这些类:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/1af4d8ceaf6b86e103052529cacd7df6.png)
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/c49a68f3c0483ade99edff04df01014a.png)
右图是所涉及到的所有单元测试。
“……………………内容较多,省略,有兴趣的可以看附件中的文档………………”
5.2 代码生成 – CodeSnippets
在《OEA 框架演示 - 快过原型的产品开发》中可以看到,OEA框架的快速开发能力中,编译期的代码生成技术是一个重要部分。我们同样为EMPS的80%使用场景都编写了CodeSnippets(关于CodeSnippets的使用方法,参见:《善用VS中的Code Snippet来提高开发效率》):![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/1fe4ec4502670552d225ea7db3eed3a5.png)
导入VS后,只要输入OEAP……,VS就支持这些代码片段的生成,如:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/bdbc9f9a0d212fd5874c117105a90144.png)
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/d730c48afe6cd4e08471a3d2c0783718.png)
5.3 其它问题
5.3.1 扩展属性的CLR属性编写注意点使用EMPS定义的属性,如果不是扩展属性,都会定义一个对应的CLR属性包装器,如:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/32fa931918c8526ef2c4ee292cd396ab.png)
注意,CLR属性内,不能添加任何代码,所有需要对Code属性的Get、Set的定制代码,都需要以回调的形式编写在EMPS中,如:
![](https://oscdn.geek-share.com/Uploads/Images/Content/202003/16/1430cb4bcfc78fdec4de8b1ee328c999.png)
原因是界面框架、ORM框架、WPF绑定等框架内容都不会调用CLR属性,而是直接调用GetProperty、SetProperty方法,而CLR中的代码只是为了方便类库的使用。
5.3.2 何时使用属性扩展,何时使用继承扩展?
EMPS虽然可以直接对某个实体类型进行属性的扩展,但是我们依然老的方案,即使用CLR类继承机制扩展旧的实体。那么,我们需要特别注意两种方案的区别:
1. 属性扩展是直接对指定的领域实体进行扩展,一旦扩展,该领域实体类在整个应用程序中的属性都被扩展。
2. 而继承扩展则需要用于不同的领域实体中。
简单地说,当你想在应用程序中扩展出一个新的领域实体类或者做一个全新的界面时,则使用继承扩展。而当在做客户化时,希望对现有的领域实体类进行完全扩展时,则应该使用EMPS来进行属性扩展。
相关文章推荐
- OpenExpressApp:实体扩展属性系统 - 设计方案说明书
- 《OEA - 实体扩展属性系统 - 设计方案说明书》
- OEA - 实体扩展属性系统 - 设计方案说明书
- 基于特性(Attribute)的实体属性验证方案设计
- 中小型商城系统中的分类/产品属性/扩展属性的数据库设计
- 中小型商城系统中的分类/产品属性/扩展属性的数据库设计
- sharepoint 2010 扩展调查字段属性系列-投票系统扩展方案
- 可复用可扩展的红包后台系统设计方案
- 中小型超市系统中的分类/产品属性/扩展属性的数据库设计
- OEA 扩展属性系统 - 任意适配 WPF Binding 的设计分析
- OEA 扩展属性系统 - 主要设计类图
- 中小型商城系统中的分类/产品属性/扩展属性的数据库设计
- 中小型商城系统中的分类/产品属性/扩展属性的数据库设计
- 中小企业如何设计存储系统方案
- 可扩展、高可用服务网络设计方案
- 图书销售系统系统设计说明书
- Linux 文件系统扩展属性(对文件进行锁定)
- 可绑定可扩展的帐号系统设计原理及其实现(一)
- 基于哈希数据结构的Ogre自定义文件系统初步设计方案
- 百亿级微信红包的高并发资金交易系统设计方案