microsoft Corporation
Leo Romano
Sierra System Group Inc.
2000年8月
摘要:详细说明用来设计可重用企业组件的一种方法。本文重点讨论 Engine-Collection-Class (ECC) 设计模式,它是一种为分布式/分层应用程序创建可重用企业组件的灵活模型,这类应用程序既可与传统的“胖”客户机一起工作,也可与脚本客户机一起工作。
下载 desipat.exe 样例文件 (1.46 MB)。
目录
本文详细说明用来设计可重用企业组件的一种方法。重点讨论一种已被证实的设计模式,该设计模式已由多个项目和客户实现,并进行了改进。
Engine-Collection-Class (ECC) 设计模式是一种为分布式/分层应用程序创建可重用企业组件的灵活模型,这类应用程序既可与传统的“胖”客户机一起工作,也可与日益流行的脚本客户机一起工作。由于它有简化的接口和直观的编码样式,ECC 设计模式缩短了开发时间,并使多个开发人员可以实现一致的编程。各公司和开发人员在实现将来的增强时可利用它的可扩展性,并在支持某个应用程序时利用它的可维护性。
因为此设计模式的核心以 com 和其它流行的设计模式为基础,所以 ECC 既可使用面向对象的语言实现,也可使用面向组件的语言实现。本文重点讨论 ECC 设计模式的 Microsoft® Visual Basic® 实现。
本文主要作为 Engine-Collection-Class 的一个初级读物,并未详细讨论 ECC 设计模式的某些更高级的特性。
为有效使用 ECC 设计模式,建议首先满足以下必备条件:
- 对 n 层体系结构和设计有基本的了解,包括 Microsoft windows® DNA 和每一层的功能。
- 对 COM 的概念和 Visual Basic 如何实现它有基本的了解。
- 对面向对象的概念有基本的了解。
- 对组件设计有基本的了解,包括对象的用途、松散耦合的概念以及接口与实现的分离。
- 对数据库设计有基本的了解 — 知道实体关系图 (ERD) 以及如何在数据库设计中使用它们。
- 同意以下观点:若非作出决策的各种因素导致实现设计不灵活,许多系统设计都会是非常灵活的。ECC 设计模式考虑了这种情况,并使用了一种灵活的实现方式,以便可以降低这些决策的成本。
注意: 有关进一步阐述这些必备条件的著作和文章的列表,请参阅参考资料部分。
您是否完成过这样的项目:您认为它是可重用的和可扩展的,但后来却发现为了利用最新的技术变革,您的系统必须重写。您是否遇到过这种情况:您启动了一个项目,而完成到一半的时候却意识到将 XML 或最新的 SOAP 协议用于互操作性会极大地改进将来的系统,不料发现这将破坏已写好的每个组件,并使成本直冲云霄。如果您回答“是”,则您不是唯一遇到这种情况的人。
在现在的市场竞争条件下,各公司需要以最少的信息作出快速的、长期的决策。错误的决策可能影响公司的每个部分,并使之倒退几个月甚至几年。企业行政管理人员和技术开发人员都想知道是否有一种方法可以降低开发风险。本文介绍一种用来构建可重用企业组件的设计模式。ECC 是一种简单且直观的设计模式,它的开发始终考虑着一样东西:变化。业务变化、规则变化、技术变化和人员变化。
但是,正是因为事情的变化并不意味着您必须忍受这种变化带来的全部影响。从企业的角度来看,ECC 设计模式有助于减轻以下因素的影响:
- 未达到的要求 — 所传达的意思和所理解的意思之间的差异。
- 人员不足 — 可能存在人员不足的情况,但在一个问题上投入更多的人力同样可能减少收益。
- 更高的学习要求 — 为了维护和扩展现有的系统,只懂一种编程语言是不够的。
- 更紧迫的时间 — Inte.NET 世界的事情发生得很快,机遇之门很快就会关闭。
- 额外的成本 — 项目超支和因项目范围的变化而引起的计划修订都会造成不利的影响。
从技术角度来看, ECC 是一种用来开发 n 层业务组件的三层设计模式,它允许以对解决方案影响最小的方式更改组件的创建、存储和属性。ECC 通过使用以下技术减少了变化对技术的影响:
- 组件的标准且一致的接口,它们提高了易用性。
- 开发人员所熟悉的启发式的通用面向对象编程 (oop)/面向对象设计 (OOD),如设计模式和封装。
- 使组件既能在传统的“胖”客户机上使用,也能在脚本客户机上使用的简化接口。
- 直观的组件创建方式,使开发更快更容易。
使用可扩展体系结构的前端规划对于下一代应用程序和业务是至关重要的。在当前的项目中使用 ECC 设计模式,就可以开发出推动将来业务发展的更强健、更灵活的系统。
在开发可重用的设计模式解决方案时考虑了以下的核心元素:
n 层分布式设计(层的隔离) 应用程序被分成多个层,以限制对应用程序某一部分的更改影响应用程序另一部分的级联效应。虽然这需要更长的前端设计时间,但分层隔离与其它可供选择的方法相比降低了运作成本。
最小化网络调用 使用基于 web 的解决方案(位置透明性), 应用程序的设计必须使应用程序在链的任一端完成尽可能多的工作,而在网上发送的信息量最少。这一概念可扩展到减少进程之间、线程之间和上下文之间的调用。
灵活的体系结构 缺乏灵活性的应用程序源于缺乏灵活性的实现。在努力达到设计模式的预定目标的过程中,最重要的一个问题是:“如果发生变化会怎样?”。目标是提供最大化的可扩展性、可重用性、可维护性和独立性(数据源、数据位置、语言)。归根结底,目的是要开发是一种设计模式,而这种模式将有助于降低投资成本、管理范围的扩展、最小化各种变化造成的影响,以及在不增加使用复杂性的同时提高可重用性和可维护性。
可维护性和可扩展性与性能 可维护性和可扩展性通常以牺牲性能为代价,但是当系统增强的成本很低时,牺牲的性能就会得到弥补。如果必须在可维护性和可扩展性与性能之间作出选择,则保持可维护性和可扩展性总是最好的选择。设计模式的实现者最终将具备放弃一种选择而选择另一种的能力。
简单一致的接口 构建简单一致的启发机制来调用系统中的对象有助于降低培训用户的成本,也有助于降低其他开发人员的学习难度和知识要求。
封装并集中管理外部依存性 组件通常可以从外部资源(如操作系统)继承服务。大多数 Windows 组件(或应用程序)依赖操作系统或其它服务(如 MTS 或 COM+)来提供安全访问。如果这一点无法实现,或者使用继承的服务得不偿失,则集中管理组件中的外部依存性是有价值的,因为它允许集中控制。因此,如果这些依存性发生了变化,程序员将不必判断哪些模块或代码段需要更改。
现实世界的实体及其相互关系的模型化(RdbMS、Use Cases、OOP) 现有的许多方法学都描述现实世界的实体以及它们藉以相互作用的过程。下面是几个例子:
- ERD、流程图和数据库设计
- Use cases(模型实体及其方案)
- 面向对象编程 (OOP) 将对象松散耦合到另一个对象或者它们所实现的接口
ECC 设计模式使用这些方法学中已被证明的启发方式来构建可重用的设计。
ECC 设计模式也按照 Windows DNA 模型来构建可扩展的、强健的分布式系统,并将它们与符合行业标准的设计技术(如 OO 设计、设计模式和数据库设计)组合在一起。这些技术知识对于与应用程序打交道的人来说很常见,所以从一个阶段转到下一个阶段变得简单明了而可靠。
本文在讨论 ECC 设计模式的过程中提到了这些核心元素。但是,重要的一点是记住:这并不是每个分布式应用程序的最终设计模式。它只是众多选择中的一种,在采用它之前必须考虑实现问题。
Engine-Collection-Class 设计模式要求作一些与设计人员的习惯稍有不同的考虑。设计人员倾向于用过程化设计处理一个项目 — 要达到要求或处理数据,我必须完成哪些过程?对于 ECC 设计模式而言,这些过程是第二位的,并且如果模型设计正确,这些过程在逻辑上将归入对象本身。
图 1 揭示了 Engine、Collection 和 Class 之间的符号关系。从逻辑设计的角度看,Class 实际上是 Engine 和 Collection 存在的主要原因。Class 通过其属性(如名和姓)与现实世界的实体相联系。Engine 和 Collection 则提供用来管理和扩展其行为的方法。对于要被建模的实体,必须创建所有这三种元素。
图 1. ECC 设计模式关系图
ECC 将实体的创建、存储和属性分离开,从而为业务对象提供了一种三层设计。
ECC 设计模式使用面向对象的技术(如封装)和一些很流行的设计模式(如 object Factory 和 Opaque Object Adapter)。
注意: 有关这些模式的其它信息,请阅读 Visual Basic Design Patterns,它列在参考资料中。
通常,Engine 实现一种称为 Object Factory 的设计模式,后者定义了一个用来创建其它对象的接口。在这种情况下,Engine 既创建 Collection 对象,也创建 Class 对象。以这种方式设计 Engine 提供了以下好处:
- 通过单个接口控制和强制对象的创建
- 提供检查和强制对象创建的安全性
- 通过强制同一 COM 单元中对象的创建,在多线程进程中提供运行时好处
- 提供客户机与 Collection 和 Class 对象之间的松散耦合,这可以增加新的功能而不影响客户机
- 用已知的有效状态强制对象的创建
Collection 实现一种称为 Opaque Object Adapter 的模式,后者允许一个对象私有地封装和使用另一个对象的接口和功能。这种技术使 Visual Basic 能够模拟实现的继承,即使它目前只支持接口的继承。正如图 2 所示的那样,Collection 包含对 ConcreteInternalStorage 的一个引用。这称为合成。ConcreteInternalStorage 对象用于存储 Class 对象,并且根据所提供功能的多少,它也可以用来为 Collection 提供扩展功能。数组、Visual Basic 的 Collection 对象、字典对象和 ADO 记录集都是 ConcreteInternalStorage 对象的例子。这样实现 Collection 提供下列好处:
- 实现的继承,虽然 Visual Basic 目前不支持它
- 通过更改 ConcreteInternalStorage 对象增强功能
- 客户机和 Collection 的内部存储对象之间的松散耦合,允许对象变化对客户机产生最小的影响,或者根本没有影响
- 处理 Class 对象的能力(如计算总和、计数)
Class 实现封装的表示方法,意味着内部实现细节对使用它的客户机是隐藏的。这提供以下好处:更改实现细节不会影响客户机。
图 2 揭示了 ECC 设计模式中的对象是具体的类这一事实。这意味着没有使用 Visual Basic 的 Implements 关键字,也不存在抽象类(只有接口而没有实现的类)。其主要原因是已对 ECC 设计模式做了调整,它现在使用相同的组件就可以同时支持传统的“胖”客户机和永远流行的脚本客户机,而不需要额外的编码。这种设计决策有助于缩短项目的上市时间并降低成本,因为不必为内部(或外部)应用程序创建或维护不同的组件。
图 2. Engine-Collection-Class 模型
Engine-Collection-Class 模型是用 Object Factory 设计模式和 Opaque Object Adapter 模式实现的。该图表明了每个对象可以实现的基本方法。
设计的好处
ECC 设计模式类似于业务对象的三层设计,在业务对象中将实体的创建、存储和属性分离会提供巨大的灵活性和好处,如调用拦截、强制实施业务规则、数据验证、导出的属性、简化的接口和强制对象创建。
有效的 ECC 设计是由数据库(数据源)设计推动的,而后者反过来又是由业务需求推动的。通常,数据源方案模拟现实世界中实体的关系和存储,在数据源或 ERD 的开发中投入了大量的精力。对于 ECC 设计模式而言,为了作为继承基本业务规则的起始点,Class 映射到数据库的表、视图、存储过程或其它结构化的存储。使用这种策略可以平衡现有的工作和知识,并较为容易地向其他人解释系统的设计。这改善了交流和反馈,并减少了知识传递的问题。面向对象的技术(如 Use Case)可以很好地获得用来将各个部分结合在一起的方案,诸如 Microsoft Visual Modeler 和 Microsoft Visio® 之类的工具可用来说明和生成对象。
当修改或增强数据源之后,可以通过新的特性或方法扩展 ECC。这可以通过适当地对 COM 中发布的接口建立版本来实现。
注意: 您不能更改、修改或删除现有的公共接口而不破坏 COM 的兼容性。ECC 不解决与修改系统相关的问题;但是,它有助于使更改更为容易。
用 Class 封装数据源方案特性为存取和处理数据提供了一致的抽象层。但 Class 不仅限于数据源方案特性。可以将导出的属性定义为允许使用附加的功能。这些属性从物理意义上可以存在于另一个数据库(诸如 LDAP 或 Microsoft Active Directory™ Service 之类的数据源)中,也可以由客户自定义(IsNew、IsDirty),或者也可以是聚合函数(Sum、Count)。最后,这个抽象层意味着细节被隐藏,可以自由更改,并提供一个进行调用拦截的场所。调用拦截是指这样一种能力:访问请求,并处理调整调用者或被调用者的请求所需的任何调解方法。例如,当您在 Visual Basic 中调用 New 操作符时,Visual Basic 将在第一次引用此对象时创建它。开发人员无须为此功能做任何事情;但是,您也不能进入此调用更改它。开发人员能够捕获的调用拦截类型处在一个更高的级别(特性调用、方法调用、接口调用和事件)。这就赋予了开发人员响应请求的能力。这是一种关键的功能,因为 ECC 使用这种调用拦截来强制实施业务规则。例如,某个业务规则可能是:口令字段的长度必须至少是 4 个字符,但不能多于 10 个字符。Password 特性会成为检查和强制实施此业务规则的一个挂钩,它最终维护基层数据存储的数据完整性,而无须网络调用。另一个好处是,这一规则只处于单一的位置上 — 如果这一规则要发生改变,则此位置会提供更好的可管理性。
ECC 设计模式的最后一个(但并不一定是最不重要的一个)好处是,能够提供简化的接口。因为接口定义了组件所能完成的工作,所以它就像一个有约束力的合同。与所有细节都在一个大型文档中的合同不同,ECC 接照较少的接口和特定的作用将功能划分到各个对象中。因此,每个对象都可以支持不同于它自己但在整个应用程序中对该对象的作用都保持一致的功能。例如,尽管 Engine 和 Collection 以彼此不同的方式执行函数,但它们将支持相同的基本功能。Engine 用来创建 Collection 和 Class 对象,Collection 则用来存储和操纵 Class 对象。因此,将对象分开使更加简化的接口成为可能,而不是创建一个同时支持两个接口的超级对象。此外,这使得每个对象都可以封装特定的作用和功能,并使每个对象单独演变而不会影响另一个对象。这样,开发人员就实现了灵活的应用程序设计,而没有牺牲代码的简单性或一致性。另外,熟悉这种模型的开发人员可以从开发过程的各个地方开始工作,而不会由于急剧增加的学习难度而使生产效率大幅下降。
下面是每个对象的详细说明以及它们在 ECC 模型环境中的含义。
Class 设计
Class 设计为将现实世界的实体映射到业务组件提供了蓝本,它也是 Engine 和 Collection 对象存在的主要原因。Class 的责任是:
- 在属性级强制实施业务规则并维护数据完整性
- 隐藏 Class 属性的的实现细节
- 将其它对象连接在一起来模拟复杂的关系(可选)
如果模型中存在一个 Class,则必须有一个 Engine 和一个 Collection 对象。在 Class 不存在的情况下,也不会存在相应的 Collection,不过 Engine 可以存在,它将用作一个服务或实用程序对象。
因为业务规则是特定于项目的,所以它们的强制实施是特定于实现的。重要的一点是,您的设计体系结构应该允许在一个地方灵活地定义业务规则,而在多个地方强制实施它们。Class 的责任是强制实施以下几个方面:
- 数据的有效性 — 维护数据完整性
- 数据的格式 — 允许定制格式化条件,其中可能包括数据转换
- 数据访问的安全性 — 检查用户的完全性,以确保他们可以更新这个字段或执行这项操作(提供基于角色的安全性)
与 Engine 和 Collection 不同,Class 的方法和特性是以数据架构为基础的。每个 Class 都可以镜像应用程序中的实体。Class 特性到数据特性的映射是特定于实现的。例如,您可以使用单个 Class 引用三个物理表/视图,也可以使用三个单独的 Class。每种设计都需要分析对象的目的,并确定哪个模型最适合此实现。确定实现设计时需要考虑的一些事情包括:方法边界、事务边界、安全性和数据位置(sql SERVER™、COMTI,等等)。图 3 显示了一个样例实现的 ERD。在本例中,每个实体都具有一个 Class:UserInfo、Account、Bill。此外,如果实体的结构类似,则可用单个 Class 来表示多个实体。在此样例实现中,IOBPLookup Class 为 ActivityType、BillType、Status 和 UserType 实现了这一功能。这些表主要存储要用于置入控件或下拉式列表中的只读信息。将这些实体组合到一个 Class 集合中的一个好处是共享功能。例如,如果 IOBPLookup Class 实现了高速缓存机制,则其它组件就可以使用它的功能。这减少了要开发、测试和维护的代码量。
图 3. Online Bill 数据库图
此样例实现包括某些附加的特定于业务的字段:
- RecordTimestamp 用于并发性和记录的锁定
- DeleteFlag 标记一个要删除的记录,删除是通过外部过程发生的:手动或自动
- CreateDateTime 存储创建记录的日期/时间以便进行审核
- UpdateDateTime 存储最后一次更新记录的日期/时间以便进行审核
此模型中的实体是通过存储过程访问的,每个存储过程都对应于一个功能(读取、插入、更新和删除)。此样例实现将单个存储过程用于多个功能,但对于 ECC 而言,您可以访问多个存储过程对数据进行更改,这将在 Collection 中实现。它们以用在 ERwin 中的宏模板为基础来自动生成实体(请参阅与本文对应的代码示例中的宏模板 "databaseonlinebills.er1")。您可以使用标准的 SQL 语句,但为了获得真正的灵活性,鼓励使用存储过程。如果您决定使用 SQL 语句,则应构建一般的 select、Insert、Update 和 Delete 语句。但是请注意,使用 SQL 语句存在一些限制。例如:
- 插入可能需要多次往返,这取决于行标识符的生成和锁定机制的使用。
- 根据像 UpdateDateTime 和 RecordTimestamp 这样的列的使用情况,更新可能需要多次往返。
图 4 说明了一个以 UserInfo 实体为基础的样例 Class,以及 ECC 应该包括的其它特性。Visual Modeler 用来生成从数据库表得到的初始图形。请注意,对象带有一个表示 Class 的前缀 c。
图 4. UserInfo 实体的基本 Class 图
数据库中的每个属性都有两个特性:Let() 或 Set() 和 Get()。每个特性都被标记为 Public、Protected (Friend) 或 Private。图 4 显示 UserNumber 特性是共享可读的 (Get),并带有写 (Let) 保护。UserNumber 特性是 Class 的主关键字,因此不应该由外部实体更新,因为它是从数据库 (例如 identity 列)自动生成的。通过使用这项技术,Engine 可以初始化 Class 的特性,但仍然提供一种机制使值不能被窜改或更改。Class 是自含的,因为 Class 可以被保持和重新创建,所以很有用。
对 Class 接口的基本建议如下所示:
- Dirty—Boolean,Friend Let(),Public Get()— 当在记录级更改属性时进行设置。Collection 方法也用它来检测 Class 的状态是否发生了改变。它不能用于只读 Class。
- ClassStorage—Boolean,Friend Let(),Friend Get()— 用来绕过属性级包含的内部业务规则检查。Engine 也用它来最初设置单个特性,或者 Collection 用它来检索 Update() 中的值。
- IsNew—Boolean,Friend Let(),Public Get()— 当向 Collection 中添加新 Class 时进行设置。Collection 用它来确定数据存储中当前是否没有包含这条记录。如果不允许插入,就不会使用它。这类似于 ADO 中的 EditMode。
- SecurityToken—GUID(或某个唯一标识符),Friend Let(),Friend Get()— 用来为字段级限制存储用户的安全符号。如果不需要字段级验证,则这个特性是可选的。
注意: 这是一个本文未涉及但却包括在实现中的 ECC 高级概念。
可以添加到此 Class 中的其它接口包括:
- 具有相应的 Let()、Get() 或 Set() 方法的每个字段项的特性以及声明(Public、Protected/Friend、Private)。
- 根据 Class 中的属性返回值的导出属性或导出方法。例如,如果 Class 是 Student,则您可以编写一种方法来计算平均等级分。
- On-Demand Loading 特性或用来在需要时加载额外信息的方法。通常检索此信息在资源和时间两方面都是昂贵的,所以您只想在需要时获取它。
- Hierarchy 特性,用来在设计中建立层次或结构。例如,UserInfo Class 与 Account Class 之间具有一对多的关系。可以添加一种方法,以便通过 UserInfo Class 直接访问 Account 信息。这种方法可与 On-Demand Loading 特性一起使用。
注意: 这是一个本文未涉及但却包括在实现中的 ECC 高级概念。
在此样例实现中,为了获得灵活性,每个共公接口特性都是使用 Variant 数据类型定义的。虽然这些特性可以设置为其它固有数据类型,但 Variant 却为脚本客户机提供按引用传递项目的能力。在 Visual Basic 客户机中,Variant 数据类型可被类型转换为特定的数据类型,这将消除性能问题。仍唯一一个仍会存在的问题是:作为 Variant 这样一个参数的模糊类型说明可以是任何东西。用文档说明这些接口并将此信息发布给其他开发人员使用,可以克服这一问题。另一个好处是,能够更改数据类型而无须重新定义接口。这使接口能够更好地适应变化的业务需求。
每个特性的值都存储在私有变量中。它们可被类型转换为相应的数据类型,以强制实施这些值的类型检查。这样就可以发出相应的错误来通知用户使用了无效的数据类型。还鼓励将值按它们真正的存储状态存储在私有变量中。当数据库可以存储 NULL 而数据类型(如 Integer)不能时,这就可能引起问题。为了解决这个问题,此样例实现为大多数私有变量使用 Variant,以便既处理有效的数据值,也处理 NULL 条件。
为需要为此应用程序创建的每个实体重复这一过程。
Class 的实现
Get 特性负责将数据返回给调用者,可能使用适用的特殊格式化条件。一个典型的 Get 语句如下所示:
Public Property Get UserID() As Variant On Error GoTo ErrorHandler If Me.ClassStorage Then '用于获取原始数据(未格式化的) UserID = mvarUserID Else '目前不需要任何特殊的格式化 UserID = mvarUserID End If <...> End Property
上面的 If..Else 语句,或者它的某些变化形式,应该出现在每个 Get 属性中。基本规则如下:
- 检查以确定是否应该绕过业务规则 (ClassStorage)
- 检查安全性或其它 Class 级规则(未在本例中实现)
- 为客户格式化或准备数据
您可能已经注意到:If..Else 语句计算同一个条件,因为预先布置了 Get 特性的代码结构;如果这一情况在需要不同功能的地方出现,则可以很容易地实现它。这种特性结构对代码生成程序(如 Visio 和 Visual Modeler)是非常直观的。为这些工具提供模板会显著增加生产效率,并减少对项目的时间限制。
Let 特性负责检查数据类型、强制实施业务规则以及将局部(私有)变量设置为传入的值。每个特性有一个相关联的局部变量,该变量与特性的名称相同,名称前带有一个字母 m(对于成员变量)和一个类型(匈牙利表示法)。ID 特性的本地副本将是:mvarUserID。一个典型的 Let 特性如下所示:
Public Property Let UserID(ByVal vData As Variant) Dim errNum as long Dim errSrc as string On Error GoTo ErrorHandler If Me.ClassStorage Then '用来更改原始数据(未格式化的) mvarUserID = vData ElseIf Me.DeleteFlag Then Err.Raise ERR_USERINFODELETEFLAGSET, "CUserInfo.UserID LET", _ "Can't change the value because the record is marked " & _ "for deletion." GoTo CleanExit 'Note, this provides a safety net Else '禁止错误处理捕获类型不匹配 On error Resume Next mvarUserID = vData if err.number <> 0 then '使用基本错误加上应用程序标志 errNum = Err.Number Or OBPExBusinessRule Or OBPUserError errSrc = Err.source Err.Clear 'reset the error state On Error GoTo ErrorHandler Err.Raise errNum, errSrc, "Please enter a valid value" Else On Error Goto ErrorHandler end if '在此处检查业务条件 Me.Dirty = True End If <...> End Property
上面的第一个 If..Else 行,或者它的某个变化形式,应该出现在每个 Let 特性中,以确保接收到的数据对此特性是有效的数据类型,并确保调用者确实传入了某个值。基本规则如下:
- 检查以确定是否应该绕过业务规则检查 (ClassStorage)。
- 检查安全性和其它 Class 级规则(如删除条件)。
- 如有必要,检查以确定此变量是否未初始化。
- 检查数据类型。
- 检查业务规则(如长度)。
- 为内部值格式化或准备数据(未在本例中说明)。
- 设置 Dirty 条件(不用于只读)。
第一步是确定是否应该绕过业务规则检查。请注意,在 Class 完全置入以后,它的 ClassStorage 特性就被关闭。这样,对此 Class 的后续调用将检查并强制实施业务规则。这样就减少了加载 Collection 和Class 的开销,并允许访问原始数据。
在强制实施业务规则以后,下一步就是检查是否对此 Class 作了删除标记。如果是,则更新此特性的值毫无意义。本例中在遇到此条件时会发出一个错误。
最后一步是强制实施业务规则。您可以添加一项检查来强制 UserID 的长度必须多于 n 个字符(实际上,最小长度值应该从系统参数表中读出)。如果数据传递这些测试,则它将此值赋给本地副本。
在本例中,还设置了另一个称为 Dirty 的特性变量。Dirty 表明数据是否已被更改。请注意,没有使用成员变量 mvarDirty 来直接设置这个值;而是使用 Dirty 属性的 Let 方法来设置它。这就保证了即使在 Class 内部设置此特性,也会强制实施这些业务规则。
注意: 通过 Let 方法设置变量会存在一些开销,所以在某些情况下直接使用私有变量是一种可以接受的做法。但请小心,这将使得组件的调试和维护更加困难。
在上面的示例中,发出了一个称为 Goto CleanExit 的错误。它永远不应该被执行,而应该用来说明程序流程。
UserInfo Class 也包含一个称为 Account 的方法。这说明了 ECC 设计模式对“On-Demand 加载”(在需要时加载对象的能力)和“层次”(将不同 ECC 组件连接在一起的能力)的实现。因为 UserInfo 和 Account 实体有关系(一对多),所以这一实现允许开发人员根据现有的 UserInfo 对象检索 Account 对象。因此,它允许如下简单而直观的代码:
Set objCAccount = objUserInfo.Account.Item(1)
在下面的示例中,Account 方法将返回一个 Collection (ColAccount),该 Collection 包含 Account Class (CAccount)。使用了后联编,在开发周期中提供了更大的灵活性。可以通过为组件生成 TLB 文件来避免后联编。Account 对象,分别为 CAccount 和 ColAccount,驻留在 UserInfo 对象的不同 DLL 中,并被标记为 PublicNotCreatable,所以您必须调用 Account Engine 来创建和检索这些对象:
Public Property Get Account() As Object On Error GoTo ErrorHandler Dim engAccount As Object Dim ErrorNum As Long If mcolAccount Is Nothing Then If Not SafeCreateObject(engAccount, OBP_ACCOUNT_ENG, _ ErrorNum) Then Err.Raise ERR_ACCOUNTSAFECREATEFAILED, _ "IOBPUserInfoEng.Account Property", "Unable to " & _ "create " & OBP_ACCOUNT_ENG & ". Return Code: " & _ ErrorNum GoTo CleanExit End If '只获取未删除的记录(默认条件)。 Set mcolAccount = engAccount.Search(SecurityToken := _ SecurityToken, UserNumber := Me.UserNumber) End If Set Account = mcolAccount <...> End Property
再仔细看一下:首先,检查内部变量看当前是否有引用指向 Account Collection。如果有,则返回此 Collection;否则,调用 Account Engine 的 Search 方法来返回与当前的 UserNumber 相关联的一个或多个 Account。这意味着对于 ColUserInfo Collection 中的每个 CUserInfo Class,都将有一个特定的 Account 或多个 Account — 对应该单个 UserNumber; 换句话说,一个层次结构。此外,在调用 Account 方法之前,CUserInfo 中并不存在 Account Collection,因此实现了按需要加载。因为对 Search 方法的内部调用只完成了一次,所以像 Refresh 这样的参数可以被添加到该对象的接口中。这就允许动态重新加载内容而无须破坏和重新创建 CUserInfo。这说明构建在 ECC 中的可重用性和层次结构中的松散耦合对象的价值。它还允许以最小的影响更改实现的细节。用户永远不会知道在这个罩盖之下发生的一切。一个样例用户的代码行如下所示:
strAccountNumber = ColUserInfo.Item(1).Account.Item(1).Number
准确了解在此行中发生的情况是重要的。首先,假定您已经创建了一个称为 ColUserInfo 的 UserInfo Collection,其中每个用户至少有一个账户。第一步,ColUserInfo.Item(1),调用 Item 方法并返回与该用户对应的 CUserInfo Class。下一步,.Account,创建一个 Account Engine,此 Engine 又创建一个 Account Collection,并向其中置入一个或多个 CAccount Class,然后返回此 Collection。再下一步,.Item(1),调用 Account Collection 的 Item 方法,此方法返回第一个 CAccount Class。最后一步,.Number,调用 Number Property Get 方法并返回带有此账户编号的一个字符串,该字符串又被赋给 strAccountNumber 变量。
获取一个特性需要做大量的工作,访问该特定对象的每个特性都意味着为所访问的每个特性创建一个 Collection 和 Class。在这种情况下,将此代码写为如下的形式较好:
Dim ColAccounts as colAccount Set ColAccounts = UserInfo.Item(1).Account strAccountCode = ColAcounts.Item(1).Number strPhone = ColAccounts.Item(1).CustPhone strName = ColAccounts.Item(1).CustName
使用这种方法,就避免了对 UserInfo 的 Account 方法所进行的几个调用的开销。也可能使用一个 For..Each 结构。当仅访问一个账户时,可以编写以下的代码:
Dim objAccount as CAccount Set objAccount = UserInfo.Item(1).Account.Item(1) strAccountCode = objAccount.Number strPhone = objAccount.Phone strName = objAccount.CustName
这是非常有效的,并减少了为每个账户属性多次调用 Items() 方法的开销。它同样应该提供最佳的性能。
Collection 的设计
Collection 是 Class 对象的容器,也是内部存储方案的包装。大多数面向对象的开发人员,特别是 Visual Basic 开发人员应该熟悉 Collection/Class 模型。在 ECC 模型中,Collection 的主要责任是:
- 提供一个存储 Class 的容器
- 将接口与容器的实现细节分开
- 为操纵容器中的 Class 提供服务
- 保持 Class 的信息
- 连接其它对象来模拟复杂的关系或层次结构(可选)
可以用许多不同的方法来实现 Collection 的内部存储:数组、链接列表、记录集、XML 或者另一个公用数据存储模型。此处,重要的概念是:实际存储对于高级开发人员是隐藏的,他们总是以相同的方式访问数据,而不管它的存储形式。与 Class 一样,Collection 的接口和实现也是分开的,这就减少了更改实现带来的影响。
下面讨论一个存储 CUserInfo Class 的样例 Collection 以及 ECC 设计模式建议的其它特性/方法。Visual Modeler 用来生成初始图形。
注意: 该对象的前缀 col 表示一个 Collection。
每个 Class 都有一个单独的 Collection,这样就会获得更大的灵活性,因为它允许为 Class 适当地定制接口。其中的一个例子便是 Add(),它要求传入特定的 UserInfo 参数。
图 5. UserInfo 实体的基本 Collection 图
对 Collection 接口的基本建议,Collection 接口提供创建、读取、更新和删除 (CRUD) 功能,如下所示:
- Add—Long,Public Function()— 用来根据已定义的参数向 Collection 中添加一个新 Class。例如,此实现要求新的 UserInfo Class 在创建一个新 Class 之前需要有四个参数。它返回新对象的索引。
注意: Add() 方法应该将新 Class 放在 Collection 的尾部,否则现有的索引将失效。它也可以检查此项目是否事先已经存在(特定于实现的)。
- Count—Long,Public Get()— 用来返回 Collection 中 Class 的数目。
- Delete—Boolean,Public Function()— 用来删除来自外部数据源的一个或多个持久 Class。
- Item——Class Object,Public Get() 用来根据索引偏移量检索 Collection 中的项目。这可能被扩展,用来支持其它索引类型或值。
- Load—Boolean,Friend Function()— Engine 用它来加载 Collection 和 Class 的初始状态。这与对象的构造函数非常类似。应该对此方法加以保护,以避免安全问题和数据完整性问题。
- Remove—Boolean,Public Function ()— 用来根据索引关键字从 Collection 中删除一个 Class(仅在内存中)。
- SecurityToken—Variant,Friend Let(),Friend Get()— 用来存储用户的安全符号。为安全起见,这需要加以保护。
- Update—Boolean,Public Function()— 用于将一个或多个 Class 持久化到数据源中。此函数既可插入,亦可更新,这取决于 Dirty 和 IsNew 的值。
下面是一些其它特定于实现的特性/方法:
- Clear—Public Sub()— 删除 Collection 中的项目(仅在内存中)。这对内存管理和特殊的功能(如撤消)有好处。
- MarkForDelete—Boolean,Public Function()— 用来为项目作删除标记 (DeleteFlag)。在调用 Update 方法之前这不会在网络上传输。当需要对 Collection 中的多个项目作删除标记时,此方法很方便。
- NewEnum—IUnknown,Public Function(),Iterator— 用在 Collection 中的一个方法。此方法可在 Visual Basic 的 For..Each 语法中使用。这是说明内部存储对象功能的用法的一个示例。
注意: 此函数需要内部存储的支持,否则就需要附加的定制代码。
- Store—Byte Array,Public Function()— 以数据流的形式返回 Collection 的内部存储。
注意: 这是一个本文未涉及但却包括在实现中的 ECC 高级概念。
- UnDelete—Boolean,Public Function()— 取消一个项目的删除标记(与 MarkForDelete 相对)。
此模块中的大多数方法都是自含的,但 Delete 和 Update 方法实际上对数据访问层进行了外部调用。它们的部分关键功能包括:
- 将 Class 信息打包到某个输送结构(variant()、记录集)中。
- 确定需要执行哪种命令(存储过程、接口、插入、更新、删除)。
- 将 Class 信息发送到外部源(数据访问层、ADO、COMTI)。
- 将结果展开到内部存储器中。
可为 Collection 添加的其它功能包括:
- 根据 Collection 中的 Class 返回各种值的导出特性或方法。例如,如果 Collection 中包含的是 Student Class,则可以编写一个方法来计算 Collection 中学生的平均成绩。
- On-Demand Loading 特性或用来在需要时加载额外信息的方法。通常,检索此信息在资源和时间两方面都是昂贵的,所以您只想在需要时获取它。
为需要为此应用程序创建的每个 Collection 重复这一过程。
Collection 的实现
在大多数实现中,Collection 是对 Visual Basic 的 Collection 的一个封装,Visual Basic 的 Collection 驻留在 Collection 的私有部分中。在大多数情况下,Collection 将包含以下方法:Item()、Count()、Load()、 Add()、Delete() 和 Update()。
下面是对每种方法的讨论。
Item():
Public Property Get Item(ByVal Index As Variant) As CUserInfo On Error GoTo ErrorHandler Dim mintCodeID As Integer Set Item = Nothing If mCol Is Nothing Then GoTo CleanExit End If If mCol.Count = 0 Then GoTo CleanExit End If If Trim(Index & "") = "" Or Index <= 0 Then GoTo CleanExit Else '将索引转换为一个整数,否则 Collection 不会工作 mintCodeID = CInt(Index) Set Item = mCol.Item(mintCodeID) End If <...> End Property
Item() 返回一个存储在 Collection 中的 CUserInfo。它首先进行默认条件检查:空、范围和类型,然后为该 Class 设置一个引用。
此该函数可能因所用的内部存储模型的不同而有所不同。对于此样例实现的绝大部分而言,虽然使用 Visual Basic 的 Collection,但样例代码确实包含了另一种存储模型的一个样例。
注意: 这是一个本文未涉及但却包括在实现中的 ECC 高级概念。
Count():
Public Property Get Count() As Variant On Error GoTo ErrorHandler If mCol Is Nothing Then Count = 0 Else Count = mCol.Count End If <...> End Property
Count() 返回零或存储在内部的 Class 的数目。
注意: 如果将“可变数组”用于内部存储机制,则可用像 Ubound 这样的函数来确定项目数。
Load():
Friend Function Load(ByVal SecurityToken As Variant, ByVal _ FilledStorage As Variant) As Variant On Error GoTo ErrorHandler Load = False mvarSecurityToken = SecurityToken Set mCol = FilledStorage Load = True <...> End Function
Load() 方法假定 Engine 已经在内部存储中填入了必要的数据。它随后只是将数据赋给一个局部变量。允许 Engine 填充内部存储提供了更大的灵活性,因为它知道原始请求。它同时也存储了 SecurityToken 的一个副本,以供以后需要用 SecurityToken 来进行处理的请求使用。
Add():
Public Function Add(ByVal UserID As Variant, ByVal UserTypeID As _ Variant, ByVal Password As Variant, ByVal Name As Variant) _ As Variant On Error GoTo ErrorHandler Dim NewTemp As CUserInfo '设置默认返回值 Add = -1 '检查参数值 If UserID = "" Or UserTypeID = "" Or Password = "" _ Or Name = "" Then Err.Raise ERR_USERINFOFAILEDADDCHECK, _ "ColUserInfo.Add PROC", "You must specify values" GoTo CleanExit End If If mCol Is Nothing Then Set mCol = New Collection End If Set NewTemp = New CUserInfo With NewTemp '设置所需要的字段 .ClassStorage = True .UserNumber = 0 .UserID = UserID .UserTypeID = UserTypeID .Name = Name <...> .Password = Password .SecurityToken = SecurityToken .ClassStorage = False End With '将项目添加到 Collection 的尾部 If mCol.Count <> 0 Then mCol.Add NewTemp, , , mCol.Count Else mCol.Add NewTemp End If Add = mCol.Count <...> End Function
Add() 方法首先检查所传递的参数是否符合业务规则。随后创建一个用户 Class (CUser),将默认值赋给特性,并将 CUser 添加到内部存储中。如果尚未创建内部 Collection,则此方法同时创建内部 Collection 的成员。此函数返回此 Collection 中 Class 的项目编号,以便调用者可以立即访问此项目。
Delete():
注意: 请记住,此方法会通过网络访问外部源。
Public Function Delete(Optional ByVal Index As Variant) As Variant On Error GoTo ErrorHandler Dim ErrorNum As Long Dim oDALEng As IOBPDA.IOBPConnection Dim oCUserInfo As CUserInfo Dim vParam() As Variant Dim LowerLimit As Long Dim Upperlimit As Long Dim inx As Long Dim SPName As String Delete = False If Me.Count = 0 Then 'Nothing to Update Update = True GoTo CleanExit End If '如果提供了索引,则只删除已提供的记录 If Not IsMissing(Index) Then If Index < 1 Or Index > Me.Count Then Err.Raise ERR_USERINFOINDEXOUTOFRANGE, _ "ColUserInfo.Delete PROC", "Index out of range" GoTo CleanExit Else LowerLimit = Index UpperLimit = Index End If Else LowerLimit = 1 UpperLimit = Me.Count End If If Not SafeCreateObject(oDALEng, OBP_DA_CONNECTION, ErrorNum) Then Err.Raise ERR_USERINFOSAFECREATEFAILED, _ "ColUserInfo.Delete PROC", "Unable to create " & _ OBP_DA_CONNECTION & ". Return Code was: " & ErrorNum GoTo CleanExit End If For inx = LowerLimit To UpperLimit Set oCUserInfo = Me.Item(inx) If Not oCUserInfo.IsNew Then 'Delete from DB If Not New ReDim vParam(PARMUBOUND, 1) With oCUserInfo .ClassStorage = True 'Fill Parameter Array vParam(PARMNAME, 0) = PARMNAMESP_USERINFOUSERNUMBER vParam(PARMTYPE, 0) = PARMTYPESP_USERINFOUSERNUMBER vParam(PARMLENGTH, 0) = 4 vParam(PARMDIR, 0) = adInput vParam(PARMVALUE, 0) = .UserNumber <...> .ClassStorage = False End With If Not oDALEng.Execute(SecurityToken, SP_D_USERINFO, _ vParam) Then Err.Raise ERR_USERINFODALDELETEFAILED, _ "ColUserInfo.Delete PROC", _ "Delete Failed. SPName was: " & SP_D_USERINFO GoTo CleanExit End If End If Set oCUserInfo = Nothing '从 Collection 中删除 mCol.Remove (inx) Next Delete = True <...> End Function
Delete() 方法从数据存储(通常是数据库)中删除一个或多个项目。如果此项目不是新的(IsNew = False),则它将被从数据库中删除。这是因为其 IsNew 特性设置为真的项目在数据库中不存在。此方法通过将主关键字和记录时间戳打包来完成删除;此信息被传递给数据访问对象,后者将删除此记录。此项目然后被从 Collection 中删除,以便确保客户机不会访问数据库中不存在的数据。这对于维持状态的“胖”客户机是有用的。
注意: Delete 方法可能会导致 Collection 的 ItemIndex 发生变化,尤其是在它被存储在用户代码中时。建议在 Delete() 之后刷新引用,以确保它们指向 Collection 中的正确项目。
Update():
注意: 请记住,此方法也要在网络上传输。
Public Function Update(Optional ByVal Index As Variant) As Variant On Error GoTo ErrorHandler Dim ErrorNum As Long Dim oDALEng As IOBPDA.IOBPConnection Dim rs As ADOR.Recordset Dim oCUserInfo As CUserInfo Dim vParam() As Variant Dim LowerLimit As Long Dim UpperLimit As Long Dim inx As Long Dim SPName As String Update = False If Me.Count = 0 Then '没有要更新的项目 Update = True GoTo CleanExit End If '如果提供了索引,则只更新已提供的记录 If Not IsMissing(Index) Then If Index < 1 Or Index > Me.Count Then Err.Raise ERR_USERINFOINDEXOUTOFRANGE, _ "ColUserInfo.Update PROC", "Index out of range" GoTo CleanExit Else LowerLimit = Index UpperLimit = Index End If Else LowerLimit = 1 UpperLimit = Me.Count End If If Not SafeCreateObject(oDALEng, OBP_DA_CONNECTION, ErrorNum) Then Err.Raise ERR_USERINFOSAFECREATEFAILED, _ "ColUserInfo.Update PROC", "Unable to create " & _ OBP_DA_CONNECTION & ". Return Code was: " & ErrorNum GoTo CleanExit End If For inx = LowerLimit To UpperLimit Set oCUserInfo = Me.Item(inx) If oCUserInfo.Dirty Then ReDim vParam(PARMUBOUND, 16) With oCUserInfo .ClassStorage = True '填充参数数组 vParam(PARMNAME, 0) = PARMNAMESP_USERINFOUSERNUMBER vParam(PARMTYPE, 0) = PARMTYPESP_USERINFOUSERNUMBER vParam(PARMLENGTH, 0) = 0 vParam(PARMDIR, 0) = adInput vParam(PARMVALUE, 0) = .UserNumber <...> .ClassStorage = False End With '检查是否将现有的记录更新到数据库中 If oCUserInfo.IsNew = False Then '更新此记录 SPName = SP_U_USERINFO Else '插入此记录 SPName = SP_I_USERINFO End If If Not oDALEng.Execute(SecurityToken, SPName, vParam, _ rs) Then Err.Raise ERR_USERINFODALUPDATEFAILED, _ "ColUserInfo.Update PROC", _ "Update to Database Failed. SPName was: " & _ SPName GoTo CleanExit ElseIf (Not rs Is Nothing) Then '如果 DB 中没有返回任何数据,则设置为 True If rs.RecordCount > 1 Then Err.Raise ERR_RETURNTOOMANYRECORDS, _ "ColUserInfo.Update PROC", _ "Update to Database Failed. Returned more " & _ "than one record. SPName was: " & SPName GoTo CleanExit Else '用返回的数据更新 Collection With oCUserInfo .ClassStorage = .UserNumber = rs.Fields(FN_USERINFOUSERNUMBER) .UserID = rs.Fields(FN_USERINFOUSERID) .UserTypeID = rs.Fields(FN_USERINFOUSERTYPEID) <...> .IsNew = False .SecurityToken = SecurityToken .ClassStorage = False .Dirty = False End With End If End If End If 'Dirty Set oCUserInfo = Nothing Next inx Update = True <...> End Function
Update() 与 Delete() 类似,因为它可以更新一个或多个项目。另一个关键功能是能够在插入和更新之间作出选择。再次用到了 IsNew 特性,但是在这种情况下,true 表示此方法应该执行插入,而 false 则表示它应该执行更新。Class 的信息被打包,然后发送给数据访问对象,并在那里被插入或更新。一旦进行了远程更新或插入,就会更新内部 Class 来反映可能已对数据库作了更改的那些变化。例如,Recordtimestamp 将发生改变或被创建,因此该 Class 需要反映出这个新值。
有关其它函数,请参阅与本文相关的代码样例。
Engine 的设计
Engine 是位于 Collection 和 Class 之上的一个层,它是 Class 和 Collection 的单点入口。Engine 成为安全和对象创建控制的绝缘层。它的关键责任如下:
- 充当创建 Collection 和 Class 的代理
- 在创建对象之前检查安全性
- 将多个 Class 打包成一个 Collection,并将它们返回给客户
- 为访问 Collection 和 Class 提供一个标准接口
- 提供业务特定的处理
Engine 是用于特殊对象的公共接口,并且是 ECC 模型中唯一可公共创建的 Class。这有很多好处。作为唯一一种可公共创建的对象,Engine 独立存在并实现一般的业务功能。由于具备这种功能,Engine 被称为服务,并且没有相应的 Collection 和 Class。一个服务对象的例子是:只进行业务处理并返回一个值。IOBPSecurity 的实现说明服务对象如何工作。
在发挥 Engine 更强大功效的情况下,它将与 Collection 和 Class 联合使用。在这种情况下,Engine 是其它对象的主要接口,并能控制它们的创建方式。如图 2 所说明的那样,Engine 在一定程度上与 Collection 和 Class 有关联,因为它必须知道如何检索 Class 的信息,将它打包,然后将它们置入 Collection 中。但是,在实现细节方面,Engine 与这些对象之间只是一种松散耦合关系。这样设计 Engine 的几个主要原因是:
- 对对象创建强制实施安全性。不经 Engine 验证,任何人(或进程、或服务器,等等)都不能创建 Collection 或 Class。验证可由外部组件完成,因此可以自由更改应用程序的安全性。
- 将访问对象的接口与它们的实现分开,这样 Collection 和 Class 的实现就可以改变而不会影响调用此 Engine 的客户(或多个客户)。接口也提供了调用这些对象的标准方法。
- 隐藏(或封装)对象的创建。Engine 可用多种不同的方法执行 Collection 和 Class 的创建请求。在每个方案中,用户都不会受到影响,并且开发人员具有更大的灵活性。
Engine 的关键作用之一是提供一种方法来打包并返回 Class。这一功能在一个或多个带有前缀 Get 的方法中得到了实现。Engine 不返回单个 Class。它首先将各个 Class 放入一个 Collection,然后返回这个 Collection。因此,要使 Engine 能够在 ECC 模型中工作,它必须提供一种方法来返回 Class。
下面将讨论一个创建 CUserInfo 和 ColUserInfo 的样例 Engine;然后讨论建议应该包括在 ECC 设计模式中的其它特性/方法。Visual Modeler 用来生成初始图形。
注意: ECC 设计模式使用一致的对象命名约定。在 Engine 级,对象名是一个前缀和后缀的组合。前缀分为两部分:"I" 表示接口,"OBP" 表示应用程序特定的代码或首字母缩写词。后缀 "Eng" 表示 Engine。这种命名约定有两个作用:在注册表中可以更容易地找到对象,对象在 Visual Basic IDE References Window 中被组合在一起。
图 6:UserInfo 实体的基本 Engine 图
在本例中,关键方法被称为 GetUserInfo。语法通常是 Get。这使 Engine 能够返回一个或多个 Collection 和 Class。实现这一点的另一种方法是使用更具体的方法,例如 GetUsersByEMail。这种方法将返回某特定电子邮件地址的多个用户,这是非常直观的。本样例实现中选择的方法使用一个更一般的 Search 方法,它接受几个不同的可选参数作为查找依据,并返回不同的用户。实现细节待您来决定。
可以将业务特定的其它功能添加到 UserInfo Engine 中。例如,可以提供一个 EnrollUser 函数作为向系统中注册用户的方法。如上所述,此处就是将实体必须实现的过程合并到 ECC 设计中的地方。这些过程表面看起来各就其位 — 与展示这个功能的对象在一起。此外,Engine 是无状态对象;因此,它不应该有特性。
下面是应该在此实现中提供的基本方法:
- Get 负责创建 Class,向这些 Class 中填充信息并将它们放入一个 Collection 中,然后返回此 Collection。至少应该有一种方法属于此类型。这个方法通常按其主关键字检索项目。当找不到记录时,此方法(或返回一个 Collection 的 Engine 方法)的默认行为是返回不包含任何 Class 的一个 Collection (col.Count = 0)。返回一个空 Collection 的原因旨在使客户能够调用 col.Add(),而不是返回到 Engine 并调用 New() 方法。这种处理方法使客户能够检查计数,然后立即调用 Add() 方法和返回的 Collection。另一个默认行为是错误条件;这会在出现某些错误时发生。当出现这种情况时,应该做两件事情:首先将返回引用设置为空,然后向客户(调用者)发出一个错误。
其它方法包括:
- Search 与 Get 非常类似,但这种方法可以用在任何字段上。最好将数据参数定义为可选,以便支持增长的需要。基层的代码将决定用户是否传递了足够的数据来进行相应的搜索。
- New 为添加 Class 创建一个空 Collection。这对最初不需要到数据源的往返的数据项有好处。
- Restore 与 Collection 上的 Store 方法相对。它允许客户还原(或重新组合)Collection 和 Class 的持久状态。
注意: 这是一个本文未涉及但却包括在实现中的 ECC 高级概念。
可能向 Engine 中添加的其它功能包括:
- 基于服务的方法,GetUserType 就是它的一个例子。这将根据 UserNumber 返回 UserType。
Engine 的实现
Engine 负责创建 Collection,并用一个或多个 Class 填充它。由于具备这种功能,Engine 被称为类工厂,因为它负责创建其它对象。在 ECC 设计模式中,Engine 是可以创建具体 Class 的一个很具体的工厂。下面的样例代码说明客户将如何编写代码以及如何使用 Engine 返回一个 UserInfo Collection (ColUserInfo):
Dim UserEngine As UserEng Dim Users As ColUserInfo Dim User as CUserInfo Dim strUserNumber as String Set UserEngine = New IOBPUserInfoEng strUserNumber = 10 Set Users = UserEngine.GetUserInfo("Token", strUserNumber) Set UserEngine = Nothing Set User = Users.Item(Users.Count) MsgBox User.Name & "'s E-mail address is: " & User.emailAddress
正如您所见到的那样,客户应用程序只需要知道用户编号就可以访问有关此用户的信息,而无须了解底层的细节,如所请求数据的字段名或如何处理内部存储(例如记录集或 XML dom 对象)。如果字段名或内部存储要发生变化,则这种变化只会在 Engine 中发生,而不会在使用该 Class 的客户应用程序中发生。
请注意,Engine 返回用户的一个 Collection ,一旦完成此项任务,就会结束 Engine,并可以将它删除(设置为空)。现在就可以在 Collection 中使用这些项目的特性了。在用户的 Engine 中对 GetUserInfo 方法的实现如下所示:
Public Function GetUserInfo(ByVal SecurityToken As Variant, _ Optional ByVal UserNumber As Variant, _ Optional ByVal DeletedRecords As DeleteRecordType = _ NondeletedRecords) As ColUserInfo On Error GoTo ErrorHandler Dim ErrorNum As Long Dim oDALEng As IOBPDA.IOBPConnection Dim rsUserInfo As ADOR.Recordset Dim oColUserInfo As ColUserInfo Dim oCUserInfo As CUserInfo Dim vParameters() As Variant Dim col As Collection Dim inx As Long Set GetUserInfo = Nothing '首先检查安全符号 If Trim(SecurityToken) = "" Then Err.Raise ERR_USERINFOINVALIDSECURITYTOKEN, _ "IOBPUserinfoEng.GetUserInfo PROC", _ "Invalid security token. Security Token cannot be empty." GoTo CleanExit End If ReDim vParameters(PARMUBOUND, 0) If IsMissing(UserNumber) Then UserNumber = Null ElseIf Trim(UserNumber) = "" Then UserNumber = Null End If '开始将参数打包 vParameters(PARMNAME, 0) = PARMNAMESP_USERINFOUSERNUMBER '名称 vParameters(PARMTYPE, 0) = PARMTYPESP_USERINFOUSERNUMBER '类型 vParameters(PARMLENGTH, 0) = 0 '大小 vParameters(PARMDIR, 0) = adInput '方向 vParameters(PARMVALUE, 0) = UserNumber '值 If DeletedRecords <> AllRecords Then If Not IsEmptyArray(vParameters) Then inx = UBound(vParameters, 2) + 1 ReDim Preserve vParameters(PARMUBOUND, inx) Else inx = 0 ReDim vParameters(PARMUBOUND, inx) End If vParameters(PARMNAME, inx) = PARMNAMESP_USERINFODELETEFLAG vParameters(PARMTYPE, inx) = PARMTYPESP_USERINFODELETEFLAG vParameters(PARMLENGTH, inx) = 1 '大小 vParameters(PARMDIR, inx) = adInput '方向 vParameters(PARMVALUE, inx) = DeletedRecords '值 End If '创建“数据访问层”对象 If Not SafeCreateObject(oDALEng, OBP_DA_CONNECTION, ErrorNum) Then Err.Raise ERR_USERINFOSAFECREATEFAILED, _ "IOBPUserInfoEng.Get PROC", "Unable to create " & _ OBP_DA_CONNECTION & ". Return Code was: " & ErrorNum GoTo CleanExit End If '执行 SQL 来检索记录 If Not IsEmptyArray(vParameters) Then If Not oDALEng.Execute(SecurityToken, SP_S_USERINFO, _ vParameters, rsUserInfo) Then Err.Raise ERR_USERINFODALCALLFAILED, _ "IOBPUserInfoEng.Get PROC", _ "Call to Database Failed. SPName was: " & SP_S_BILL GoTo CleanExit End If Else If Not oDALEng.Execute(SecurityToken, SP_S_USERINFO, , _ rsUserInfo) Then Err.Raise ERR_USERINFODALCALLFAILED, _ "IOBPUserInfoEng.Get PROC", _ "Call to Database Failed. SPName was: " & SP_S_BILL GoTo CleanExit End If End If Set oDALEng = Nothing If (Not rsUserInfo Is Nothing) Then '开始将数据打包到多个 Class 对象中 Set oColUserInfo = New ColUserInfo Set col = New Collection Do Until rsUserInfo.EOF Set oCUserInfo = New CUserInfo With oCUserInfo .ClassStorage = True .UserNumber = rsUserInfo.Fields(FN_USERINFOUSERNUMBER) .UserID = rsUserInfo.Fields(FN_USERINFOUSERID) .UserTypeID = rsUserInfo.Fields(FN_USERINFOUSERTYPEID) <...> .SecurityToken = SecurityToken .IsNew = False .ClassStorage = False .Dirty = False End With col.Add oCUserInfo rsUserInfo.MoveNext Set oCUserInfo = Nothing Loop '将信息置入 Collection 对象 If Not oColUserInfo.Load(SecurityToken, col) Then Err.Raise ERR_USERINFOLOADFAILED, _ "IOBPUserInfoEng.GetUserInfo PROC", _ "Load failed for private collection" GoTo CleanExit End If '成功 Set GetUserInfo = oColUserInfo End If <...> End Function
GetUserInfo 方法是说明应该如何定义 Get Engine 方法的主要示例。下面是基本建议:
- 检查安全性,要么确保调用者有调用此方法的权限,要么强制实施基于角色的安全性。
- 将传递的参数或其它所需的字段打包。
- 调用数据源(我们的实现使用一个“数据访问层”对象)来检索数据。
- 将数据解包到多个 Class 中。
- 填充 Collection 并将它返回给调用者。
当业务组件与客户在同一进程空间中运行时,ECC 模型的效率最高。广泛使用对象特性是其主要原因。对某个特性的每个调用都需要从客户到此对象的一个往返。如果这些往返跨越进程边界,则开销变得相当可观。在基于 Web 的解决方案中,这意味着这些对象与 Web 站点在同一进程空间中运行或尽可能地接近。在这种情况下,最好将 Web 站点配置为在单独的内存空间中运行。这提供了错误隔离功能,从而使 Web 服务器能够在进程中止以后重新启动此进程。
本文的作者已经使用此方法在多个项目或客户身上取得了巨大成功。我们发现:ECC 设计模式最强有力的部分就是它所倡导的类似模板的方法。作为项目主持人,我们能够向开发人员讲授此模式,然后很快地复查不符合它的代码。因为它是经过此前对该实现的改进所证实的一个模式,所以它使我们可以用更多的时间来考察客户特定的要求。此外,大多数代码成为剪切和粘贴操作;一旦创建了第一个业务组件(模板),其它组件就可以用它作为一个起点。一般说来,对象的调试和设计在所有组件中变得更加通用,从而使开发人员更容易地交换任务。在项目的设计阶段会出现多次改动,范围从 UI 到数据库。我们能够在对系统产生最小影响的情况下实施许多更改。下面是所进行的更改的几个示例:
- 数据传输格式 最初的设计将 ADO 记录集用作数据传输格式。一个设计更改要求应用程序将数据传输换为 XML。如果使用以前的方法将记录集返回给 ASP,则我们将必须修改应用程序中的每一段代码。ECC 为开发人员在数据传输和 ASP 代码之间提供了一个隔离层。该层大大减少了影响,因为 Active Server Pages 的数量比组件数量大十几倍。
- 安全性 最初的设计使用实体级的安全性,具有传统的 CRUD 功能。安全性是在 Engine 和 Collection 级实现的。一个设计更改要求设计需要具有列级安全权限。因为 ECC 倡导使用映射到列的特性,所以我们很快就能实现这一要求。为此,我们必须在 Class 级存储 SecurityToken(通过 Engine 或 Collection 方法),并在调用相应的特性方法时检查用户的权限。
- 加密 最初的设计将 Store/Restore 方法用于组件的持久化。客户所关心的:Byte() 在存储到数据库后是否是可读的格式。因为我们不使用内建的 IPersist 方法,所以能够将加密/解密添加到相应的方法中。
注意: 如果 Class 是公共可创建的,则在 Visual Basic 中只能使用 IPersist;而不能使用 Collection 和 Class 方法。
- 高速缓存 最初的设计受到很多性能问题的困扰,尤其是为显示一个页面而在数据库之间多次往返时。存在问题的页面具有多个下拉式列表,每个都对应一个单独的往返,而每次又都要带着多个用户进行往返。为了提高性能,我们在相应的组件中加入了高速缓存。我们还修改了 Engine,使其在到数据库的往返之前首先检查高速缓存。
这些只是说明 ECC 设计模式的灵活性的少数几个例子。
Engine-Collection-Class 设计模式提供了一种与实现无关的应用程序框架。这一体系结构由以下几部分组成:Engine,用来控制创建 Class;Collection,用来存储 Class;Class,用来表示现实世界的一个实体。这种对象化使对象的创建非常明确,并为业务规则验证、数据检索和数据处理提供了一致的接口。此外,它还提供了一种松散耦合的体系结构,在这种体系结构下,客户和组件都可以自由变化而不会彼此影响。
与纯粹基于服务的方法不同,ECC 首先为实体的数据建模,然后才为过程建模。这意味着单个组件可以重新用于业务逻辑的许多不同部分,而且业务规则将封装在一个组件中。
著作
Eddon,Guy 和 Henry Eddon。Programming Components with Microsoft Visual Basic 6.0, 2nd Edition。Redmond,WA:Microsoft Press,1998。
Kirtland,Mary。Designing Component Based Applications。Redmond,WA:Microsoft Press,1999。
Pattison,Ted。Programming Distributed Applications with COM and Microsoft Visual Basic 6.0。Redmond,WA:Microsoft Press,1998。
Platt,David S。Understanding COM+。Redmond,WA:Microsoft Press,1999。
Stamatakis,William。Visual Basic Design Patterns。Redmond,WA:Microsoft Press,2000。
文章
用您自己的登录代理在 windows NT® Windows® 2000 中处理登录适宜(英文)
最大的 Windows DNA 性能错误以及避免方法(英文)
高级的基础知识: 事务编程(英文)
来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/10752043/viewspace-990510/,如需转载,请注明出处,否则将追究法律责任。
转载于:http://blog.itpub.net/10752043/viewspace-990510/