3

我正在尝试进一步构建这个优秀的示例,它已经实现了这些非常有见地的 RubberduckVBA.com 文章中讨论的最佳实践:

  1. 通过代理类从 Excel 工作簿/工作表中抽象出来;
  2. 使用 UserForm 控件而不弄乱其默认实例的状态;
  3. 将“应用”逻辑添加到 #2

我想在现有示例中添加一个事件处理程序,它(为简单起见)报告 Sheet1 的“A1”单元格中 Sheet2 的“更改”范围左上角单元格的值,以及“A2”中的更改时间”。我通常会在 Sheet2 的代码隐藏中这样做:

Private Sub Worksheet_Change(ByVal Target As Range)
    Sheet1.Cells(1, 1).Value2 = Target.Cells(1, 1).Value2
    Sheet1.Cells(1, 2).Value2 = CStr(Now)
End Sub

但我想知道如何在给定的示例中最好地实现这一点,考虑到它是围绕 MVP 模式设计的,并通过代理接口利用工作簿和工作表抽象——这意味着预期零/最小工作表代码隐藏。

我能够理解事件处理是如何在令人敬畏的战舰教程中实现的,但它的设计在一些重要方面有所不同:

  1. “战舰”遵循 MVC 设计模式,而我想像示例中那样坚持使用 MVP;
  2. “战舰”通过“视图”类从其工作表中抽象出来,而我想为每个工作表有一个单独的代理接口+类;
  3. “战舰”部署了适配器模式,而我可以将视图和工作表代理实现与演示者耦合(如果可能的话,关于事件处理)。

考虑到这一点,我绝对希望看到一个代码示例,它将我上面描述的“Worksheet_Change”事件添加到已经实现工作簿和工作表代理并遵循 MVP 模式的基础项目中。

即使没有代码示例,如果我弄清楚这些问题也会有很大帮助:

  1. 工作表代理方法是否规定应该绝对零表代码隐藏?如果我在 Sheet2(不是它的代理)内开始我的“Worksheet_Change”事件实现,这会是朝着错误方向迈出的一步,如下所示:
Public Event SheetChanged(ByVal changedRange As Range)

Private Sub Worksheet_Change(ByVal Target As Range)
    RaiseEvent SheetChanged(Target)
