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

时间:2017-07-11 04:01:25

标签: vba excel-vba word-vba excel

我开始在VBA中使用Classes,并欣赏SO上已有的一些精彩信息。

据我所知,似乎缺乏的是解释一个类中的代码应该或者我怀疑不应该做什么。例如:

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

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

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

但最理想的是:

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

OR

  • 使用常规模块检查和/或编辑是否最佳?

或者不重要吗?如果有一种首选方式,那么每种方法的优点/缺点是什么?

[编辑 - 此处的后续问题:Understanding classes in VBA - help improve these comments ]

1 个答案:

答案 0 :(得分:2)

首先,为进入OOP精彩的兔子洞而感到荣幸!

简短回答:取决于。

(非常)长的答案:

您希望避免从Application.Worksheets(或Application.Sheets)集合中提取[在编译时存在]的工作表,并使用该表格{{1相反。 VBA创建了一个全局范围的对象引用供您使用,以每个工作表CodeName命名。

这个代码如何编译,CodeName没有在任何地方声明:

Sheet1

使用该全局范围" free"在工作表的代码隐藏之外的任何地方实现工作表特定功能的问题。对象变量,就是单独的模块现在与该Option Explicit Sub Test() Debug.Print Sheet1.CodeName End Sub 对象耦合

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

您需要专注的内聚模块 - 高凝聚力低耦合

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

中考虑此代码
Class1

现在Public Sub DoSomething() With Sheet1 ' do stuff End With End Sub 只能与Class1一起使用。这会更好:

Sheet1

这里发生了什么? 依赖注入。我们在特定工作表上有一个依赖,但我们不是针对该特定对象进行编码,而是告诉全世界"给我任何工作表和我用它做我的事情"。那是方法级别。

如果一个类意味着使用单个特定工作表,并且公开了多个使用该工作表执行各种操作的方法,那么在每个方法上都有Public Sub DoSomething(ByVal sheet As Worksheet) With sheet ' do stuff End With End Sub 参数并没有多大意义。

相反,您将其作为财产注入:

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属性,否则可能会出现错误。那个设计糟糕的IMO。

一种解决方案可能是将依赖注入原则进一步推进,实际上依赖于抽象;我们正式化了我们想要为该类公开的接口,使用另一个类模块作为接口 - Sheet类没有实现任何东西,它只定义了存根揭露了什么:

IClass1

我们的'@Interface Option Explicit Public Property Get Sheet() As Worksheet End Property Public Sub DoSomething() End Sub 课程模块现在可以实施该界面,如果你一直在关注这一点,希望我不会在这里失去你:

  

注意:VBE中不显示模块和成员属性。他们在此处使用相应的Rubberduck注释代表。

Class1

这个'@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 类模块提供了两个接口:

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

现在调用代码看起来像这样:

DoSomething()

因为它是针对Dim foo As IClass1 Set foo = Class1.Create(Sheet1) Debug.Assert foo.Sheet Is Sheet1 foo.DoSomething 接口编写的,所以仅调用代码"看到" IClass1Sheet成员。由于DoSomething的{​​{1}}属性,VB_PredeclaredId函数可以通过Class1 默认实例访问,几乎与{{1}完全相同无需创建实例即可访问(UserForm类也具有默认实例)。

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

Sheet1完全与IClass1分离后,让Class1对所有需要在其初始化的工作表上发生的事情负责,这绝对没有错。

耦合得到了解决。 Cohesion 完全是另一个问题:如果你发现Class1正在增长头发和触角,并为许多事情负责,你甚至不知道它是什么写的,很可能是单一责任原则正在受到打击,而且Sheet1界面有如此多的无关成员,接口隔离原则 也是 em>受到殴打,其原因可能是因为界面没有考虑到开放/封闭原则而设计。

上述内容无法通过标准模块实施。标准模块在OOP上不能很好地发挥作用,这意味着更紧密的耦合,从而降低了可测试性。

TL; DR:

没有单一"对"设计任何东西的方法。

  • 如果您的代码可以处理与特定工作表紧密耦合更喜欢在该工作表的代码隐藏中实现该工作表的功能,以便更好地凝聚 即可。仍然使用专门的对象(类)来执行专门的任务:如果您的工作表代码隐藏负责设置数据库连接,通过网络发送参数化查询,检索结果并将它们转储到工作表中,那么您需要做错了,现在无需点击数据库即可单独测试该代码,这是不可能的。
  • 如果您的代码更复杂并且无法与特定工作表紧密耦合,或者如果工作表在编译时不存在,那么在可以工作的类中实现该功能使用任何工作表,并且有一个类负责该运行时创建的工作表的模型

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