阵风和歼20:VBScript中的类

来源:百度文库 编辑:偶看新闻 时间:2024/05/01 05:10:01

摘录自"VBScript程序员参考手册(第三版)" 略有修改

尽管这个功能已经存在了一段时间,但大部分人还是不知道可以在VBScript中定义类。VBScript中的类为VBScript程序员提供了一个强大的工具,尤其是在规模较大的脚本项目中。诚然,VBScript的面向对象能力比不上Java、C++、VB.NET,甚至不如Visual Basic 6,但这确实可以让VBScript程序员在编写程序时利用一些面向对象的优点。

对象、类和组件
在开始编写和使用您自己的VBScript类之前,先介绍一些术语。近年来类(class)、对象(object)、组件(component)这些技术术语被误解、混淆了。尽管它们有着不同的含义,但这些术语经常被视为可以互换的。这使得纯粹的面向对象主义者有些抓狂,而且这还加大了新手的学习难度。这里先明确一下这些术语的含义。

严格地说,对象是复杂数据和程序结构在内存中的表现,只有在程序运行时才存在。一个合适的比喻就是数组,这也是一个只有在运行时才存在的复杂数据结构。在某段代码中使用数组时,大部分人都知道这是指的内存中的数据结构。不过,在程序员使用“对象”这个词时,并不一定是指其严格的定义,即运行时存在于内存中的数据结构。

对象与数组还是有些区别,最重要的就是对象并不只是像数组那样存放一些复杂数据(以属性的形式);对象还有“行为”(也就是说“它知道该怎么做”)这表现为方法。属性可以存放任何类型的数据,而方法则可以是过程或函数。将数据和行为一起放入对象中,这样就可以在设计程序时将被操作的数据和操作数据的代码放在一起。

类是对象的模板。对象只有在运行时才会存在于内存中,而类则是在设计时就能直接使用的程序设计结构。类是代码,而对象是在程序运行时对这段代码的使用。如果要在运行时使用对象,必须先在设计时定义一个类。在运行时会根据类所提供的模板创建对象。(这里只是用不同的方式表达同一个意思。)例如,可以编写一个名为Customer的类。保存这个类定义之后,就可以再用其他代码在内存中创建任意数量的Customer对象。

但是很多人都将类等同于对象,比如“我编写了Customer对象,然后创建了一千个Customer对象并根据他们的消费额排序”。前面说过,这会给新手造成混淆,但是随着经验的增长,您将学会如何根据上下文来理解它的真实含义。

组件只是一种打包机制,一种将一个或多个相关类编译成一个可以部署到一台或多台计算机上的二进制文件的方法。在Windows操作系统中,组件通常都是一个.DLL或.OCX文件。在程序员用其他方式编写相互关联的类时,如果他想让其他人也能在运行时使用这些类创建对象就应该将其打包,并将这些类以组件的形式发布。一个程序或是一个脚本有可能会用到很多不同组件中的类。

但是,组件并不是使用类的唯一方式。图8-1只展示了一种可能的情况(虽然是一种很常见的情况)。例如,在Visual Basic程序中可以将类与程序本身编译在一起,而不将其暴露给外部。这些类只存在于程序内部,它们的目的就是服务于这个一个程序。在这种情况下,就无需考虑将这些类打包成组件,因为它们不会被别人使用。

人们发现将类打包成独立于程序的组件更有效率。在稍后的几个类上这个想法会得到验证,并且将它们打造成一个可移植的组件能使这些类更容易重用。在VBScript中,这两种技术都可以使用:可以在脚本内创建类,只有那个脚本能使用这个类;也可以将您的类打包成Windows脚本组件(Script Component)。


类语句
创建VBScript类的关键字是Class语句。跟用Function … End Function或Sub … End Sub语句设定过程的边界类似,Class和End Class语句也适用于设定类的边界。可以在一个脚本中使用多个Class … End Class代码块定义多个类(但是在VBScript中类不能嵌套)。

如果您是从其他语言转向VBScript,可能习惯于将类定义在单独的文件中。但是这对VBScript的类并不适用。通常,您必须将定义VBScript类的代码与创建类实例的代码放在一起。

这看上去是一个很大的限制(因为创建类的一个目的就是方便代码的移植和重用)但是这里有另外两种实现这个目的的方法。

●       将一个或若干个VBScript类打包成Windows组件,在第16章中会对此做深入探讨。

●       使用ASP的# INCLUDE指示符,将类的定义存放到一个文件中,并在多个ASP页面中包含它。第20章会探讨ASP脚本中的VBScript类。

但是在这里我们只会探讨与使用类的代码定义在用一个脚本中的类。