End Sub
  1. 如果不是绝对需要使用适配器模式进行事件处理,那么使用“IViewCommands”和“IViewEvents”接口来列出从 Presenter 发送到 View 的所有命令以及从 View 引发的事件是否仍然是一个好主意?分别发送给Presenter?
  2. 我假设我需要使用惰性对象/弱引用才能公开事件。如果是这样,并且假设我可以在没有适配器的情况下完成工作(参见上面的#2),这是否意味着我的“Sheet2Proxy”类将不得不通过它的“IViewEvents”来持有对 Presenter 的弱引用(再次参见 # 2)接口?
4

1 回答 1

4

您正在抽象Worksheet“代理”类的背后;根据定义,它与工作表耦合,您想要确保抽象是密封的,以免您看到泄漏的抽象并最终将其他代码与该Excel.Worksheet类型耦合,从而破坏了整个目的。

对于项目的其余部分,工作表代理类充当了一个门面,它操纵和理解有关特定的所有信息Excel.Worksheet:这样做的结果是您现在可以使用两个模块来抽象工作表的东西 - 工作表本身,和代理类:

  • 工作表代码隐藏可以抽象ListObject/tables、命名范围等内容;使用Property Get代理可以使用的成员。
  • 工作表代理类从其余代码中抽象出工作表操作。

事实上,这种方法并没有为实际工作表代码隐藏留下太多空间/需要:我会开始在代理类中编写所有内容,如果该模块变得过于冗长,或者我发现它的抽象级别需要得到一个再高一点,然后我会将较低级别的东西移到工作表本身的代码隐藏中。

Workheet模块和其他文档模块不应该实现接口 - 让工作表实现接口是混淆和崩溃 VBA 的好方法:不要这样做。所以这可能是你的代码隐藏:

Option Explicit

Public Property Get SomeSpecificRange() As Range
    Set SomeSpecificRange = Me.Names("SomeSpecificRange").RefersToRange
End Property

然后代理类可以这样做:

Option Explicit
Private sheetUI As Sheet1
Private WithEvents sheet As Worksheet

Private Sub Class_Initialize()
    Set sheet = Sheet1
    Set sheetUI = Sheet1
End Sub

Private Sub sheet_Change(ByVal Target As Range)
    If Intersect(Target, sheetUI.SomeSpecificRange) Then
        '...
    End If
End Sub

因此,代理类可以很好地处理工作表事件,而无需整个适配器管道。Public它还可以通过其公开的成员处理来自演示者的命令。

但是代理类又名“抽象工作表”不是响应事件的正确位置:它是需要运行节目的演示者。

因此,您让代理触发一个事件以响应工作表事件,将消息打包并转发给演示者:

Option Explicit
Public Event SomeSpecificRangeChanged()
Private sheetUI As Sheet1
Private WithEvents sheet As Worksheet

Private Sub Class_Initialize()
    Set sheet = Sheet1
    Set sheetUI = Sheet1
End Sub

Private Sub sheet_Change(ByVal Target As Range)
    If Intersect(Target, sheetUI.SomeSpecificRange) Then
        RaiseEvent SomeSpecificRangeChanged
    End If
End Sub

然后演示者可以处理SomeSpecificRangeChanged代理类 - 调出一些用户表单,启动一些数据库查询,无论要求是什么:

Private WithEvents proxy As Sheet1Proxy

Private Sub Class_Initialize()
    Set proxy = New Sheet1Proxy
End Sub

Private Sub proxy_SomeSpecificRangeChanged()
    'business logic to run when SomeSpecificRange is changed
End Sub

问题是代理类与工作表耦合,现在演示者与代理耦合:我们已经抽象了很多东西,但是仍然没有办法将工作表/代理依赖换成其他东西并测试演示者逻辑而不涉及工作表。

所以我们创建了一个接口来将演示者与代理解耦 - 比如说,ISheet1Proxy......现在我们被卡住了,因为我们无法在接口上公开事件。

这就是适配器模式发挥作用的地方,它允许我们为“命令”(演示者 -> 视图)和“事件”(视图 -> 演示者)的接口形式化。

使用适配器,工作表/代理和演示者现在完全解耦,现在您可以实现演示者逻辑,而无需任何知识Excel.Worksheet,理想情况下是任何Excel.RangeExcel.*:每个工作表交互都被形式化为一些“命令”,发送到视图/工作表/代理,或发送给演示者的一些“事件”,就像在战舰项目中一样。

旁注,我发现WeakReference正确拆除对象层次结构并不总是需要这些东西:这就是为什么在当前版本的战舰代码中不再使用它的原因。


显然,这是很多工作。对于 OOP 原则和学习编写可进行单元测试的解耦代码,这是一个很好的实践……但对于一个小型 VBA 项目来说,这对于 IMO 来说太过分了。


所有这些都将Excel.*类视为具体类型,就 VBA 而言,也可能是这种情况。然而,Excel就 .NET 而言,互操作类型都是接口,因此 Rubberduck 将通过为广受欢迎的 .NET 模拟框架Moq提供包装 API来极大地简化一切:

Rubberduck 模拟框架即将推出

这将消除将工作表与用户代码完全分离以使其完全可测试的需要 - 唯一的要求是依赖注入,即更喜欢这个:

Public Sub DoSomething(ByVal target As Range)
    target.Value = 42
End Sub

在此:

Public Sub DoSomething()
    Dim target As Range
    Set target = Sheet1.Range("A1")
    target.Value = 42
End Sub
于 2019-04-15T15:21:07.743 回答