首先,感谢您进入 OOP 的奇妙兔子洞!
简短回答:这取决于。
(非常)长的答案:
您需要避免从中提取[在编译时存在的]工作表Application.Worksheets
(or Application.Sheets
) 集合,并使用该工作表的CodeName
反而。 VBA 创建一个全局范围的对象引用供您使用,以每个工作表的名称命名CodeName
.
这就是这段代码的编译方式,Sheet1
没有在任何地方声明:
Option Explicit
Sub Test()
Debug.Print Sheet1.CodeName
End Sub
使用全局范围的“自由”对象变量,在工作表的代码隐藏之外的任何地方实现特定于工作表的功能的问题是,现在单独的模块是coupled接着就,随即Sheet1
object.
类模块取决于工作表。Any工作表。
你想要专注,cohesive模块-高内聚力. And 低耦合.
通过在另一个模块(无论是标准模块还是类模块)中编写特定于工作表的代码,您正在创建一个依赖性并不断增加coupling,这减少了可测试性- 考虑这段代码Class1
:
Public Sub DoSomething()
With Sheet1
' do stuff
End With
End Sub
Now Class1
只能与Sheet1
。这样会更好:
Public Sub DoSomething(ByVal sheet As Worksheet)
With sheet
' do stuff
End With
End Sub
这里发生了什么?依赖注入。我们有一个依赖性在特定的工作表上,但我们不是针对该特定对象进行编码,而是告诉世界“给我任何工作表我会用它做我的事”。那是在method level.
如果一个类意味着使用单个特定工作表,并公开使用该工作表执行各种操作的多个方法,则具有ByVal sheet As Worksheet
每个方法上的参数没有多大意义。
相反,您将其作为属性注入:
Private mSheet As Worksheet
Public Property Get Sheet() As Worksheet
Set Sheet = mSheet
End Property
Public Property Set Sheet(ByVal value As Worksheet)
Set mSheet = value
End Property
现在该类的所有方法都可以使用Sheet
...唯一的问题是使用该类的客户端代码现在需要记住Set
that Sheet
属性,否则可能会出现错误。在我看来,这是糟糕的设计。
一种解决方案可能是推动依赖注入原理更进一步,实际上依赖于抽象;我们将要为该类公开的接口形式化,使用another将充当接口的类模块 - 即IClass1
类没有实现任何东西,它只是定义了所公开内容的存根:
'@Interface
Option Explicit
Public Property Get Sheet() As Worksheet
End Property
Public Sub DoSomething()
End Sub
Our Class1
类模块现在可以实施那个界面,如果你一直关注到这里,希望我不会在这里迷失你:
注意:模块和成员属性在 VBE 中不可见。他们与相应的代表在这里橡皮鸭 http://rubberduckvba.com注释。
'@PredeclaredId
'@Exposed
Option Explicit
Implements IClass1
Private mSheet As Worksheet
Public Function Create(ByVal pSheet As Worksheet) As IClass1
With New Class1
Set .Sheet = pSheet
Set Create = .Self
End With
End Function
Friend Property Get Self() As IClass1
Set Self = Me
End Property
Private Property Get IClass1_Sheet() As Worksheet
Set IClass1_Sheet = mSheet
End Property
Private Sub IClass1_DoSomething()
'implementation goes here
End Sub
This Class1
类模块呈现two接口:
-
Class1
members, accessible from the PredeclaredId
instance:
Create(ByVal pSheet As Worksheet) As IClass1
Self() As IClass1
-
IClass1
members, accessible from the IClass1
interface:
Sheet() As Worksheet
DoSomething()
现在调用代码可以如下所示:
Dim foo As IClass1
Set foo = Class1.Create(Sheet1)
Debug.Assert foo.Sheet Is Sheet1
foo.DoSomething
因为它是针对IClass1
接口,调用代码只能“看到”Sheet
and DoSomething
成员。因为VB_PredeclaredId
的属性Class1
, the Create
可以通过以下方式访问该功能Class1
默认实例,几乎一模一样Sheet1
无需创建实例即可访问(UserForm 类具有默认实例, too).
这是factory设计模式:我们正在使用默认实例 as a factory其作用是创建并初始化一个实现IClass1
接口,其中Class1
恰好正在实施。
With Class1
完全解耦Sheet1
,绝对没有什么问题Class1
负责在其初始化的任何工作表上需要发生的所有事情。
Coupling受到照顾。Cohesion完全是另一个问题:如果你发现Class1
长出毛发和触手,并负责很多事情,你甚至不知道它是为了什么而写的,很可能单一责任原则正在遭受打击,并且IClass1
接口有很多不相关的成员,以至于接口隔离原则 is also受到打击,其原因可能是因为界面设计时没有考虑到开闭原理心里。
上述内容无法使用标准模块来实现。标准模块与 OOP 的配合不太好,这意味着更紧密的耦合,从而降低了可测试性。
TL;DR:
没有一种“正确”的设计方法anything.
- 如果你的代码可以处理紧密耦合具有特定的工作表,更喜欢在该工作表的代码隐藏中实现该工作表的功能,以便更好cohesion。仍然使用专门的对象(类)来执行专门的任务:如果您的工作表代码隐藏负责设置数据库连接、通过网络发送参数化查询、检索结果并将它们转储到工作表中,那么您就是做错事™现在,在不访问数据库的情况下单独测试该代码是不可能的。
- 如果您的代码更复杂并且无法与特定工作表紧密耦合,或者工作表在编译时不存在,在可以使用的类中实现功能any工作表,并有一个类负责model该运行时创建的工作表的。
IMO 标准模块只能用于公开入口点(宏、UDF、Rubberduck 测试方法以及Option Private Module
,一些常见的实用函数),并且包含相当少的代码,仅初始化对象及其依赖项,然后一直都是课程.