实际开发过程中,一对多(1:n)的对象关系是非常常见的,比如销售订单(Sales order)
下可以有多条订单行(Sales lines),这种关系可以直接使用数据库提供的主/从表关联关系实现。面向对象分析与设计思想并不提倡将数据库作为整个模块甚至系统设计的主要对象。例如,在处理多对多的对象关系时,数据库的简单关联就显得有点力不从心了。在这种情况下,设计人员不得不增加新的数据表,以便将多对多的关系演变成一对多的关系。
举个很简单的例子:学生与老师的关系。在实际生活中,一个学生可以被多个老师(语文、数学、英语等)教,而对于老师呢?肯定是可以教多个学生的。那么“教/被教”这个关系就是个多对多的关系。在使用传统数据库理论处理这个关系的时候,不得不引入一个“Teaching”的数据表,包括两个字段,就是StudentId 和TeacherId,表示各个学生与各个老师之间存在“教/被教”关系。通过这个数据表的引入,学生和Teaching 表就是一对多的关系,由StudentId关联;老师与Teaching表也是一对多的关系,用TeacherId关联。从数据库设计角度看,增加新数据表并非解决多对多关系的唯一途径,但只有这种方法可以完善数据库的设计(达到3BNF)。从表面上看,这样的设计是合理的,因为实体及其之间的关系都已经非常明了,然而这种设计没法正确地表示人们对事物的理解。比如,在上面的例子中,您将如何去理解“Teaching”这张数据表的确切含义?如果您的客户是一位不懂计算机,而只懂得“教学”领域的领域专家,您如何去向他解释“Teaching”是什么?您对他说:“Teaching就是老师与学生之间的教学关系”么?
这种面向数据库的设计思想(或者说一拿到需求,就想到如何去定义数据库表的设计思想)对于小型系统还是有利的,至少能够加快实现进度。倘若摆在您面前的是一个超大型的软件系统,其中包括不计其数的类及其之间的复杂联系,您能有效地将它们全部映射到数据表吗?在Dynamics AX 中,处理多对多关系的一个非常有效的途径就是“接口”的引入。在实际开发过程中,接口似乎是用得很少的一个东西。
在上面的简单的类关系图中(点击查看大图),ItemBase 为所有项目的基类,而BlueItem、GreenItem、RedItem以及YellowItem都继承于ItemBase并实现了GetColor 方法。IInvokable是所有接口的抽象,BlueInvokable、GreenInvokable、RedInvokable继承于IInvokable接口。BlueItem、GreenItem、RedItem以及YellowItem在继承于ItemBase 的同时,又分别实现了这些接口。Invoker 是一个客户类(或称调用类,Consumer),通过Registry中维护的类的属性来调用左边的那四种Item。
下面是Registry.getItems、Registry.register、Invoker.Invoke 方法的实现,从这些伪代码中可以很清楚地了解到Invoker 是如何通过接口来调用这些Items 的:
public void Registry.register(ItemBase _itemBase)
{
itemCollection.Add(_itemBase);
}
public ItemBase[] Registry.getItems(IInvokable _intf)
{
ItemBase[] ret = new ItemBase[](MAX_ITEMS);
foreach (ItemBase itemBase in itemCollection)
{
if (itemBase.IsImplementing(_intf))
ret.Add(itemBase);
}
return ret;
}
public void Invoker.Invoke(IInvokable _intf)
{
ItemBase[] items = registry.getItems(_intf);
foreach (ItemBase item in items)
{
print item.GetColor();
}
pause;
}
比如,当我们需要调用所有与红色相关的Item时,我们只需要将RedInvokable 作为参数传递给Invoke方法即可。此时,Invoker 会从Registry中获得所有实现了RedInvokable接口的类,并以数组的形式返回给items,然后再在Invoke 函数中,逐个调用每个item 的GetColor 方法。上图中有一个特例,就是YellowItem,它实现了两个接口:GreenInvokable 和RedInvokable,这就表明,YellowItem可以被两个地方所调用(甚至可以被多个地方所调用,只需要在implements子句中加入接口名称即可),与此同时,当传递给Invoke函数的参数是RedInvokable 时,会有两个Item 被调用(RedItem 和YellowItem,取决于有多少个Item 实现了这个接口)。总结一下,在这种设计模式中,一个项目可以被多个地方所调用,相反,某个地方可以调用多个项目,事实上就是多对多关系的面向对象实现。
在Dynamics AX 中其实已经存在了这样的使用方法,那就是System checklist。checklist中的item可以出现于多个checklist中,而每个checklist又有多个checklist item。举例来讲,SysCheckListItem_Compile 实现了SysCheckListInterfaceUpgrade 和SysCheckListInterfaceSetup 这两个接口,这表示在Upgrade 和Setup 的checklist 中会出现Compile 的选项,要求客户对AOT 进行Compile;而实现了SysCheckListInterfaceUpgrade接口的类却不仅仅只有SysCheckListItem_Compile。在Dynamics AX 的checklist 系统中,SysCheckList::getAllCheckListItems 方法就相当于上面的Registry.getItems 方法,SysCheckList::checkListItems 方法就相当于上面的itemCollection。AX 的checklist 没有与Registry.register 对应的方法,它要求开发人员在建立新的checklist item 的时候手动地往SysCheckList::checkListItems 中写入class number 进行注册。
实际项目中也用到了这种设计模式,ASI Project EDD021 Sales order Hold就是一个很好
的例子。这种模式的引入使得今后对框架进行扩展变得非常方便。这种处理多对多的模式有一个弊端,就是项目(item)的注册。AX checklist 系统是让开发人员自己修改SysCheckList类,将新添加的checklist item类加入到checkListItems容器中实现注册;而在ASI Project EDD021 Sales order Hold项目中,我们采用了“运行时注册”的方式,也就是当销售订单执行锁定判断时完成item 注册,这种做法其实并不合理,因为并非每次运行锁定判断时都会有新的item 加入进来,锁定类型的添加毕竟不是高频事件,每次的判定会降低系统效率。