除了是否在同一个脚本文件中的区别,Visual Basic程序员可以毫无障碍地适应VBScript类的使用。除了Visual Basic和VBScript两者在语言上的差别,VBScript类的结构和相关技术与Visual Basic都是一样的。下面是Class的基本语法。

Class MyClass

   < rest of the class code will go here >

End Class

当然,应该用您自己所定义的类的名称替换其中的MyClass。这个类的名称以及相同作用域中用INCLUDE指示符包含进来的类名称,在脚本中都应该是独一无二的。类名称还不能与VBScript的保留字(比如Function或While)重复。


定义属性
在脚本根据类创建对象时,属性是存储和访问数据的机制。通过属性,数据可以存放在对象中;也可以通过属性从对象中获取数据。

私有属性变量
存储属性的值的最好方式就是私有属性变量。这是定义为类级作用域的变量(在类的开头)。这个变量是私有的(也就是说不能直接从类的外部访问这个变量),存放有属性的值。使用类的代码通过Property Let、Set和Get过程与其进行交互,但是这些过程只是这个私有属性变量的门户而已。

可以这样定义一个私有属性变量:

Class Customer

   Private mstrName

   < rest of the class code will go here >

End Class

对于类作用域的私有变量,必须用Private语句声明。m前缀是匈牙利命名法中表示模块级作用域的,也就是类级。有些地方会用c前缀(比如cstrName)表示类级作用域。但是我们并不建议使用这种方法,因为Visual Basic程序员经常用这个前缀表示Currency数据类型,容易弄混。

Property Let
Property Let过程是一种特殊的过程,用于在类的外部给私用属性变量赋值。Property Let过程类似于VBScript中没有返回值的子过程。下面是它的语法。

Class Customer

   Private mstrName

   Public Property Let CustomerName(strName)

      mstrName = strName

   End Property

End Class

注意这里没有使用Sub或Function语句来定义这个过程,而是使用的Property Let。Property Let过程必须接收至少一个参数。没有这个参数就无法实现Property Let过程的目的,也就是使得外部代码可以将值存放到私有属性变量中。这里要注意属性过程中的代码是如何将传递给该过程的strName值保存到mstrName私有属性变量中的。这个过程中可以没有任何代码,这也就不会将传递过来的值存放到某个变量或对象中,但是这完全不符合Property Let过程的目的。

反之,也可以随意地往这个过程中添加代码。在某些情况下,可能要在将传递过来的值真正赋给私有属性变量之前先对其做一些检查。例如,如果客户姓名的长度不能超过50个字符,就需要检查strName参数是否超过了50个字符的长度;如果超过了这个长度,就用Err.Raise()方法将这个错误告诉调用代码。

最后,属性过程必须以End Property语句结束(就像Function过程必须以End Function结束,Sub过程必须以End Sub结束一样)。如果要跳出属性过程,可以使用Exit Property语句(跟用Exit Function跳出Function,用Exit Sub跳出Sub一样)。

Property Get
Property Get过程跟Property Let过程正好相反。Property Let过程是用于在类外部的代码中给私有属性变量赋值,而Property Get过程则是允许类外部的代码读取私有属性变量的值。Property Get过程跟VBScript的Function过程类似,都有返回值。这里是它的语法。

Class Customer

Private mstrName

Public Property Let CustomerName(strName)

      mstrName = strName

End Property

Public Property Get CustomerName()

    CustomerName = mstrName

End Property

End Class

与VBScript的Function过程类似,Property Get过程会给调用它的代码返回一个值。这个值通常是私有属性变量的值。注意Property Get过程的名称与相应的Property Let过程的名称是一样的。Property Let过程将值存放到私有属性变量中,而Property Get过程则将其读取出来。

Property Get过程不需要任何参数。VBScript允许加入参数,但是如果要这么做,就必须给这个属性的Property Let或Property Set过程(如果使用了这个)也添加一个额外的参数。因为Property Let/Set过程的参数必须比相应的Property Get过程的参数多一个。

给Property Let/Set过程添加一个额外的参数是很难看的,并且要求使用这个类的代码在Property Let过程中使用多于一个参数也是很不好的形式。如果觉得某个Property Get过程确实需要一个参数,那最好是添加一个额外的属性来实现这个Property Get参数的功能。

如果Property Get过程返回的是一个指向对象变量的引用,就可能需要用Set语句返回这个值。例如:

Class FileHelper

'Private FileSystemObject object

Private mobjFSO

Public Property Get FSO()

    Set FSO = mobjFSO

End Property

End Class

但是,因为所有的VBScript变量都是Variant变量,所以Set语法并不是不可或缺的。下面这种语法也能运行。

