Effective Visual Basic (Visual Basic高效编程)(二)

??. 构建和使用基于COM的组件

# COM背景知识 http://www.develop.com/tutorials/vbcom

2.1 规则2-1:从接口的角度进行思考

# 只要一个类的接口不变(或仅增加新的属性/方法),类的客户就不必变化

2.2 规则2-2:使用自定义接口

# 类可以通过不断引入新的自定义接口来实现新的功能,同时保持对原有客户端程序的兼容性。

# 类模块的公开属性和方法构成其默认接口

# VB 一般通过 PublicNotCreatable 类模块自定义接口

# Implements IEmployee接口的类CConsultant,其默认接口即使为空,也可用来作为TypeOf判断的依据。 If TypeOf rEmp Is CConsultant

# 自定义接口发布后必须保持二进制兼容(#2.5),VB允许默认接口通过增加方法扩展(#2.5.3)

# 脚本环境无法使用自定义接口,需要采取其他措施(#4.5)

2.3 规则2-3:最好使用IDL独立定义自定义接口

# 通过idl文件定义接口,用MIDL编译成TypeLib,则可以避免使用PublicNotCreatable类模块定义接口定义接口类库带来的冗余类模块,接口可能被扩展而破坏兼容性等缺点。

# 用OLEView.exe可以反编译ActiveX DLL中的TypeLib生成idl文件,修改后再编译成TypeLib

需要修改的内容包括:
Single->float
struct tagX{…} -> struct X{…}
删除coclass等多余内容
去掉接口名前缀下划线
odl->object, IDispatch->IUnknown
添加helpstring

例子:
//typelib fileneme: Interfaces.DLL
[
uuid(…),
version(1.0),
helpstring("")
]
library Interfaces
{
//TLib
//TLib: Microsoft ADO: {}
importlib("msado15.dll");
//TLib: OLE Automation: {}
importlib("stdole2.tlb");
}

[
object/*odl*/,
uuid(…),
oleautomation
]
interface IEmployee: IUnknown/*IDispatch*/ {
[id(1), propget]
HRESULT Name([out,retval] BSTR* Name);
[id(2), propput]
HRESULT Name([in] BSTR* Name);
[id(3)]
HRESULT ReadFromDB([in, out] _Recordset **);
[id(4)]
HRESULT IssuePaycheck([out, retval] CURRENCY*);
};

2.4 规则2-4:使用自定义回调避免基于类的事件的局限性

# VB6基于类的事件处理机制有一些限制:
WithEvents只能用于模块级的引用变量
数组、集合、局部变量不能在事件处理机制中使用
事件必须定义在类模块中,因而无法定义多个类共用的事件,这意味着不能在自定义接口中包含事件。

# 自定义回调方案
##
‘** IRegisterClient
Option Explicit

Public Enum IRegisterClientErrors
eIntfNotImplemented = vbObjectError + 8193
eAlreadyRegistered
eNotRegistered
End Enum

Public Sub Register(rClient As Object)
End Sub

Public Sub Unregister(rClient As Object)
End Sub
需要引发事件的对象应当实现上述接口,以便客户可以注册自身,要求接收事件。

##
‘** IEmployeeEvents 希望接收IEmployee对象事件的对象实现此接口
Public Sub Changed(rEmp As IEmployee)
End Sub
希望接收事件的客户应当实现类似上述这样的接口,对应对象可能引发的各种事件,分别提供回调函数。

## 引发事件的对象示例
‘** CConsultant
Option Explicit

Implements IEmployee
Implements IRegisterClient

Private sName As String
Private rMyClient As IEmployeeEvents
‘** 如果需要处理多个客户,这里换成客户引用集合,Register和Unregister方法相应修改

Private Sub IRegisterClient_Register(rClient As Object)
If Not TypeOf rClient Is IEmployeeEvents Then
Err.Raise eIntfNotImplemented, …
ElseIf Not rMyClient Is Nothing Then
Err.Raise eAlreadyRegistered, …
Else
Set rMyClient = rClient
End If
End Sub

