细节决定成败:映射枚举

我听说有些人对这样的工具比较有兴趣:从数据库的元数据中获得映射对象有信息。我实在怀疑这种工具的价值。从数据库获取元数据信息非常容易,包括名称、类型(含有些可变长类型的长度)、是否可空、主键及外键引用(获取关系)。但是有建模经验的人基本上都知道,这些信息是远远不够的,其一是因为元数据还有元数据,有一些关系是不便通过数据库元数据定义来表达的(例如自引用),有些是表达不全面。例如每个Column除了Column名还有一个Caption(所以DataTable中的DataColumn就同时拥有ColumnNameCaption两个属性),这两个属性的功能是不同的,Column名用于数据库的Schema定义,而Caption基本上是用于领域定义(设计良好的MS Access可以获得Caption);另外他们的字符集(有些数据库规定只能用ASCII码,并且与标签的命名规则相同,尽管MS SQL Server在这方面比较宽松)、特性(有一些关键字不可以用于Column名定义)也不完全相同。所以二者不能互相替代。其二是数据库的元数据规范与面向对象的元数据规范也完全不同,很多情况下Table名不能直接用作Class名、Column名也不能直接用作ClassProperty名。t_Customer怎么也不象个Class名;f_CustomerName怎么看也不象个Property名。所以我反对直接从数据库结构直接生成业务层实体对象的源码不是没有道理的。

类似的道理,在将数据库元模型映射为领域元模型的时候,同时也将枚举映射到领域元模型中就非常有必要。例如:很多人对性别的定义是这样的:

public enum Gender
{
    Male,
    Female
}
当属性为Gender类型并映射到PropertyGrid的时候,弹出下拉列表,其中有“Male”和“Female”两项。如果你在一个英语国家,这没有问题。但是在中文里就非常遭人讨厌。于是,你可以这样定义:
 
[MappedEnum(typeof(int), “性别”, false)]
public enum Gender
{
    [MappedEnumEntry(
0, “男”, false)]
    Male,
    [MappedEnumEntry(
1, “女”, false)]
    Female
}

这样就简单了。你可以修改Gender类型的编辑器,以便让PropertyGrid可以识别MappedEnumAttributeMappedEnumEntryAttribute两个标签,能够列出“男”和“女”两个项。

有人已经注意到了,MappedEnum标签中,第一个参数是枚举的基类型(其实是保存到数据库中的基类型,可以支持intboolstringchar四种,在enum中的基类型永远都是System.Int32);第二个参数是对应的Caption;第三个参数是bool类型的可选的控制参数。当第三个参数为true的时候,将可以从该类型所在的assembly中获取资源,查找名称为Caption的资源串来替换运行时的Caption。一个非常简单的可选参数定义,解决了多语言化的问题。

当然,对于可以多选的词典项,MappedEnumEntry也可以象FlagsAttribute那样进行自动组合成集合类型。

枚举是领域中的一个非常重要的元素,所以Kanas.net2003年的第一个版本开始,就支持枚举的映射。

显然,域对象中定义为Gender类型的实体,有机会透明地按照枚举的定义来保存到数据库中,Coding的时候可以直接使用对应的标识,在UI层也可以如愿地按照客户的要求显示合适的内容,一切都是按部就班。

不过,在实际项目实践中,像性别、星期这样有穷且永远不会变化的枚举太少太少。更多的是一些能够变化、且不断丰富的枚举词典,相信更多的人都遇到过类似的情况。几乎所有的人都会为每个枚举词典定义一个表格。我见过一个国际性的行业软件,枚举词典所占用的表格几乎占了全部表格数的一半,原因是这些项目都是需要作为关键词进行分类统计的。

枚举词典有三个共性。第一个是业务无关性,无论在什么地方引用,都不牵涉到相互的联动关系。一旦枚举词典项发生改动,则一定是枚举项本身的改动所引起,与引用者不发生任何关系。第二个是相对稳定性,大部分情况下是不需要频繁变动的。第三个是共享性。引用可能发生在同一个实体内的多个属性中。这些共性决定了可能采用某种共同的、抽象的方式来处理。

我很早以前的项目实践中采用的策略是单一表:

其中有两个域复用:

category为自引用,值为零的时候,表示本行为类别;值为非零的时候表示本行为对应类别的项。fixedtrue的时候,如果本行为类别则表示该类别不允许添加新项,否则为允许添加新项。如果本行为项,则表示本行不可编辑,一方面是业务中肯定能够用到,另一方面可能是已经被用到了。

category是不可添加的(事实上添加category也不合乎逻辑),所有的操作只有添加新项和删除作废项两种。项的操作有三种:修改名称及可编辑状态、删除该项、合并到其他项。前两个比较好理解,最后一个项意味着可以删除已经被引用的项,将引用改到指定的其他项中。例如有一个项是30~40岁,后来有人错误地输入了35岁,当然要被合并了。记得当时我提供给用户的录入非常自由,可以随意在一个ComboBox中选择列表中的项同时也可以手工填入一个字符串,如果该字符串在该类别中不存在则在该类别中自动添加一个新项。久而久之,五花八门的输入都出现了,幸好我事先给管理员提供了合并项的功能,让他感激不尽。

这种方式一直觉得很好使,于是在Kanas.net1.0中加入了一个自动的类型:Dictionary。后来发现,这个类还是太简单,稍复杂一点就无法处理了。于是在后续的版本中我取消了这个类。事实上,不同的枚举词典类型并不像我设计的那样只有可添加项和不可添加项那么简单,很可能不同的枚举词典还必须承载其他的业务属性。例如单位,除了名称外还有换算关系,不同枚举类别间很可能还会发生一对多或者多对多的引用关系。不过可以肯定的是:枚举词典项不同于普通实体类型的处理,在对象关系映射中也必须采用完全不同的方式。

posted @ 2005-12-07 00:12  双鱼座  阅读(4846)  评论(14编辑  收藏  举报