Class FileHelper

'Private FileSystemObject object

Private mobjFSO

Public Property Get FSO()

    FSO = mobjFSO

End Property

End Class

但是还是应该使用Set语法,因为这样可以明确地说明这个Property Get过程返回的是一个指向对象变量的引用。


Property Set
Property Set过程与Property Let过程很类似,但是Property Set过程是针对基于对象的属性。当需要在属性中存放对象(而不是数字、日期、布尔或字符串子类型的变量),就可以提供一个Property Set过程取代Property Let过程。下面是一个Property Set过程的语法。

Class FileHelper

'Private FileSystemObject object

Private mobjFSO

Public Property Set FSO(objFSO)

    Set mobjFSO = objFSO

End Property

End Class

从功能上来说,Property Let和Property Set过程的功能是一样的。但是Property Set过程有两个不同之处:

●       明确地说明这个属性是一个基于对象的属性(任何使得代码意义更明确的技术都比其他功能相同的技术更受偏爱)。

●       类外部的代码必须用Set Object.Property = Object才能写入这个属性(因为这是实现该功能的典型方法,所以这也是优点)。

例如,下面的代码根据前面介绍的类创建了对象。

Dim objFileHelper

Dim objFSO

Set objFSO = _WScript.CreateObject("Scripting.FileSystemObject")

Set objFileHelper = New FileHelper

Set objFileHelper.FSO = objFSO

注意最后一行代码在给FSO属性赋值时使用了Set语句。这是必须的,因为FileHelper类是用Property Set过程给FSO属性赋值的。如果没有最后一行开头的Set语句,VBScript就会报错。若类中的属性是基于对象的,通常就应该用Property Set过程。大部分使用类的程序员也都希望如此。

也就是说,因为所有的VBScript变量都是Variant变量,所以也可以用Property Let过程。但是如果用Property Let过程代替Property Set过程,使用这个类的代码就不能用Set语句设置这个属性(如果这么做VBScript就会报错),并且这会导致习惯于使用Set语法的程序员感到困惑。如果确实有必要支持这两种情况,可以对同一个属性同时提供Property Let和Property Set两个过程,如下:

Class FileHelper

'Private FileSystemObject object

Private mobjFSO

Public Property Set FSO(objFSO)

    Set mobjFSO = objFSO

End Property

Public Property Let FSO(objFSO)

    Set mobjFSO = objFSO

End Property

End Class

Property Set和Property Let中的Set是可选的。因为是直接写入Variant私有属性变量中,所以可以用Set也可以不用。这个例子的功能跟前一个例子是相同的。

Class FileHelper

'Private FileSystemObject object

Private mobjFSO

Public Property Set FSO(objFSO)

    mobjFSO = objFSO

End Property

Public Property Let FSO(objFSO)

    mobjFSO = objFSO

End Property

End Class

创建只读属性
有两种方法可以创建类的只读属性:

●       只为这个属性提供Property Get过程。

●       将Property Get过程声明为公共的,而将Property Let过程声明为私有的。

下面是第一种方法:

Class Customer

Private mstrName

Public Property Get CustomerName()

    CustomerName = mstrName

End Property

End Class

要注意这里没有Property Let过程。由于没有提供Property Let过程,类外部的代码就无法修改CustomerName属性。

下面是第二种方法。

Class Customer

Private mstrName

Private Property Let CustomerName(strName)

    mstrName = strName

End Property

Public Property Get CustomerName()

    CustomerName = mstrName

End Property

End Class

用Public语句声明了Property Get过程,而用Private语句声明Property Let过程。因为Property Let过程被声明为Private,所以就能有效地对类外部的代码隐藏它。类内部的代码仍然可以通过Property Let过程修改这个属性,但在这个简单的例子中,因为类内部的代码可以直接修改这个私有属性变量,所以这没有什么实际意义,并不需要私有的Property Let过程。

也有例外情况,当Property Let过程中需要有代码对存入这个属性的值进行检查或转换时就是个例外。在这种情况下,类内部的代码也最好是用Property Let过程,而不是直接修改这个私有属性变量。

第一种创建只读属性的方法(只提供一个Property Get)更加常见。

创建只写属性
有两种方法可以创建只写属性,这两种方法与创建只读属性的方法完全相反:

●       忽略Property Get过程,只提供Property Let过程。

●       用Public语句声明Property Let过程,用Private语句声明Property Get过程。

没有属性过程的公共属性
还可以为您的类提供没有Property Let、Get和Set的属性。这是通过使用公共类级变量实现的。看一个例子,如下:

Class Customer

Private mstrName

Public Property Let CustomerName(strName)

    mstrName = strName

End Property