Private Sub IRegisterClient_Unregister(rClient As Object)
If Not rMyClient Is rClient Then
Err.Raise eNotRegistered
Else
Set rMyClient = Nothing
End If
End Sub

Private Property Get IEmployee_Name() As String
IEmployee_Name = sName
End Property

Private Property Let IEmployee_Name(ByVal sRHS As String)
sName = sRHS

On Error Resume Next ‘** ignore unreachable/problematic clients
rMyClient.Changed Me ‘** name was changed, so raise event
End Property

## 客户端示例
‘** FormTest
Option Explicit

Private colEmployees As New Collection

Implements IEmployeeEvents

Private Sub IEmployeeEvents_Changed(rEmp As IEmployee)
‘** Update form to reflect changes in rEmp.Name
End Sub

Private Sub Form_Load()
‘** Open DB and retrieve an RS of employee records

Dim rEmp As IEmployee, rObj As IRegisterClient

Do While Not rsEmployees.EOF
Set rEmp = CreateObject(rsEmployees.Fields.Item("ProgID").Value)
If TypeOf rEmp Is IRegisterClient Then ‘** event based
Set rObj = rEmp ‘** switch to register interface
rObj.Register Me ‘** and register myselft to receive events
End If

rEmp.ReadFromDB rsEmployees
colEmployees.Add rEmp
rsEmployees.MoveNext
Loop

‘** Close DB and RS
End Sub

Private Sub Form_Unload(Cancel As Integer)
Dim rEmp As IEmployee, rObj As IRegisterClient
Dim l As Long

For l = colEmployees.Count To 1 Step -1
Set rEmp = colEmployees.Item(l)
colEmployees.Remove l

If TypeOf rEmp Is IRegisterClient Then ‘** event based
Set rObj = rEmp ‘** switch to register interface
rEmp.Unregister Me ‘** and unregister myself
End If
Next
End Sub

如果不注销,引发事件对象当中保存的对客户的引用将导致客户不能释放。

# 注:在跨进程/机器引发事件时,要注意对象需要得到许可后才可以调用客户。

# 拥有多个客户时,可以实现基于优先级的事件通知方案。

# 补充:多个事件客户的方案,可以实现Model-View模式的关系

2.5 规则2-5:要谨慎保持兼容性

# 实现与客户端的兼容性需要关注的问题:
1. 服务器的总体功能必须在版本之间保持兼容性
2. 每一个类公开的接口不可以改变:方法不可以删除,方法名不能改动,方法的参数表不可以改变。
3. CLSID, IID, LIBID 不能改变

2.5.1 脚本客户端程序

# 脚本客户端对COM对象的默认接口进行后期绑定(基于IDispatch接口,先查找方法ID再Invoke,并且是使用Variant传递参数),不能访问自定义接口。
要对脚本客户保持兼容,只需要保持ProgId和默认接口内容不变。

# VB对象ProgId决定于工程名和类模块名。

# 由于使用Variant传递参数,接口中的方法参数表即使变化,只要能向上兼容(如Integer变成Long),也不影响客户端的使用。

# 编译环境下,如果使用Object或Variant变量保存对象引用,也使用后期绑定。只是如果使用New来创建对象,需要保持CLSID和默认接口IID不变。

2.5.2 已编译的客户端程序

# 编译环境下,使用具体的自定义/默认接口类型变量引用对象时,使用vtable绑定。必须在TypeLib中定义接口,在编程环境中引用。

# VB的兼容性选项:
## No Compatibility 每次编译时,所有GUID将被改变
## Project Compatibility CLSID, LibID都会保留,但是IID会变化。在类的设计完成之前采用。每次编译时,TypeLib版本加1
## Binary Compatibility 所有GUID将不再变化。一旦发布版本之后,就应当切换成此设置。注意保留已发布版本。
设置了Binary Compatibility之后如果改变接口(即使改变小到只有一个参数的类型)并试图编译,VB将会给出中断兼容性的提示。

