春鱼·编程观点

技术在进步, 世界在变得美好...

导航

数据与操作的分离、数据实体设计及零、一与多(单体与集合)的辨证统一

春鱼 2004-09-10

摘要

本文着重论述了在 .NET 平台进行应用程序开发时数据实体(entities)的设计,对象型实体与结构型实体的统一,以及由此引出的更深层次的对于应用系统设计理念的思考。

本文的重点是如何将“结构型”实体和“对象型”实体相互结合。而不是仅仅论述DataSet用作数据实体。

引言

设计一个“多层”应用系统时,一个必须考虑的关键问题就是如何在各层之间传送数据。一个已经成型的做法是设计不同规格的类来反映真实世界的业务对象。这些类往往包含常规数据类型、数列、集合或者记录集。把这些数据组合起来,就可以用来在各层的调用之间保持业务数据。 这些承载业务数据的对象就是业务实体(entities)。

NET提供了DataSet类表现多个数据表以及他们之间的关系。当我们向一个DataSet引入高级特性或者定义时,一个强类型化的DataSet就成为数据实体的新的、先进的选择。

但是,真正将DataSet投入应用时,也许事情就没有想象中那么简单了。很多朋友感言他们必须面对大量的编码工作,还有的朋友认为DataSet太有牵制性。

在应用系统的设计中,数据实体一般被定义为存储业务数据的一组类。可以将业务实体设计为仅仅定义业务数据,并不处理任何业务逻辑,实际上数据实体也最好设计成这样。可以实现有效的逻辑隔离,提高内聚性。这就是通常意义上所说的“结构型”数据实体。

与结构型数据实体相对的是“对象型”数据实体。对象型实体是传统的实体设计模式。在这一模式中,业务数据以对象的方式表现。而这些对象一般是从Object派生而来的。我们必须给对象设计都有那些特性、行为。对象型实体可以很形象地表现业务模型,但不适合表现集合数据。另外对象型实体可以实现派生、继承等特性。这是结构型实体所欠缺的。

既然“对象型”和“结构型”各有优略,本文的作者就想到了是否可以将二者结合,起到互补的作用?经过实践,本文作者也获得了一点小小的心得。特撰文同博友们分享。本文的观点、思路可能是错误的,权做批判之用。

DataSet用作数据实体优劣势分析

以作者的看法,DataSet适合用作实体在于:

1.天生可以表示多级的、逻辑结构化的、集合的数据

2.适合被表示层(presentation layer)表现。众所周知,DataSet很容易绑定到表示层的数据绑定控件

3.容易从后端活化/持久化(persistent),通过DataAdapter系列对象和一定的映射规则,可以以自由的方式实现与后端数据库的数据交换

4.数据集中,做为参数传递时形式单一,管理与维护具有一致的外观

5.适合对返回的数据集进行二次处理,例如排序,筛选等

但是单纯使用原生(raw)的DataSet时并不会带来实际的好处。

第一,原始的DataSet数据定义并不固定,程序处理一个DataSet时,并不能确定改对象中一定包含某表,某表中一定定义了某列。当我们在fill过程中出错时,DataSet也许并没有被格式化为预期的格式,这样程序就会出错。而这个错误在运行期是无法避免的。

第二,非强类型化的对象要求我们必须以字符串形式访问table和column,这样就要求我们在程序中对这些table name和column name进行硬编码,这就带来了问题,一方面我们必须以很大成本保证这些名称的一致性,另一方面当数据定义发生变动,我们又必须对程序作出相应的改动。牵一发而动全身,这样我们做“多层”,做“企业级”,实际上是给自己带来了麻烦。

考虑以上因素,我们在应用中定义一系列强类型化的DataSet做为在应用级上做为承载业务数据的业务实体,我们需要详细定义DataSet结构,包含那些表,表内包含的列,表之间的关系或约束等。所作的工作等于重新定义了一套数据库,这个内存中的看不见摸不着的“数据库”通过DataAdapter对象与后端真实的数据库交换数据,完成我们的业务操作。

两种方法可以实现类型化的(strong-typed)DataSet:

1.使用DataSet.xsd定义

2.使用纯编码

当我们依靠纯编码定义DataSet时,我们必须在派生出一个DataSet对象,之后定义一系列常量,用来表示table name和column name。然后在对象的构造阶段(构造器)中分别实例化各table对象,给table对象增加column, 所有这些完成后,我们就完成了一个强类型化的DataSet。

使用XSD定义DataSet可以免去编码的“痛苦"阶段,我们仅需要在设计期中增加表,列。甚至直接从Server Explorer中拖动一个后端的表进来,IDE可以为我们自动生成XML。

这种类型化了的DataSet通常包含数个相关的“表”。每个数据实体包含那些表,而表的结构是如何定义的(包含那些列,列的数据类型)都是确定了的。并且将表、列、表之间的关系都通过public property对外公开。这样业务实体就像是一个内部打好了格子的箱子,可以装那些数据都规定好了,我们很清楚里面都有些什么,而数据是什么格式的也就很清楚了。

当我们的业务模型变得足够复杂,建模工作将会是一项challenging task。因为我们必须考虑到业务模型的适合性,由于各个业务层次都需要参考“标准的业务模型”,这涉及到业务实体在前端的绑定性能、在中间层的处理性能以及在后端的持久化、活化性能。所以业务模型的设计工作异常之关键,必须慎之又慎。

既然设计实体是如此之艰难,必有其可取之处。我们为什么要花这么大成本设计数据实体呢?

这些就是使用数据实体的好处:

1.保持层次间数据交换结构的统一
当数据被类型化以后,所表示的数据类型就确定了下来。我们可以通过其public property访问各表、行,而不用担心会出现格式和定义错误。