Public Property Get CustomerName()

    CustomerName = mstrName

End Property

End Class

其功能与下面这段代码等价:

Class Customer

Public Name

End Class

第二种方法看上去更有吸引力。从功能和语法的角度上看,它的代码更少,而且是完全符合规定的。但是很多VBScript程序员还是偏爱于使用私有属性变量与Property Let、Set及Get过程的组合,前一节已经对此做了探讨。

而有些程序员喜欢用公共类级变量代替Property Let、Set及Get过程。主要的优点就是使用公共类级变量创建类属性所需的代码更少。但是,要考虑到不使用Property Let、Set及Get也会带来一些缺点。

除非要使用您的类的代码使用了非常难看的语法,比如objCustomer.mstrName = “ACME Inc.”,否则无法用匈牙利命名法的作用域和子类型前缀命名类级变量。若您觉得匈牙利命名法对您的代码是有意义的,那么这会降低代码的可读性和可理解性。

●       无法用前一节中描述的方法创建只读属性或只写属性。

●       类外部的代码可以随时的修改任何属性。如果在某些情况允许修改某些属性,而在其他情况不能修改某些属性,唯一的办法就是用Property Let过程中的代码检查当前的状况。您不可能知道类外部的代码会在什么时候修改属性的值。

●       没有Property Let过程,就无法编写代码检查和转换写入属性的值。

●       没有Property Get过程,就无法编写代码验证和转换从属性中读出的值。

也就是说,如果您能忍受这些缺陷,当然就可以将属性声明为公共类级变量并在需要的时候再修改它们,加上Property Let、Set、Get过程。但是,有人会说应该一开始就用正确的方法。关于这个问题,优秀的程序员也有不同的看法,但是您会发现大多数程序员喜欢用Property Let、Get、Set过程,而不是公共类级变量。

但是您经常需要在单个脚本中创建简单的类。这种情况下使用一些快捷方式使代码简化和方便编写是可以被接受的。在这种情况下,您可以放弃Property Let、Get、Set,而直接使用公共变量。

定义方法
方法(method)实际上就是函数和过程的另一个名称。当函数或过程成为类的一部分时,就可以将其称为方法。如果您知道如何编写Function和Sub过程,那您就知道如何为类编写方法。方法并不像属性那样有一些特别的语法。需要考虑的问题主要是在类中将Function或Sub声明为Public还是Private。

简单地说,类内外的代码都可以使用Public语句声明的类方法;而只有类内部的代码才能使用Private语句声明的方法。

这个示例脚本SHOW_GREETING.VBS中含有一个Greeting类,用不同类型的消息向用户问好。这个类同时使用了公共方法和私有方法。从Greeting类的代码可以看出,在类中定义方法的语法跟定义VBScript函数和过程的语法是一样的。唯一需要额外考虑的就是应该声明为公共的还是私有的。

Class Greeting

Private mstrName

Public Property Let Name(strName)

    mstrName = strName

End Property

Public Sub ShowGreeting(strType)

     MsgBox MakeGreeting(strType) & mstrName & "."

End Sub

Private Function MakeGreeting(strType)

    Select Case strType

Case "Formal"

      MakeGreeting = "Greetings, "

Case "Informal"

      MakeGreeting = "Hello there, "

Case "Casual"

      MakeGreeting = "Hey, "

End Select

End Function

End Class

类外部的代码可以调用ShowGreeting()方法,这是公共的;但是不能调用MakeGreeting()方法,因为这是私有的,只能在内部使用。SHOW_GREETING.VBS示例脚本开头的代码使用了这个类。

Dim objGreet

Set objGreet = New Greeting

With objGreet

    .Name = "Dan"

    .ShowGreeting "Informal"

    .ShowGreeting "Formal"

    .ShowGreeting "Casual"

End With

Set objGreet = Nothing

Visual Basic程序员要注意:VBScript不支持用Friend关键字定义属性和方法。

类事件
事件(event)是一种会自动被调用的特殊方法。在任何特定的环境中,您所使用的类都支持一个或若干个事件。当给定的环境中支持某个事件时,您可以编写一个事件处理器(event handler),这实际上就是在事件发生时会自动调用的特殊方法。比如,在浏览器中,当用户单击按钮时,页面中的VBScript代码可以对这个按钮的OnClick事件做出响应。

任何VBScript类都自动地支持两个事件:Class_Initialize和Class_Terminate。可以选择在您的类中为它们提供事件处理器。如果在您的类中含有事件处理器方法,那么它们就会自动地被调用;如果没有,当事件发生时就不会有任何反应—— 如果您觉得没必要提供处理器方法的话,这不会带来任何问题。