2.5.3 版本兼容的接口

# VB支持在Binary Compatibility设置下,向默认接口中添加方法而不发出警告,这时VB将在原来的vtable后面增加方法入口,生成一个新的IID标识新的接口,并把对原有接口的调用Forward到新的接口,并给TLB版本加0.1。如果客户端安装程序没有在系统中注册必要的转发信息,就会导致在运行期产生COM激活错误。(参见MS KB241637)

# 安全的方法是使用自定义接口,如果不同的发布版本需要改动接口,就定义新的接口。
(#2.3)。参见MS KB190078, 190967, 191214

2.6 规则2-6:选用正确的COM激活技术

# 创建对象的几种方法:
Set rEmp = New Employees.CConsultant
Set rEmp2 = CreateObject("Employee.CConsultant")
Set rEmp3 = GetObject("", “Employee.CConsultant")
MTS环境中:
Set rEmp4 = GetObjectContext.CreateInstance("Employee.CConsultant")
ASP环境中:
Set rEmp5 = Server.CreateObject("Employees.CConsultant")

# 按具体类型的引用变量声明时,无论如何创建对象,都是通过vtable绑定的。
而引用变量是Object或Variant类型时,无论如何创建对象,都是使用后期绑定的。
VB编写的COM对象都是支持两种绑定方式的。

2.6.1 COM激活

# COM Invocation(?)是运行期创建COM对象的过程,涉及客户端程序,GUID,COM底层服务,一个或多个注册表及COM服务器。目的是(1)创建对象;(2)获得必要的接口引用。

2.6.2 New操作符

# New操作符并不总是触发COM激活(CoCreateInstanceEx执行COM激活),如果目标类模块与客户端处于同一VB工程/DLL/EXE中,或处于VB IDE打开的同一工程组中,则创建对象的过程类似C++中的New,开销比CoCreateInstanceEx小得多。

# 注意慎用As New方式的对象引用变量声明(包括使用默认的与窗体类型名称相同的窗体变量),可能引起不必要的对象创建(当一个这样声明的引用变量用在 If rObj Is Nothing 时,如果它确实已经是Nothing,首先将触发New的操作)。

2.6.3 CreateObject

# CreateObject总是使用COM激活创建对象。

# 不能用来创建 Private 或 PublicNotCreatable 型的 VB 类的实例,而只适用于已注册的Public对象,并且无论是否使用后期绑定,都要求被创建的对象实现 IDispatch 接口。ATL编写的组件可能不实现此接口。

2.6.4 GetObject

#
Set rDoc GetObject("file.doc", “Word.Document")
rDoc.Activate
指定空的文件名则可以创建新的对象,类似 CreateObject ,但不能指定远程服务器。

# 通过队列服务异步调用对象
Dim rQObj As TLibName.CQClass
Set rQObj = GetObject("Queue:/new:TLibName.CQClass")
rQObj.SomeMethod ‘** call to SomeMethod is *queued*
MsgBox “client is done”

2.6.5 GetObjectContext.CreateInstance 和 Server.CreateObject

# GetObjectContext.CreateInstance 和 Server.CreateObject 本质上是对 CreateObject 函数的包装,但使类所处的 MTS 或 ASP 环境参与进来。

# 注:在 COM+ 中使用 CreateObject 和 CreateInstance 都是安全的。在 ASP 中,在无法使用 Server.CreateObject 时,可以使用 CreateInstance。参见 MS KB193230

2.6.6 性能考虑

# 影响COM对象性能的因素当中,绑定类型和参数的marshalling特性讨论较多,但COM激活也是应当考虑的因素之一。

# 不需要COM激活时,New最高效。

# COM激活分为进程内、本地、远程。
## 进程内激活时,New 最高效。
## 进程外激活时,New 创建对象并取得默认接口, IUnknown, IPersistStreamInit, IPersistPropertyBag 4个接口引用
而 CreateObject 和 GetObject 取得 IDispatch, IUnknown, IPersistStreamInit, IPersistPropertyBag 4个接口引用(需要两次API调用和三次方法调用)
## 进程外激活需要生成 Proxy/Stub 对,有一定的开销。
## 如果对象不执行自定义编组,与默认接口相联系的 Proxy/Stub 创建开销远大于创建与 IDispatch 等COM预定义接口相联系的 Proxy/Stub 。
## 因此,如果使用后期绑定或自定义接口而不需要默认接口,New 操作过程中创建与默认接口相联系的 Proxy/Stub 的时间就浪费了。
## 注意必须使用后期绑定方式才能体现 CreateObject 的这一优势,否则如果声明早期绑定引用变量 Dim rObj As TLibName.CClass,当调用 Set rObj = CreateObject(…) 时将触发创建默认接口的 Proxy/Stub 对。
## 然而,调用对象的方法时,后期绑定方式的开销则很大。如果需要多次调用方法(大致本地>=10,远程>=3),则 CreateObject/GetObject 创建对象时的优势就被抵消了。
## 如果系统运行在Windows 2000上,使用 GetObject("new:TLibName.CClass") 创建对象只需要取得对 IDispatch 和 IUnknown 接口的引用。如果不需要使用 IPersistStreamInit 和 IPersistPropertyBag 接口,可以提高效率。
## 后期绑定的另一个不便之处:无法使用 IntelliSense 和类型检查能力。

2.7 规则2-7:慎重使用Class_Terminate

# 编写占用某些非内存资源(如文件、共享内存、网络连接、ADO Connection, RecordSet等等)的服务类时,应当提供 Close/Dispose 方法。

# 如果设计方案允许对象在关闭之后,可以重新打开或者使用,提供 Close 方法,否则提供 Dispose 方法。

2.8 规则2-8:根据会话而不是实体来建模

# 分布式应用等交互开销比较大的情况下,基于会话流程来设计类的接口,可以提高交互的效率。(#5.2)

## 基于实体建模的例子
With rCustomer
.StreetAddr = < ...>
.City = < ...>
.State = < ...>
.Zip = < ...>
End With
每个属性需要一次网络开销

## 基于会话建模的例子
rCustomer.Update <name>, <streetaddr>, &ltcity>, …
rCustomer.PlaceOrder laProducts
这种基于会话建模的可能显得不便于客户端使用,然而可以减少网络开销。
思考:设计建模的目的是要在各种限制条件下达到提高效率等设计目标,而不是仅仅追求设计的形式优美。

2.9 规则2-9:除了简单的小规模应用系统, 避免使用ActiveX可执行程序

# 进程外对象包括 ActiveX EXE 或者配置到 MTS/COM+(MTS 3.0)的 ActiveX DLL

# 进程外对象的优点:
1) 故障隔离。对象的崩溃不会引起客户的崩溃,反之亦然。
2) 单独的安全身份机制,对象可以不同与客户端的用户身份运行。
3) 多线程服务,客户可以并行地激活多个对象或执行函数调用。
4) 可以在与客户程序不同的机器上运行对象。
最后一条使得分布式多层应用系统成为可能。

# ActiveX EXE 仅被设计用来满足小规模系统的需求。MTS或者COM+还提供安全、资源共享、分布式事务以及配置/进程管理。

# ActiveX EXE 可选的线程模型:
Thread Pool = 1 单线程程序——请求需要排队
Thread Pool > 1 限制并发线程数量——
Thread per Object 为不同客户服务的对象使用不同的线程——并发能力最大,但负载大时影响性能。

# ActiveX EXE 安全性依赖于DCOMCFG.EXE配置

# 注:BAS模块中定义的全局变量只在同一个 Apartment 中是全局的,不能在进程间共享。MTS或COM+提供Shared Properties。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值