细节决定成败:数据囊的前因后果
前因
数据囊是一组对象成员访问方法的技术。传统的数据访问技术的要点是:定义业务类型的Class,在Class中定义私有的Field,然后定义公开的Property来访问这些Field并为外界提供访问属性的门面。例如这样:
{
private int id;
private string name;
protected virtual void Change()
{
// 属性将发生变化
}
protected virtual void ChangeComplete()
{
// 属性已经发生变化
}
public int ID
{
get
{
return id;
}
}
public string Name
{
get
{
return name;
}
set
{
if (name != value)
{
Change();
name = value;
ChangeComplete();
}
}
}
}
这样,理论上,每定义一个属性都需要同时定义一个相同类型的私有字段。大部分情况下是可以工作的。但是在透明持久化时,将会发生一些微妙的事情。
一、可访问性问题
我们知道,对象模型的对Field的读写完全由Property的定义来控制的。如果Property为只读,则外界无法直接改写Field的内容,只能通过构造器参数来写入。在客户端创建一个对象实例,然后设置一些Property的值,再交给持久层去保存,应该是没有问题的,因为对持久层来说只需要读取就行了,而对Property的读取往往都是提供的。反过来,如果从持久层加载一些对象实例,持久层将无法根据预先的Class定义来访问被Property隔离的Field。
现有的框架如何解决这个问题?如果假设所有的Property都是virtual的(在Java下默认为virtual),可以采用系统提供的动态代理机制,运行时生成一个新的类型,修改Property的定义,让Property从另外的地方访问而绕开以往定义的Field。这里就导致两个问题:一个是将导致从持久层加载的实例会有双倍的大小,因为虽然读写Property绕过了Field,但Field还是要占用空间的。而代理类又必须为重载的Property准备另外一份Field的空间。事实上,手工new出来的实例反而比较少,而从持久层自动按约束Load出来的实例反而更多,ld 因而这个空间开销是非常巨大的。另外一个问题是代理方面的问题。有一些Class的Property并没有定义为virtual(在.Net下默认为final),这样可能需要重新修改Class的语义,已经无法再尊重原先对Class的定义了。
二、集合问题
自动集合持久化的目标是透明地解决对象聚合问题。所谓透明就是对于两个具有聚合关系的对象(UML图中菱形被填实的那种),实现自动级联删除 (包括保持多级递归)、自动子项收集(包括懒惰式加载)、自动联动属性修改和集合移动(当将某个子项从一个父项的集合中移动另外一个父项的集合中时,自动修改相关子项的属性;相反,当修改某个子项的相关属性的时候,将该子项从原始属性所指的父项的集合自动移动到新属性所指的父项的集合中)、整体提交(当提交父项的时候自动提交所有的子项,而提交父项前禁止提交子项)等等效果。理论上,要达到这样的目的就必须能够控制整个集合操作和属性操作过程,以便捕获属性和集合的变化。这样不可避免地要控制对集合所对应的Field的类型以及初始化过程。通过定义基类来提供集合操作的公共服务可以解决一部分问题,但不能完全解决问题,例如集合的类型事实上是不需要全部在编译期定义时,完全可以延迟到运行时再定义。
三、空值问题
在大多数情况下,空值与默认值具有不同的语义。例如某件商品的价格为空,表示价格尚不可知,与一般意义上的价格为零具有不同的含义。所以几乎所有的数据库都支持元数据定义前提下的空值定义。假设某个Property的类型为double,如果对应的Field没有定义,则这个Property永远只会返回double的默认值0。当然,所有可空的类型可以另外定义一个bool型属性并分配对应的字段来保存是否为空的状态。这样就必须为每一个nullable的Property添加额外的定义,而没有一种更通用的解决方案自动处理相关的空值问题。
后果
数据囊(DataCell)用来解决这个问题。数据囊替代了对所有Property的Field的定义,只需要定义一个统一的数据囊访问方法:
protected DataCell EnsuredCell
{
if (_Cell == null)
{
lock(this)
{
_Cell = DataCell.CreateCell(this);
}
return _Cell;
}
}
当然,这个定义可以通过一个基类来实现:
{
private DataCell _Cell = null;
protected DataCell EnsuredCell
{
if (_Cell == null)
{
lock(this)
{
_Cell = DataCell.CreateCell(this);
}
return _Cell;
}
}
}
这样,对于每个Class来说,必须通过访问键来获取相应的属性:
{
public int ID
{
get
{
return this.EnsuredCell.GetInteger(0);
}
}
public string Name
{
get
{
return this.EnsuredCell.GetString(1);
}
set
{
this.EnsuredCell.SetString(1, value);
}
}
}
那么,数据囊如何解决上述三大问题?
一、可访问性问题
虽然对于引用数据囊的用户Class来说,一样不能越过Property的只读性限制来访问只读的Property。但是,对于可以获得DataCell实例的对象来说,却可以通过SetInteger(0)这样的方法来写,而不是访问用户Class的Property。这样,框架可以获得额外的控制权,高效而安全地访问内部Field。无论是客户手工new出来的实例还是持久层Load出来的实例都是同出一门,完全相同。数据囊保证了底层系统对Field的定义、初始化和改写的控制。
二、集合问题
由于数据囊是完全暴露在底层系统面前的结构,这样可以在构造集合类型的Field实例的时候,实际构造一个重载过的集合类型,在重载过程中织入捕获集合访问的代码。
三、空值问题
数据整还提供了一个通过属性访问键访问属性是否为空的读写方法:
{
}
public void SetNull(int key)
{
}
只要在客户Class中以更友好的方式公开这个方法即可得到是否为空的状态并将状态改成“值为空”。
属性访问键是什么东西?
属性访问键可以认为是对某个class来说,某个property的唯一标识。这样,只要是整型并且在一个class中不重复即可,并不一定要连续。当然,Property名本来也可以作访问键的。但是显然string类型的访问效率无法与int类型相比,复杂的、耗时的操作总是集中地放到某个地方,并尽可能将减少调用的次数。事实上,访问键是由建模工具维护的,对使用者来说仅仅是一个符号而已。
DataCell“背着”客户干了些啥?
现在揭示DataCell的秘密。
DataCell就是一个非常简单的类。从外面看,DataCell只有一个字段(就是该数据囊的所有者)和一大堆虚拟方法,一共21个。打开源码,这些方法大都没有内容,或者返回一个空值,或者返回一个默认值。但是这21个方法就是Field访问链的起点!
DataCell被定义为一个abstract class,这样就无法主动new了,而只能由DataCell的静态方法CreateCell来代劳了。的确,CreateCell作了大量的工作,首先是通过反射,获取所有被代理的对象(MasterObject)的全部Property,然后从DataCell派生一个运行时的类型,根据实际类型定义一些Field,恰好这些Field与每一个Property对应。然后将这个运行时类型加入到应用域中,并建立一个实例。此外,对于可为空的Property还要通过另外一种机制来保存每个Property的访问状态;对于特别定义的集合类型,还必须动态生成一个可截获所有集合操作的运行时集合类型,虽然这些类型只是将集合操作的信息传递到指定的类型上,自己并不实际处理。
动态生成的DataCell类型是什么样子?以下是一个典型的派生类:
{
private int _Field_0;
private string _Field_1;
public override int GetInteger(int key)
{
if (key == 0)
{
return _Field_0;
}
return base.GetInteger(key);
}
public override void SetInteger(int key, int value)
{
if (key == 0)
{
_Field_0 = value;
}
else
{
base.SetInteger(key, value);
}
}
public override string GetString(int key)
{
if (key == 1)
{
return _Field_1;
}
return base.GetString(key);
}
public override void SetString(int key, string value)
{
if (key == 1)
{
_Field_1 = value;
}
else
{
base.SetString(key, value);
}
}
}
所有的DataCell的继承体系都与其MasterObject的继承体系相对应。派生的DataCell仅处理对应Class中直接声明的Property,并不处理其基类所声明的Property,这样保证了整个体系所需要的联动以及实例共享。
题外的话
“数据囊(DataCell)”这个词是我定义的。无论如何,任何对类型/属性映射为表/列结构的框架都不可避免地用到类似的技术,有一些是采用代理(Proxy)、有一些则是动态生成。给客户提供的业务类型总是作为一种数据的承载体。虽然很多人都认为这样的业务类型都是数据集性质的,谈不上面向对象,觉得对象一定得有行为。我不敢苟同。将数据层面与数据处理层面分开,似乎更显其合理性。DataSet就没有行为,一直以来都OO地工作着。
我大略分析过两种.Net下比较成功的商业框架ECOII和DataObjects.NET,两者有一个共同之处,业务对象不可以通过客户端new出来实例,而必须由框架提供的构造方法来构建实例。这样除了事务容器可以更早地介入实体实例内部之外,避免了使用背离客户所定义业务类型的原义而获取代理所花费的时间和空间开销。这两个架构有一个共同的最典型的标志是,其业务类型简直就不能算作完整的业务类型,前者都是将实体和实体集合映射成抽象接口(这样更方便建模期支持,因为建模期无法完成方法的编码),后者也是将实体和实体集合映射为抽象类型(采用抽象类而不是用接口正是为了运行时构造类型时有更多的编译期方法可以调用,简化运行时类型生成器的逻辑),所有属性访问方法都是抽象的。无论如何,运行时的类型是根本不可知的。这种运行时修改编译类型的伎俩现在遍地皆是,连ASP.NET运行时WebForm类型也和你编译时定义的类型完全不同。