Class_Initialize事件
当有代码根据类实例化出一个对象时就会引发类中的Class_Initialize事件。只要是基于某个类实例化出一个对象,就会发生这个事件,但是类中是否含有对此做出响应的代码就取决于您自己了。如果不想响应这个事件,完全可以忽略这个事件的事件处理器。下面这个例子中的类含有一个Class_Initialize事件处理器。

Class FileHelper

'Private FileSystemObject object

Private mobjFSO

Private Sub Class_Initialize

    Set mobjFSO = _

WScript.CreateObject("Scripting.FileSystemObject")

End Sub

'<>

End Class

正如这个例子所演示的,用Class_Initialize处理器初始化类级变量是一种相当典型的应用。如果要确保在类第一次启动时某个变量是某个特定的值,可以在Class_Initialize事件处理器中设置它的初始值。还可以用Class_Initialize事件实现其他的准备工作,比如打开数据库连接或打开文件。

Class_Initialize事件处理器的开始和结束语法必须与这个例子一样。您的代码只能在文件处理器内部做所需的处理,但是不能改变这个过程的名称。处理器的第一行必须是Private Sub Class_Initialize,并且最后一行必须是End Sub。实际上,事件处理器就是一个普通的VBScript子过程,只是有着一个特殊的名称。

从技术上来说,您可以用Public语句(而不用Private)声明事件处理器,但是事件处理器通常都是私有的。如果将其声明为公共的,类外部的代码就可以像调用其他方法一样调用它,这通常都不是我们所希望的。

在一个类中只能有一个Class_Initialize事件处理器。如果不需要的话也可以一个都没有,但绝对不能多于一个。

Class_Terminate事件
Class_Terminate事件与Class_Initialize事件相反。当类被实例化为对象时会触发Class_Initialize事件,而当基于这个类的对象被销毁时则会触发Class_Terminate事件。有两种销毁对象的方法:

●       将特殊值Nothing赋给最后一个引用了该对象的对象变量

●       超出了最后一个引用该对象的对象变量的作用域

发生这两种情况中的任一种时,Class_Terminate事件会在对象真正被销毁之前发生。

下面是前一节例子中见过的FileHelper类,现在添加了一个Class_Terminate事件处理器。

Class FileHelper

'Private FileSystemObject object

Private mobjFSO

Private Sub Class_Initialize

    Set mobjFSO = _

    WScript.CreateObject("Scripting.FileSystemObject")

End Sub

Private Sub Class_Terminate

    Set mobjFSO = Nothing

End Sub

'

End Class

在这个例子中,用Class_Terminate事件处理器销毁了在Class_Initialize事件中实例化的对象。这实际上是没有必要的,因为当FileHelper对象被销毁时,私有的mobjFSO变量也会超出其作用域,脚本引擎会自动地将其销毁。但是,有些程序员喜欢显式地销毁所有实例化的对象,这对于示例还是很有用的。

还可以用Class_Terminate事件关闭数据库连接、关闭文件或是将类的某些信息保存到数据库或文件中。关于Class_Initialize事件处理器的语法限制同样也适用于Class_Terminate事件处理器。

类常量
VBScript不支持声明类级的具名常量,原因不明。也就是说,不能在类中使用Const语句声明供类内部或外部代码使用的常量。例如,下面这段代码会产生编译错误:

Option Explicit

Dim objTest

Set objTest = new ConstTest

objTest.SayHello

Set objTest = Nothing

Class ConstTest

Private Const TEST_CONST = "Hello there."

Public Sub SayHello

    MsgBox TEST_CONST

End Sub

End Class

编译错误出现在这一行:

Private Const TEST_CONST = “Hello there.”

因为这个语句的作用域仅限于这个类,这意味它是在这个类中声明的,但不是在这个类的方法或属性中。(Const语句适用于属性或方法,但是其作用域也仅限于这个属性或方法。)但是有解决的办法,比如下面这个例子:

Option Explicit

Dim objTest

set objTest = new ConstTest

objTest.SayHello

Class ConstTest

Private TEST_CONST

Private Sub Class_Initialize

    TEST_CONST = "Hello there."

End Sub

Public Sub SayHello

    MsgBox TEST_CONST

End Sub

End Class

这里创建了一个伪常量。没有用Const语句声明TEST_CONST,而是将其声明为一个普通的私有类变量(也可以将其声明为公共的)。然后在Class_Initialize事件处理器中将您希望的“常量”值赋给TEST_CONST变量。但这里存在一点风险,因为类中的代码还是可以修改TEST_CONST变量的值。不过,全大写的命名方式可能有助于防止这种情况(大部分程序员都习惯将全大写的变量等同为具名常量)。您只需要确保类内部的代码不会修改这个变量的值就可以了。

