VBA 工厂 VB_PredeclaredId = True 与 "anonymous" 实例优缺点
VBA Factory with VB_PredeclaredId = True vs "anonymous" instance pros and cons
好奇 VB_PredeclaredId = True 的工厂和 VBA 中的 "anonymous" 实例之间的 advantages/disadvantages 是什么。每个都有自己的场景或更好地坚持其中一个,为什么?任何我可以阅读更多相关信息的反馈或链接将不胜感激!谢谢!
从 A1 开始的工作表数据
Name Manager Target
Person1 Manager1 97
Person2 Manager2 92
Person3 Manager3 95
Person4 Manager4 105
Person5 Manager5 108
Person6 Manager6 88
工厂 - Class
'@Folder("VBAProject")
Option Explicit
'@PredeclaredId
Public Function getPerson(ByVal name As String, ByVal manager As String, ByVal target As Double) As Person
With New Person
.name = name
.manager = manager
.Targer = target
Set getPerson = .self
End With
End Function
人 - Class
Private pName As String
Private pManager As String
Private pTarger As Double
Public Property Get Targer() As Double
Targer = pTarger
End Property
Public Property Let Targer(ByVal value As Double)
pTarger = value
End Property
Public Property Get manager() As String
manager = pManager
End Property
Public Property Let manager(ByVal value As String)
pManager = value
End Property
Public Property Get name() As String
name = pName
End Property
Public Property Let name(ByVal value As String)
pName = value
End Property
Public Function toString() As String
toString = "Name: " & Me.name & ", Manager: " & Me.manager & ", Targer: " & Me.Targer
End Function
Public Function self() As Person
Set self = Me
End Function
测试 - 模块
Sub test()
Dim i As Long
For i = 2 To 6
With New Person
.name = Range("A" & i)
.manager = Range("b" & i)
.Targer = Range("c" & i)
Debug.Print .toString
End With
Debug.Print Factory.getPerson(name:=Range("A" & i), _
manager:=Range("B" & i), target:=Range("C" & i)).toString
'or shorter whithout feild names
Debug.Print Factory.getPerson(Range("A" & i), Range("B" & i), Range("C" & i)).toString
Next i
End Sub
TL;DR: 真的是苹果和香蕉。匿名对象是一种语言结构;工厂没有在语言规范中定义,它们更像是一种 设计模式 ,是的,使用每种不同的方法有不同的原因 - 尽管 IMO "factory bag module"是个坏主意。
匿名对象
匿名对象很棒 - 您可以调用 With
块持有的对象的成员,而无需添加局部变量。那是 With New
,但它对 With CreateObject
:
也特别有用
With CreateObject("Scripting.FileSystemObject")
'...
End With
它们创建对象,但这些对象(通常 - .Self
getter 可以阻止它)被限制在包含 With
的块中;匿名对象对于您此时此地需要的对象很有用,超过那个点就不再需要了。它们基本上是语言功能。
工厂Class(或模块)
我们将离开 语言特性 领域,进入 设计模式 领域。现在我们正在谈论将给定对象的相对复杂的创建封装在专用于该目的的 class 中。
VB_PredeclaredId = True
属性(无论是否由 @PredeclaredId
Rubberduck 注释设置)使 class 像任何其他标准模块一样工作。
Factory
class 公开了 getPerson
(或 CreatePerson
)方法的问题是,您现在有借口用一些 getAnotherThing
(或 CreateAnotherThing
),不知不觉中,您看到的是一个 class/module,它几乎可以创造任何东西,无论这些东西是否相关。
肯定有更好的方法。
工厂方法
如果 Person
对象知道如何创建 Person
class 的新实例会怎么样?
Set p = Person.Create(1188513, "Mathieu")
这需要 Person
class 上的 VB_PredeclaredId = True
属性和 Person
的 默认接口 才能公开要从该默认实例调用的 Create
方法。
问题是,现在任何使用 Person
对象的东西现在都会看到一个令人困惑的 API,它公开了 ID
和 Name
的 Property Let
成员属性,以及一个仅用于默认实例的 Create
函数。
这个问题有一个解决方案:唯一 "allowed" 与 Person
一起工作的代码应该是负责调用该类型的 Create
工厂方法的代码。其他一切都只会看到一个 IPerson
- Person
实现的一个显式接口,用于定义其余代码应如何与该类型的对象交互......而 IPerson
不需要公开任何 Property Let
成员,或 Create
方法。
还有一个问题该模式没有解决 - 假设 IPerson
由两个或多个 class 实现(例如,Employee
和 Manager
) : 即使整个项目只看到 IPerson
,仍然有一些代码在调用 Employee.Create
或 Manager.Create
,并且这些代码本质上是 耦合的 与 IPerson
接口的这些特定实现。这是一个问题,因为这种耦合首先从根本上否定了针对接口进行编码的好处。
抽象工厂
为了将创建 IPerson
对象的代码与 Employee
和 Manager
classes 分离,我们可以创建一个抽象的 IPersonFactory
接口工厂方法本身:现在使用工厂和创建 IPerson
对象的代码甚至不知道它正在创建什么具体类型的对象,解耦已经完成。
你可能不需要那种级别的解耦,除非你已经用一套完整的单元测试覆盖了所有内容,但无论如何知道它的存在是很有用的。
所以你会有 EmployeeFactory
和 ManagerFactory
classes,它们都实现了 IPersonFactory
并为一些 Create
函数提供了一个实现 returns 一个 IPerson
对象。
...老实说,这就是 person/employee 示例有点崩溃的地方,因为这样的 class 更像是一个简单的 DTO(数据传输对象 - 即一堆 public 字段,或 read/write 属性)而不是任何实际有责任的东西 - 所以让我们放弃它。
我们将有一个 SqlConnectionFactory
、一个 OracleConnectionFactory
和一个 MySqlConnectionFactory
,所有这些都实现一些 IDbConnectionFactory
以产生一些封装的 IDbConnection
对象,你会猜到,一个数据库连接。
所以我们可以得到如下代码:
Public Sub DoSomething(ByVal factory As IDbConnectionFactory)
Dim pwd As String
pwd = InputBox("SA Password?") 'bad example: now we're coupled with the InputBox API!
Dim conn As IDbConnection
Set conn = factory.Create("server name", "sa", pwd)
'...
End Sub
DoSomething
方法可以针对 SQL 服务器、Oracle 或 MySQL 数据库服务器执行其操作,并且永远不需要关心是哪个一个它实际上与 .
一起工作
Abstract Factory 在您注入依赖项(c.f。"dependency injection")时非常有用,这些依赖项本身具有直到最后一刻才能解决的依赖项,例如,因为您需要一些用户需要通过一些 UI 提供的参数(理想情况下,它本身也被抽象在接口后面,这样您就可以在不弹出 UI 的情况下对方法进行单元测试 -虽然,Rubberduck 的 Fakes
API 确实允许您劫持一个 InputBox
... 假设我们正在弹出一个 UserForm
。
好奇 VB_PredeclaredId = True 的工厂和 VBA 中的 "anonymous" 实例之间的 advantages/disadvantages 是什么。每个都有自己的场景或更好地坚持其中一个,为什么?任何我可以阅读更多相关信息的反馈或链接将不胜感激!谢谢!
从 A1 开始的工作表数据
Name Manager Target
Person1 Manager1 97
Person2 Manager2 92
Person3 Manager3 95
Person4 Manager4 105
Person5 Manager5 108
Person6 Manager6 88
工厂 - Class
'@Folder("VBAProject")
Option Explicit
'@PredeclaredId
Public Function getPerson(ByVal name As String, ByVal manager As String, ByVal target As Double) As Person
With New Person
.name = name
.manager = manager
.Targer = target
Set getPerson = .self
End With
End Function
人 - Class
Private pName As String
Private pManager As String
Private pTarger As Double
Public Property Get Targer() As Double
Targer = pTarger
End Property
Public Property Let Targer(ByVal value As Double)
pTarger = value
End Property
Public Property Get manager() As String
manager = pManager
End Property
Public Property Let manager(ByVal value As String)
pManager = value
End Property
Public Property Get name() As String
name = pName
End Property
Public Property Let name(ByVal value As String)
pName = value
End Property
Public Function toString() As String
toString = "Name: " & Me.name & ", Manager: " & Me.manager & ", Targer: " & Me.Targer
End Function
Public Function self() As Person
Set self = Me
End Function
测试 - 模块
Sub test()
Dim i As Long
For i = 2 To 6
With New Person
.name = Range("A" & i)
.manager = Range("b" & i)
.Targer = Range("c" & i)
Debug.Print .toString
End With
Debug.Print Factory.getPerson(name:=Range("A" & i), _
manager:=Range("B" & i), target:=Range("C" & i)).toString
'or shorter whithout feild names
Debug.Print Factory.getPerson(Range("A" & i), Range("B" & i), Range("C" & i)).toString
Next i
End Sub
TL;DR: 真的是苹果和香蕉。匿名对象是一种语言结构;工厂没有在语言规范中定义,它们更像是一种 设计模式 ,是的,使用每种不同的方法有不同的原因 - 尽管 IMO "factory bag module"是个坏主意。
匿名对象
匿名对象很棒 - 您可以调用 With
块持有的对象的成员,而无需添加局部变量。那是 With New
,但它对 With CreateObject
:
With CreateObject("Scripting.FileSystemObject")
'...
End With
它们创建对象,但这些对象(通常 - .Self
getter 可以阻止它)被限制在包含 With
的块中;匿名对象对于您此时此地需要的对象很有用,超过那个点就不再需要了。它们基本上是语言功能。
工厂Class(或模块)
我们将离开 语言特性 领域,进入 设计模式 领域。现在我们正在谈论将给定对象的相对复杂的创建封装在专用于该目的的 class 中。
VB_PredeclaredId = True
属性(无论是否由 @PredeclaredId
Rubberduck 注释设置)使 class 像任何其他标准模块一样工作。
Factory
class 公开了 getPerson
(或 CreatePerson
)方法的问题是,您现在有借口用一些 getAnotherThing
(或 CreateAnotherThing
),不知不觉中,您看到的是一个 class/module,它几乎可以创造任何东西,无论这些东西是否相关。
肯定有更好的方法。
工厂方法
如果 Person
对象知道如何创建 Person
class 的新实例会怎么样?
Set p = Person.Create(1188513, "Mathieu")
这需要 Person
class 上的 VB_PredeclaredId = True
属性和 Person
的 默认接口 才能公开要从该默认实例调用的 Create
方法。
问题是,现在任何使用 Person
对象的东西现在都会看到一个令人困惑的 API,它公开了 ID
和 Name
的 Property Let
成员属性,以及一个仅用于默认实例的 Create
函数。
这个问题有一个解决方案:唯一 "allowed" 与 Person
一起工作的代码应该是负责调用该类型的 Create
工厂方法的代码。其他一切都只会看到一个 IPerson
- Person
实现的一个显式接口,用于定义其余代码应如何与该类型的对象交互......而 IPerson
不需要公开任何 Property Let
成员,或 Create
方法。
还有一个问题该模式没有解决 - 假设 IPerson
由两个或多个 class 实现(例如,Employee
和 Manager
) : 即使整个项目只看到 IPerson
,仍然有一些代码在调用 Employee.Create
或 Manager.Create
,并且这些代码本质上是 耦合的 与 IPerson
接口的这些特定实现。这是一个问题,因为这种耦合首先从根本上否定了针对接口进行编码的好处。
抽象工厂
为了将创建 IPerson
对象的代码与 Employee
和 Manager
classes 分离,我们可以创建一个抽象的 IPersonFactory
接口工厂方法本身:现在使用工厂和创建 IPerson
对象的代码甚至不知道它正在创建什么具体类型的对象,解耦已经完成。
你可能不需要那种级别的解耦,除非你已经用一套完整的单元测试覆盖了所有内容,但无论如何知道它的存在是很有用的。
所以你会有 EmployeeFactory
和 ManagerFactory
classes,它们都实现了 IPersonFactory
并为一些 Create
函数提供了一个实现 returns 一个 IPerson
对象。
...老实说,这就是 person/employee 示例有点崩溃的地方,因为这样的 class 更像是一个简单的 DTO(数据传输对象 - 即一堆 public 字段,或 read/write 属性)而不是任何实际有责任的东西 - 所以让我们放弃它。
我们将有一个 SqlConnectionFactory
、一个 OracleConnectionFactory
和一个 MySqlConnectionFactory
,所有这些都实现一些 IDbConnectionFactory
以产生一些封装的 IDbConnection
对象,你会猜到,一个数据库连接。
所以我们可以得到如下代码:
Public Sub DoSomething(ByVal factory As IDbConnectionFactory)
Dim pwd As String
pwd = InputBox("SA Password?") 'bad example: now we're coupled with the InputBox API!
Dim conn As IDbConnection
Set conn = factory.Create("server name", "sa", pwd)
'...
End Sub
DoSomething
方法可以针对 SQL 服务器、Oracle 或 MySQL 数据库服务器执行其操作,并且永远不需要关心是哪个一个它实际上与 .
Abstract Factory 在您注入依赖项(c.f。"dependency injection")时非常有用,这些依赖项本身具有直到最后一刻才能解决的依赖项,例如,因为您需要一些用户需要通过一些 UI 提供的参数(理想情况下,它本身也被抽象在接口后面,这样您就可以在不弹出 UI 的情况下对方法进行单元测试 -虽然,Rubberduck 的 Fakes
API 确实允许您劫持一个 InputBox
... 假设我们正在弹出一个 UserForm
。