2.提高灵活性 当我们修改了数据实体的定义时,不用因为数据的更改牵制到关键接口的更改。

3.减少出错的可能性 当我们面对一类经过定义的“透明”的实体时,减少了将数据定义硬编码在源代码中,可降低出错的可能性。

4.可实现“类型化的绑定过程”:我们向特定的数据绑定控件(有可能也是类型化的)绑定类型化的结构数据时,绑定的过程是非常统一的。实际上我们只需要在一个统一环节中定义绑定的过程。而用到这种类型的绑定的情境中只要简单地使用一个命令就可以了。

5.可实现“类型化的持久化和活化”:这一提法实际上指的是业务实体与后端关系数据库之间的映射。当我们的业务系统设计得足够好时,我们可以定义业务实体和数据库之间表、列之间的映射关系。这样就可以把数据的活化和持久化、更新过程进一步简化。甚至“写删改”等对数据的改动操作都可以简化为单一的“Update”过程。

值得说明的是,以上仅仅是“类型化”DataSet所带来的。到现在我们可以意识到,我们需要通过大量的设计工作、编码工作来换取极高的灵活性。

既然是极高的灵活性,我们就来看一下这一方案是否足够完美。引言中提到,使用结构型的数据实体就丧失了对象实体的继承和派生的性能。而有的时候我们的业务对象的确是有继承和派生关系的。而单纯的使用结构型的业务实体(strong-typed DataSet)虽然不需要将数据的定义(schema)硬编码到系统中,但是仍然有着一些不方便之处。集合数据的绑定就不必说了。比如我们现在需要的仅仅是一个单一的对象的时候,比如一个书店电子商务系统中需要一本数的详细情况是,我们不得不使用BookData.Tables["BookMaster"].Rows[0][""]的形式来访问一些特性。不如对象型实体Book.Name来的直接。而虽然说数据的格式是定义好的,但其中的细节肯定会有一些改变,这个时候实体的更改同样会牵制很多地方的更改。

有没有可能在集合数据绑定和与后端数据库交互的时候使用“结构型”,而在业务逻辑中使用对象型数据呢?本文就探讨了一些实现方法。

绑上一个对象:数据与操作的分离

关于这一问题,本方案的最初想法是这样的。既然我我们必须通过BookData.Tables["BookMaster"].Rows[0][]的方式访问数据的细节,为什么不给目前的DataSet增加一系列public property呢。但是一开始我就否定了这一想法。这是因为我想到现在我们作出的DataSet很纯洁。仅仅用于承载数据。我们不可以仅仅因为我们存取数据方便就给DataSet很规则的内部世界增添过多的行为。所以我想到了将原本应该写在DataSet里的代码分离出来,集中在一个特别设计的“类”中。这样就基本上形成了本文的中心思想-将操作与数据分离

除了设计类型化的DataSet之外,为每个类型化的DataSet设计这么一组类:

他们之间互相有派生与继承关系。甚至有公共的行为定义在基类或者接口中。另外,他们分别熟悉自己所对应的类型化DataSet。通过公开一系列public property,将DataSet的数据公开来。包括其中包含的记录数目,以及分别读取每个单体对象(行)的细节数据(列)。通过这样让每个对象型实例“携带”一个DataSet,可以分别利用对象型和结构型的优点。例如针对以上的实例,我们把“商品”的结构型数据实体(类型化的DataSet)定义为BookData,那么用来管理BookData数据的“对象型”类就叫做Book。Book可以访问BookData包含的记录数目,每条记录的各个列,并且以BookColletion.Count, Book.Name的形式表现出来。

而当不只一个表表示“商品”时,可以定义Book对象自然地读取相关表中的数据。(例如出版社名称)

而除了读取之外,可以对数据进行增删修改等操作。Books对象执行该操作的时候即修改其携带的结构型实体。这样两种实体相互依附,各司其职而密切合作。

此外,当进行实现时,还有一些需要说明的地方:

1.对象的初始化过程:可以重载初始化过程,以对应的结构化DataSet为参数

2.一些特定操作:可以集中提取“结构”里的特定数据,为业务需求服务。其实,多有需要熟悉DataSet机构的操作都应该集中到“对象”里来

3.public propery:get过程与set过程中必须检查“结构”是否为空对象,是否不包含任何行。而这一过程可以提取出来。

非常重要的是:这里的“对象型”实体,归根结底还是实体,所进行的操作,页仅仅是一些简单的读取、删除、修改的操作,以及一些内置的校验。至于和业务相关的逻辑,应该由业务逻辑组件来承担。目的仅仅是为了对应用人员更友好。本文从立意到主题,并没有与OR映射有任何关系。

零、一以及多的辨证统一

标题上还有一个似乎是哲学问题的很吓人的词。其实这只是为了吓唬各位。这的事情没没有那么玄奥。只是虚作声势,哗众取宠而已。


我们注意到,以往我们从后端提取数据“填充”到类型化的DataSet中的时候,我们必须读取数据前必须检查BookData.Tables["BookMaster"].Rows.Count的不为0的时候才敢读数。应用了“对象化”之后,首先我们可以通过Count检查,也可以定义在“结构”中没有行时,返回空的属性值以提示给用户程序员。而另一问题时,BookData中承载的数据时集合化的,而Book表示的是对象的单体。这就需要提前告诉Book读取第几行的数据。如果这一值没有显式的定义,那么就缺省得读取第一行的数据。这样不管提取到的数据没有行、有单一行、还是多行,都单纯地使用同一种表现方式。这就是零、一以及多的辨证统一。

读者可能意识到了,Book为单数,但是有时候表示复数的意义,例如有Count属性。这也是这样设计的一个极不优雅的地方。

posted on 2005-01-17 17:45  春鱼  阅读(4432)  评论(25编辑  收藏  举报