以下是对给出答案的一些理论和实践贡献,以便于那些想了解implements/interfaces的人。我们知道,VBA不支持继承,所以我们可以几乎盲目地使用接口来在不同类之间实现共同属性/行为。但我认为有必要描述一下两者的概念差异,以便后来的问题更好地得到解决。
继承:定义了is-a关系(一个正方形是一个图形);
接口:定义了must-do关系(一个典型的例子是“drawable”接口,要求可绘制对象必须实现“draw”方法)。这意味着来自不同根类的类可以实现共同的行为。
继承意味着基类(某种物理或概念原型)被扩展,而接口实现了一组定义特定行为的属性/方法。因此,可以说Shape是一个基类,其他所有形状都继承自它,并且可以实现drawable接口使所有形状可绘制。该接口将是一个契约,保证每个Shape都有一个draw方法,指定如何/在哪里绘制形状:圆可能与正方形不同。
类IDrawable:
'IDrawable interface, defining what methods drawable objects have access to
Public Function draw()
End Function
由于VBA不支持继承,我们被迫选择创建一个接口IShape,保证通用形状(正方形、圆形等)实现特定属性/行为,而不是创建一个抽象的Shape基类来进行扩展。
class IShape:
'Get the area of a shape
Public Function getArea() As Double
End Function
我们遇到麻烦的部分是当我们想要使每个形状都可绘制时。
不幸的是,由于IShape是VBA中的一个接口而不是基类,我们无法在基类中实现可绘制接口。似乎VBA不允许我们一个接口实现另一个接口;经过测试后,编译器似乎没有提供所需的行为。换句话说,我们不能在IShape内实现IDrawable,并期望由此强制IShape的实例实现IDrawable方法。
我们被迫将此接口实现到实现IShape接口的每个通用形状类中,幸运的是,VBA允许实现多个接口。
类cSquare:
Option Explicit
Implements iShape
Implements IDrawable
Private pWidth As Double
Private pHeight As Double
Private pPositionX As Double
Private pPositionY As Double
Public Function iShape_getArea() As Double
getArea = pWidth * pHeight
End Function
Public Function IDrawable_draw()
debug.print "Draw square method"
End Function
'Getters and setters
下面是接口的典型用途/好处。
让我们通过编写一个工厂来开始我们的代码,该工厂返回一个新的正方形。(这只是我们无法直接向构造函数发送参数的解决方法):
模块mFactory:
Public Function createSquare(width, height, x, y) As cSquare
Dim square As New cSquare
square.width = width
square.height = height
square.positionX = x
square.positionY = y
Set createSquare = square
End Function
我们的主要代码将使用工厂创建一个新的正方形:
Dim square As cSquare
Set square = mFactory.createSquare(5, 5, 0, 0)
当您查看可用方法时,您会发现您逻辑上可以访问在cSquare类中定义的所有方法:
![enter image description here](https://istack.dev59.com/4zH8o.webp)
我们稍后会看到为什么这很重要。
现在你应该想知道如果你真的想创建一组可绘制对象会发生什么。你的应用程序可能包含不是形状但仍然可绘制的对象。理论上,没有什么能阻止你拥有一个可以绘制的IComputer接口(可能是一些剪贴画或其他东西)。
你可能想要一组可绘制对象的原因是,你可能想在应用程序生命周期的某个时刻循环渲染它们。
在这种情况下,我将编写一个装饰器类来包装集合(我们将看到原因)。
class collDrawables:
Option Explicit
Private pSize As Integer
Private pDrawables As Collection
'constructor
Public Sub class_initialize()
Set pDrawables = New Collection
End Sub
'Adds a drawable to the collection
Public Sub add(cDrawable As IDrawable)
pDrawables.add cDrawable
'Increase collection size
pSize = pSize + 1
End Sub
装饰器允许您添加一些原生VBA集合不提供的便利方法,但实际重点在于该集合仅接受可绘制的对象(实现IDrawable接口)。如果我们尝试添加一个不可绘制的对象,则会抛出类型不匹配的错误(只允许可绘制的对象!)。
因此,我们可能希望循环遍历一组可绘制的对象以进行渲染。允许非可绘制的对象进入集合将导致错误。渲染循环可能如下所示:
Option Explicit
Public Sub app()
Dim obj As IDrawable
Dim square_1 As IDrawable
Dim square_2 As IDrawable
Dim computer As IDrawable
Dim person as cPerson
Dim collRender As New collDrawables
Set square_1 = mFactory.createSquare(5, 5, 0, 0)
Set square_2 = mFactory.createSquare(10, 5, 0, 0)
Set computer = mFactory.createComputer(20, 20)
collRender.add square_1
collRender.add square_2
collRender.add computer
For Each obj In collRender.getDrawables
obj.draw
Next obj
End Sub
请注意,上述代码增加了很多透明度:我们将对象声明为IDrawable,这使得循环永远不会失败,因为绘制方法在集合中的所有对象上都可用。
如果我们尝试将Person添加到集合中,如果此Person类没有实现可绘制接口,则会抛出类型不匹配的异常。
但也许最重要的原因是,将对象声明为接口很重要,因为
我们只想公开在接口中定义的方法,而不是那些在个别类中定义的公共方法,正如我们之前所看到的。
Dim square_1 As IDrawable
![enter image description here](https://istack.dev59.com/qkQmw.webp)
我不仅确定square_1有一个draw
方法,而且它还确保只有由IDrawable定义的方法被暴露出来。
对于正方形来说,这个好处可能并不立即清晰,但让我们来看一个更加清晰的Java集合框架的类比。
想象一下你有一个通用接口叫做IList
,它定义了不同类型列表适用的一组方法。每种类型的列表是一个特定的类,实现了IList接口,定义了它们自己的行为,并可能在其上添加更多自己的方法。
我们如下声明列表:
dim myList as IList 'Declare as the interface!
set myList = new ArrayList 'Implements the interface of IList only, ArrayList allows random (index-based) access
在上面的代码中,将列表声明为IList可以确保您不会使用ArrayList特定的方法,而只能使用接口规定的方法。想象一下,您将列表声明如下:
dim myList as ArrayList 'We don't want this
您将可以访问ArrayList类中明确定义的公共方法。有时这可能是需要的,但通常我们只想利用内部类行为,而不是由类特定公共方法定义。
如果在代码中使用此ArrayList 50次以上,并突然发现最好使用LinkedList(允许与此类型的列表相关的特定内部行为),则受益会变得清晰明了。
如果我们遵循接口,就可以更改以下行:
set myList = new ArrayList
到:
set myList = new LinkedList
而且,只要接口确保合同得到履行,即仅使用在IList上定义的公共方法,因此其他代码都不会出现问题,因此可以随时交换不同类型的列表。
最后一件事(在VBA中可能较少人知道的行为)是您可以为接口提供默认实现。
我们可以按以下方式定义一个接口:
IDrawable:
Public Function draw()
Debug.Print "Draw interface method"
End Function
还有一个实现绘制方法的类:
cSquare:
implements IDrawable
Public Function draw()
Debug.Print "Draw square method"
End Function
我们可以通过以下方式在不同实现之间进行切换:
Dim square_1 As IDrawable
Set square_1 = New IDrawable
square_1.draw 'Draw interface method
Set square_1 = New cSquare
square_1.draw 'Draw square method
如果您将变量声明为cSquare,则不可能实现这一点。我暂时想不到什么好的例子可以说明其有用性,但如果您测试它,则在技术上是可行的。