我开始在 VBA 中使用类,并欣赏 SO 上已经提供的一些精彩信息。
据我所知,似乎缺少的是对类中的代码应该做什么的解释,或者,正如我怀疑的那样,不应该做什么。例如:
假设我有一个文档并希望插入/修改一个表格。在这个例子中,我想:
关于“排序”,我认为类模块非常适合根据某些标准确定信息应放入表格中的顺序。
但理想情况下:
或者
还是没关系?如果有首选方法,那么每种方法的优点/缺点是什么?
首先,感谢您进入 OOP 的奇妙兔子洞!
\n\n简短的回答:这取决于。
\n\n(非常)长的答案:
\n\n您需要避免从Application.Worksheets
(或Application.Sheets
) 集合中提取[在编译时存在的]工作表,而使用该工作表CodeName
。VBA 创建一个全局范围的对象引用供您使用,以每个工作表的CodeName
.
这就是这段代码的编译方式,无需Sheet1
在任何地方声明:
Option Explicit\n\nSub Test()\n Debug.Print Sheet1.CodeName\nEnd Sub\n
Run Code Online (Sandbox Code Playgroud)\n\n使用全局范围的“自由”对象变量在工作表的代码隐藏之外的任何地方实现特定于工作表的功能的问题是,单独的模块现在与该对象耦合Sheet1
。
您需要重点突出、有凝聚力的模块——高内聚力。并且耦合度低。
\n\n通过在另一个模块(无论是标准模块还是类模块)中编写特定于工作表的代码,您将创建依赖关系并增加耦合,这会降低可测试性- 请考虑以下代码Class1
:
Public Sub DoSomething()\n With Sheet1\n \' do stuff\n End With\nEnd Sub\n
Run Code Online (Sandbox Code Playgroud)\n\n现在Class1
只能与Sheet1
. 这样会更好:
Public Sub DoSomething(ByVal sheet As Worksheet)\n With sheet\n \' do stuff\n End With\nEnd Sub\n
Run Code Online (Sandbox Code Playgroud)\n\n这里发生了什么?依赖注入。我们依赖于特定的工作表,但我们不是针对该特定对象进行编码,而是告诉世界“给我任何工作表,我将用它来做我的事情”。那是在方法级别。
\n\n如果一个类意味着使用单个特定工作表,并公开使用该工作表执行各种操作的多个方法,则ByVal sheet As Worksheet
在每个方法上都有一个参数没有多大意义。
相反,您将其作为属性注入:
\n\nPrivate mSheet As Worksheet\n\nPublic Property Get Sheet() As Worksheet\n Set Sheet = mSheet\nEnd Property\n\nPublic Property Set Sheet(ByVal value As Worksheet)\n Set mSheet = value\nEnd Property\n
Run Code Online (Sandbox Code Playgroud)\n\n现在该类的所有方法都可以使用Sheet
...唯一的问题是使用该类的客户端代码现在需要记住Set
该Sheet
属性,否则可能会出现错误。在我看来,这是糟糕的设计。
一种解决方案可能是将依赖注入原则进一步推进,并且实际上依赖于抽象;我们使用另一个充当接口的类模块来形式化要为该类公开的接口 - 即IClass1
不实现任何内容,它只是为所公开的内容定义存根:
\'@Interface\nOption Explicit\n\nPublic Property Get Sheet() As Worksheet\nEnd Property\n\nPublic Sub DoSomething()\nEnd Sub\n
Run Code Online (Sandbox Code Playgroud)\n\n我们的Class1
类模块现在可以实现该接口,如果您已经了解了这么多,希望我不会在这里迷失您:
\n\n\n注意:模块和成员属性在 VBE 中不可见。它们在这里用相应的Rubberduck注释表示。
\n
\'@PredeclaredId\n\'@Exposed\nOption Explicit\nImplements IClass1\n\nPrivate mSheet As Worksheet\n\nPublic Function Create(ByVal pSheet As Worksheet) As IClass1\n With New Class1\n Set .Sheet = pSheet\n Set Create = .Self\n End With\nEnd Function\n\nFriend Property Get Self() As IClass1\n Set Self = Me\nEnd Property\n\nPrivate Property Get IClass1_Sheet() As Worksheet\n Set IClass1_Sheet = mSheet\nEnd Property\n\nPrivate Sub IClass1_DoSomething()\n \'implementation goes here\nEnd Sub\n
Run Code Online (Sandbox Code Playgroud)\n\n该类Class1
模块提供两个接口:
Class1
可从实例访问的成员PredeclaredId
:\n\nCreate(ByVal pSheet As Worksheet) As IClass1
Self() As IClass1
IClass1
会员,可从IClass1
界面访问:\n\nSheet() As Worksheet
DoSomething()
现在调用代码可以如下所示:
\n\nDim foo As IClass1\nSet foo = Class1.Create(Sheet1)\nDebug.Assert foo.Sheet Is Sheet1\nfoo.DoSomething\n
Run Code Online (Sandbox Code Playgroud)\n\n因为它是针对IClass1
接口编写的,所以调用代码只能“看到”Sheet
和DoSomething
成员。由于VB_PredeclaredId
的属性Class1
,Create
可以通过Class1
默认实例Sheet1
来访问该函数,这与在不创建实例的情况下访问该函数非常相似(UserForm 类具有该默认实例(UserForm 类也
这是工厂设计模式:我们使用默认实例作为工厂,其作用是创建和初始化接口的实现IClass1
,该实现Class1
恰好正在实现。
与Class1
完全解耦Sheet1
,绝对没有什么问题Class1
,负责初始化它的任何工作表上需要发生的所有事情
耦合得到照顾。凝聚力完全是另一个问题:如果你发现Class1
头发和触手长出来了,并且要对很多事情负责,你甚至不知道它是为了什么而写的,那么单一责任原则很可能会受到打击,并且IClass1
接口有太多不相关的成员,以至于接口隔离原则也受到了打击,其原因可能是因为接口没有按照开放/封闭原则设计。
上述内容无法使用标准模块来实现。标准模块不能很好地与 OOP 配合,这意味着更紧密的耦合,从而降低可测试性。
\n\n设计任何东西都没有单一的“正确”方法。
\n\nIMO 一个标准模块应该只用于公开入口点(宏、UDF、Rubberduck 测试方法,以及Option Private Module
一些常见的实用函数),并且包含相当少的代码,仅初始化对象及其依赖项,然后是它的类一直往下。