请注意在老版本的VBScript中,类级常量也是不被支持的。但是,奇怪的是,这不会导致编译错误;它们的值会被忽略。如果您使用的VBScript没有产生编译错误,您还是会遇到同样的问题,上述解决方法还是有用的。

构建和使用VBScript类
现在您所开发的脚本将实现以下功能:

●       提供一个数据结构存放通讯录的条目。

●       提供一个数据结构存放一系列的通讯录条目。

●       提供一种查找和显示通讯录条目的方法。

将要开发的这个示例脚本包含以下元素:

●       用于存放单个通讯录条目的ListEntry类。这个类知道如何显示其中的数据。

●       PhoneList类,利用内部的Dictionary对象存放一系列的ListEntry对象。这个类用电话号码作为关键字,并且支持获取和显示单个条目。

●       支持不基于类的代码使用前面的两个类来操作通讯录,并要求用户输入电话号码以查找和显示。


首先,来看一下ListEntry类。

Class ListEntry

Private mstrLast

Private mstrFirst

Private mstrPhone

Public Property Let LastName(strLastName)

    If IsNumeric(strLastName) or _

IsDate(strLastName) Then

Err.Raise 32003, "ListEntry", _

      "The LastName property may not " & _

      "be a number or date."

    End If

    mstrLast = strLastName

End Property

Public Property Get LastName

LastName = mstrLast

End Property

Public Property Let FirstName(strFirstName)

    If IsNumeric(strFirstName) or _

IsDate(strFirstName) Then

Err.Raise 32004, "ListEntry", _

      "The FirstName property may not " & _

      "be a number or date."

    End If

    mstrFirst = strFirstName

End Property

Public Property Get FirstName

    FirstName = mstrFirst

End Property

Public Property Let PhoneNumber(strPhoneNumber)

    mstrPhone = strPhoneNumber

End Property

Public Property Get PhoneNumber

    PhoneNumber = mstrPhone

End Property

Public Sub DisplayEntry

    MsgBox "Phone list entry:" & vbNewLine & _

        vbNewLine & _

        "Last: " & mstrLast & vbNewLine & _

        "First: " & mstrFirst & vbNewLine & _

        "Phone: " & mstrPhone

End Sub

End Class

这个类有三个属性:LastName、FirstName和PhoneNumber。每个属性都是用私有属性变量以及Property Let和Property Get过程实现的。因为这个类的每个属性都有Let和Get过程,所以这些属性都是可读写的。还要注意LastName和FirstName的Property Let过程,对输入的数据做检查以确保外部代码不会将数字或日期存放到这个属性中。如果传递过来的是非法值,这段代码会生成一个错误(见第6章)。

在做验证时只是检查其是否为数字或日期是很不严谨的;这个例子的主要目的只是说明使用Property Let过程确保程序员不会将不合适的数据存放到这个属性中。这种技术对VBScript这种弱数据类型的语言非常重要;因为所有的变量都是Variant类型的,其中可以存放任何值。

还可以选择其他类型的验证方法。可以检查数据长度(最小或最长),非法的特殊字符,合适的格式等等。例如,可以在PhoneNumber Property Let中添加一个检查代码验证XXX-XXX-XXXX格式。或者可以添加转换功能,将电话号码转换成合适的格式。选择什么验证方法和转换方式取决于具体情况。关键是测试其他代码成功运行的前提是否成立,以避免缺陷和错误。

ListEntry类有一个方法:DisplayEntry,它用MsgBox()函数显示出通讯录条目的各个属性。这个例子选择了将这段代码放在ListEntry类中,因为类的一个原则就是类应该知道如何实现其所提供的功能。ListEntry类知道姓名和电话号码。所以,应该尽可能地将操作数据的代码与其所操作的数据放在一起,也就是将DisplayEntry()方法放在ListEntry类中。

在面向对象方法中,这称为“关注点分离”(separation of concerns)或“基于责任的设计”(responsibility-based design)。每个类都要知道一些东西,以及如何做一些事情。在设计时,要将类按逻辑分割开,尽量减少类之间不必要的交互。类之间的交互越少越好。

但是有时候有些功能是无需类知道该怎么做的。这可以使类更加通用,因此可以在多种情况下以不同的方式使用它。您将会看到这样例子,并且会这样继续构建您自己的代码。

继续,这是第二个类,PhoneList:

Class PhoneList

Private objDict

Private Sub Class_Initialize

    Set objDict = CreateObject("Scripting.Dictionary")

End Sub

Private Sub Class_Terminate

    Set objDict = Nothing

End Sub

