这篇设计文档是 12 月份写来参加公司的研发峰会的,自己倒是信心满满,不过最后还是没有入围。现在想想也没啥大用,所以贴出来,期待与园友交流。
文档有点长,没全部贴在博客中,有兴趣的可以下载附件中的 PDF。
================= 分隔线 ======================
目录
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 元数据重载
属性的一切行为都将以回调的形式存放在元数据中。而元数据是可以被重载的。这样,子类就才重写这些行为。同时,我们就可以在进行产品客户化的时候,为属性重新定制这些行为。
最后,可以看一下在《实体扩展属性方案分析脑图》脑图文档中整理出来的需求概况图,这些需求都是历史版本中所不能支持的:
图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属性的方%