在 VBA 中,应该避免在类模块中修改文档的代码

Slo*_*ner 5 excel vba ms-word

我开始在 VBA 中使用类,并欣赏 SO 上已经提供的一些精彩信息。

据我所知,似乎缺少的是对类中的代码应该做什么的解释,或者,正如我怀疑的那样,不应该做什么。例如:

假设我有一个文档并希望插入/修改一个表格。在这个例子中,我想:

  • 检查表是否存在
  • 如果表不存在:
    • 在特定位置添加表格
    • 向表中添加信息(即添加行)
  • 如果表确实存在
    • 向/从表中添加/删除信息
    • 根据某些标准对表格进行排序

关于“排序”,我认为类模块非常适合根据某些标准确定信息应放入表格中的顺序。

但理想情况下:

  • 是否应该使用类模块(或第二类模块)来检查和编辑文档?

或者

  • 使用常规模块最好进行检查和/或编辑吗?

还是没关系?如果有首选方法,那么每种方法的优点/缺点是什么?

Mat*_*don 5

首先,感谢您进入 OOP 的奇妙兔子洞!

\n\n

简短的回答:这取决于。

\n\n
\n\n

(非常)长的答案:

\n\n

您需要避免从Application.Worksheets(或Application.Sheets) 集合中提取[在编译时存在的]工作表,而使用该工作表CodeName。VBA 创建一个全局范围的对象引用供您使用,以每个工作表的CodeName.

\n\n

这就是这段代码的编译方式,无需Sheet1在任何地方声明:

\n\n
Option Explicit\n\nSub Test()\n    Debug.Print Sheet1.CodeName\nEnd Sub\n
Run Code Online (Sandbox Code Playgroud)\n\n

使用全局范围的“自由”对象变量在工作表的代码隐藏之外的任何地方实现特定于工作表的功能的问题是,单独的模块现在与该对象耦合Sheet1

\n\n

类模块取决于工作表。任何工作表。

\n\n

您需要重点突出、有凝聚力的模块——高内聚力。并且耦合度低

\n\n

通过在另一个模块(无论是标准模块还是类模块)中编写特定于工作表的代码,您将创建依赖关系并增加耦合,这会降低可测试性- 请考虑以下代码Class1

\n\n
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. 这样会更好:

\n\n
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\n

相反,您将其作为属性注入:

\n\n
Private 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...唯一的问题是使用该类的客户端代码现在需要记住SetSheet属性,否则可能会出现错误。在我看来,这是糟糕的设计。

\n\n

一种解决方案可能是将依赖注入原则进一步推进,并且实际上依赖于抽象我们使用另一个充当接口的类模块来形式化要为该类公开的接口 - 即IClass1不实现任何内容,它只是为所公开的内容定义存根:

\n\n
\'@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
\n\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模块提供两个接口:

\n\n
    \n
  • Class1可从实例访问的成员PredeclaredId:\n\n
      \n
    • Create(ByVal pSheet As Worksheet) As IClass1
    • \n
    • Self() As IClass1
    • \n
  • \n
  • IClass1会员,可从IClass1界面访问:\n\n
      \n
    • Sheet() As Worksheet
    • \n
    • DoSomething()
    • \n
  • \n
\n\n

现在调用代码可以如下所示:

\n\n
Dim 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接口编写的,所以调用代码只能“看到”SheetDoSomething成员。由于VB_PredeclaredId的属性Class1Create可以通过Class1 默认实例Sheet1来访问该函数,这与在不创建实例的情况下访问该函数非常相似(UserForm 类具有该默认实例(UserForm 类也

\n\n

这是工厂设计模式:我们使用默认实例作为工厂,其作用是创建和初始化接口的实现IClass1,该实现Class1恰好正在实现。

\n\n

Class1完全解耦Sheet1,绝对没有什么问题Class1,负责初始化它的任何工作表上需要发生的所有事情

\n\n

耦合得到照顾。凝聚力完全是另一个问题:如果你发现Class1头发和触手长出来了,并且要对很多事情负责,你甚至不知道它是为了什么而写的,那么单一责任原则很可能会受到打击,并且IClass1接口有太多不相关的成员,以至于接口隔离原则受到了打击,其原因可能是因为接口没有按照开放/封闭原则设计

\n\n
\n\n

上述内容无法使用标准模块来实现。标准模块不能很好地与 OOP 配合,这意味着更紧密的耦合,从而降低可测试性。

\n\n
\n\n

长话短说:

\n\n

设计任何东西都没有单一的“正确”方法

\n\n
    \n
  • 如果您的代码可以处理与特定工作表的紧密耦合,则最好在该工作表的代码隐藏中实现该工作表的功能,以获得更好的内聚性。仍然使用专门的对象(类)来执行专门的任务:如果您的工作表代码隐藏负责设置数据库连接、通过网络发送参数化查询、检索结果并将它们转储到工作表中,那么您正在做错误\xe2\x84\xa2并且现在在不访问数据库的情况下单独测试该代码是不可能的。
  • \n
  • 如果您的代码更复杂并且无法与特定工作表紧密耦合,或者工作表在编译时不存在,请在可以与任何工作表一起使用的类中实现该功能,并拥有一个类它负责运行时创建的工作表的模型。
  • \n
\n\n

IMO 一个标准模块应该只用于公开入口点(宏、UDF、Rubberduck 测试方法,以及Option Private Module一些常见的实用函数),并且包含相当少的代码,仅初始化对象及其依赖项,然后是它的类一直往下

\n