Public Property Get ListCount

    ListCount = objDict.Count

End Property

Public Function EntryExists(strPhoneNumber)

    EntryExists = _

objDict.Exists(strPhoneNumber)

End Function

Public Sub AddEntry(objListEntry)

    If TypeName(objListEntry) <> "ListEntry" Then

Err.Raise 32000, "PhoneList", _

"Only ListEntry objects can be stored " & _

"in a PhoneList class."

    End If

   'We use the PhoneNumber property as the key.

   If Trim("" & objListEntry.PhoneNumber) = "" Then

        Err.Raise 32001, "PhoneList", _

      "A ListEntry object must have a " & _

      "phone number to be added to the " & _

      "phone list."

    End If

    objDict.Add objListEntry.PhoneNumber, objListEntry

End Sub

Public Sub DisplayEntry(strPhoneNumber)

    Dim objEntry

    If objDict.Exists(strPhoneNumber) Then

    Set objEntry = objDict(strPhoneNumber)

    objEntry.DisplayEntry

Else

    Err.Raise 32002, "PhoneList", _

"The phone number'" & strPhoneNumber & _

       "' is not in the list."

    End If

End Sub

End Class

第一个要注意的事情是这个类内部使用Dictionary对象存储这个通讯录。这是一种强大的技术,原因有两点:

●       这演示了您的类如何借用其他类的功能。

●       实际上您没有将内部的Dictionary对象暴露给PhoneList类外部的代码,也就是说使用PhoneList类的代码无需知道PhoneList类是如何存储数据的。

您还可以将Dictionary改成其他的数据存储方法(比如数组、哈希表、文本文件等等),这不会对使用PhoneList类的代码产生任何影响。第二点,本章前面已经演示过,使用Class_Initialize和Class_Terminate事件控制内存中Dictionary对象(objDict)的生存期。这样其他代码就会觉得始终有一个可以使用的Dictionary对象。

接着,创建了名为ListCount的Property Get过程,以及EntryExists方法。ListCount属性是对objDict.Count属性的封装;而类似的,EntryExists是objDict.Exists方法的封装。您也可以将Dictionary的其他属性方法暴露出来。但是,要小心,因为这可能会降低您将来将Dictionary对象换成其他数据存储结构的灵活性。

例如,可以只是简单地将objDict做一个属性供外部代码直接使用。但是,如果这么做,外部代码就会与类的内部紧密的耦合在一起—— 这意味着外部代码对类的内部工作原理知道得太多。应该尽可能地将PhoneList设计为一个“黑箱”;可以使用黑箱的功能,可以知道它的输入输出,但是无法看到其内部的工作方式。

接下来是AddEntry()方法。这个方法实际上只做了一件事情:调用Dictionary的Add()方法,其中关键字就是通讯录各个条目的电话号码。

objDict.Add objListEntry.PhoneNumber, objListEntry

注意,这里是将ListEntry对象本身存放到这个字典中,跟第7章类似,那里是将通讯条目的数组存放在字典中。

但这是这个方法的最后一行。在其之前的所有代码都是验证代码。这里只是测试并将该方法的假设存放起来。这个方法有两个隐含的假设:

●       PhoneList类中只有ListEntry对象

●       将PhoneNumber属性作为关键字

为了测试这两个假设:

●       使用TypeName函数检查外部代码传递过来的是否是ListEntry对象,而不是其他类型的数据。由于VBScript弱类型的特点,这是很有必要的,确实需要自行对其进行验证。

●       检查ListEntry对象的PhoneNumber属性是否为空。这样就能确保能将其作为关键字使用。

还有其他需要测试的假设,但是这两个假设容易产生奇怪的缺陷或错误消息,导致用您的类的程序员很难找出是什么地方出了问题。这些清晰的错误消息文档所关注的就是这些重要的假设。

最后,PhoneList有一个名为DisplayEntry的方法。等一会—— 在ListEntry类中不是也有一个DisplayEntry方法吗?为什么会有两个功能相同的方法?

这都取决于设计观点。没有完全正确的类设计方法。PhoneList类的DisplayEntry方法实际上是“代表”ListEntry.DisplayEntry()方法显示一个条目,看这几行代码。

If objDict.Exists(strPhoneNumber) Then

Set objEntry = objDict(strPhoneNumber)

objEntry.DisplayEntry

所以,尽管有两个方法,但实际上显示的代码只存在于ListEntry类中,所以并没有重复。这里的设计方式使得PhoneList类有专门的方法(比如DisplayEntry())供程序员对通讯录的条目实施专门的操作(比如显示它们),而不是提供更通用的方法,仅仅是提供一系列的条目,让外部代码用前面的三行代码去实现这个功能—— 也就是找到正确的条目,并要其显示自身。两种设计都是可行的,对您将来用不同的方式扩展这些类都不会有阻碍。

现在有两个类,可以来看看使用这些类的代码了(再提醒一下,所有的这些代码都可以在PHONE_LIST_CLASS.VBS文件中找到)。

Option Explicit

Dim objList

FillPhoneList

On Error Resume Next

objList.DisplayEntry(GetNumberFromUser)

If Err.Number <> 0 Then

If Err.Number = vbObjectError + 32002 Then

    MsgBox "That phone number is not in the list.", _

vbInformation

Else

    DisplayError Err.Number, Err.Source, _

Err.Description

End If

End If

Public Sub FillPhoneList

Dim objNewEntry

Set objList = New PhoneList

Set objNewEntry = New ListEntry

With objNewEntry

    .LastName = "Williams"

    .FirstName = "Tony"

    .PhoneNumber = "404-555-6328"

End With

objList.AddEntry objNewEntry

Set objNewEntry = Nothing

Set objNewEntry = New ListEntry

With objNewEntry

    .LastName = "Carter"

    .FirstName = "Ron"

    .PhoneNumber = "305-555-2514"

End With

objList.AddEntry objNewEntry

Set objNewEntry = Nothing

Set objNewEntry = New ListEntry

With objNewEntry

    .LastName = "Davis"

    .FirstName = "Miles"

    .PhoneNumber = "212-555-5314"

End With

objList.AddEntry objNewEntry

Set objNewEntry = Nothing

Set objNewEntry = New ListEntry

With objNewEntry

    .LastName = "Hancock"

    .FirstName = "Herbie"

    .PhoneNumber = "616-555-6943"

End With

objList.AddEntry objNewEntry

Set objNewEntry = Nothing

Set objNewEntry = New ListEntry

With objNewEntry

    .LastName = "Shorter

    .FirstName = "Wayne"

    .PhoneNumber = "853-555-0060"

End With

objList.AddEntry objNewEntry

Set objNewEntry = Nothing

End Sub

Public Function GetNumberFromUser

GetNumberFromUser = InputBox("Please enter " & _

    "a phone number (XXX-XXX-XXXX) with " & _

    "which to search the list.")

End Function


这个精巧的例子中最重要的一点就在这两行简单的代码中(没有包括与错误处理相关的代码)。

FillPhoneList

objList.DisplayEntry(GetNumberFromUser)

这两行代码就展示了整个脚本的逻辑:创建一个通讯录,填入内容,要求用户输入需要查找的电话号码,然后显示这个条目。这就是将代码分割成类和过程所带来的好处。如果将类和过程做得尽可能的通用,那么利用它们实现具体功能的代码就会比较简单、易于理解和修改—— 总的来说也就是方便重用。

FillPhoneList()过程创建PhoneList对象(objList),并填入一些条目。想象一下,通讯录的条目可以来自于数据库表格、文件或用户的输入。FillPhoneList()使用了一个名为objNewEntry的“临时对象变量”。对于列表中的每个条目,都实例化objNewEntry,填入数据,然后将其传递给objList.AddEntry()方法。

注意在FillPhoneList中使用New关键字实例化自定义的VBScript对象。

Set objList = New PhoneList

以及

Set objNewEntry = New ListEntry

为什么不用CreateObject()函数?CreateObject()函数只是用于实例化非原生的VBScript对象(比如Dictionary和FileSystemObject),而您必须用New来实例化同一个脚本中自定义的VBScript类。背后的原因很复杂,所以只需要记住这条原则:如果用自定义的VBScript类实例化一个对象,使用New;否则,用CreateObject。

GetNumberFromUser()函数非常简单。用InputBox()函数提示用户输入一个电话号码,并返回用户的输入。然后脚本开头的代码将这个值传递给objDict.DisplayEntry()。如果这个条目存在,ListEntry对象就将其显示出来;如果不存在,objDict.DisplayEntry()就返回一个错误。

还可以用PhoneList类和ListEntry类用不同的方式实现各种不同的功能。如果有新的需求,可以扩展这两个类,而不会破坏正在使用它们的代码。将来使用这个脚本的程序员在短时间内就能理解这个脚本。毕竟,这只有两行代码。

FillPhoneList

objList.DisplayEntry(GetNumberFromUser)

如果有程序员想进一步地了解脚本工作原理的底层细节,他可以继续阅读其他的代码,深入这些过程和类。但是很多情况下,这都没有必要,除非程序员需要修复缺陷或是添加一些功能。如果程序员要知道的只是这个脚本在做什么,答案就是这两行简单的代码。