如何 运行 `Create` 函数中的子项以及如何为图表 `Series` 创建 mock/stub/fake?
How to run a sub in a `Create` function and how to make a mock/stub/fake for a chart `Series`?
前言
大约 10 年前,我开始重构和改进 John Walkenbach 的 ChartSeries
class。不幸的是,原来的它似乎不再在线。
跟随 Rubberduck Blog 一段时间后,我努力提高自己的 VBA 技能。但过去我只写过——我想专家们会称之为——“脚本般的上帝程序”(因为不知道更好)。所以我对 classes 很陌生,尤其是接口和工厂。
实际问题
我尝试通过将其分成多个 class 来重构整个 class,同时使用接口并添加单元测试。对于仅 阅读 公式的部分,获取 Series.Formula
然后进行所有处理就足够了。所以最好在 Create
函数中调用 Run
sub。但是到目前为止我尝试做的一切都失败了。因此,我目前 运行 Run
在所有 Get
属性等中(并测试,如果公式更改并退出 Run
比。这可能吗?如果是,如何?
其次,要添加单元测试——当然要为它们使用 rubberduck——我目前依赖 real Charts
/ChartObjects
.如何为 Series
创建 stub/mock/fake? (抱歉,我不知道正确的术语。)
这里是代码的简化版本。
非常感谢您的帮助。
普通模块
'@Folder("ChartSeries")
Option Explicit
Public Sub ExampleUsage()
Dim wks As Worksheet
Set wks = ThisWorkbook.Worksheets(1)
Dim crt As ChartObject
Set crt = wks.ChartObjects(1)
Dim srs As Series
Set srs = crt.Chart.SeriesCollection(3)
Dim MySeries As IChartSeries
Set MySeries = ChartSeries.Create(srs)
With MySeries
Debug.Print .XValues.FormulaPart
End With
End Sub
IChartSeries.cls
'@Folder("ChartSeries")
'@Interface
Option Explicit
Public Function IsSeriesAccessible() As Boolean
End Function
Public Property Get FullFormula() As String
End Property
Public Property Get XValues() As ISeriesPart
End Property
'more properties ...
ChartSeries.cls
'@PredeclaredId
'@Exposed
'@Folder("ChartSeries")
Option Explicit
Implements IChartSeries
Private Type TChartSeries
Series As Series
FullSeriesFormula As String
OldFullSeriesFormula As String
IsSeriesAccessible As Boolean
SeriesParts(eElement.[_First] To eElement.[_Last]) As ISeriesPart
End Type
Private This As TChartSeries
Public Function Create(ByVal Value As Series) As IChartSeries
'NOTE: I would like to run the 'Run' sub somewhere here (if possible)
With New ChartSeries
.Series = Value
Set Create = .Self
End With
End Function
Public Property Get Self() As IChartSeries
Set Self = Me
End Property
Friend Property Let Series(ByVal Value As Series)
Set This.Series = Value
End Property
Private Function IChartSeries_IsSeriesAccessible() As Boolean
Call Run
IChartSeries_IsSeriesAccessible = This.IsSeriesAccessible
End Function
Private Property Get IChartSeries_FullFormula() As String
Call Run
IChartSeries_FullFormula = This.FullSeriesFormula
End Property
Private Property Get IChartSeries_XValues() As ISeriesPart
Call Run
Set IChartSeries_XValues = This.SeriesParts(eElement.eXValues)
End Property
'more properties ...
Private Sub Class_Initialize()
With This
Dim Element As eElement
For Element = eElement.[_First] To eElement.[_Last]
Set .SeriesParts(Element) = New SeriesPart
Next
End With
End Sub
Private Sub Class_Terminate()
With This
Dim Element As LongPtr
For Element = eElement.[_First] To eElement.[_Last]
Set .SeriesParts(Element) = Nothing
Next
End With
End Sub
Private Sub Run()
If Not GetFullSeriesFormula Then Exit Sub
If Not HasFormulaChanged Then Exit Sub
Call GetSeriesFormulaParts
End Sub
'(simplified version)
Private Function GetFullSeriesFormula() As Boolean
GetFullSeriesFormula = False
With This
'---
'dummy to make it work
.FullSeriesFormula = _
"=SERIES(Tabelle1!$B,Tabelle1!$A:$A,Tabelle1!$B:$B,1)"
'---
.OldFullSeriesFormula = .FullSeriesFormula
.FullSeriesFormula = .Series.Formula
End With
GetFullSeriesFormula = True
End Function
Private Function HasFormulaChanged() As Boolean
With This
HasFormulaChanged = (.OldFullSeriesFormula <> .FullSeriesFormula)
End With
End Function
Private Sub GetSeriesFormulaParts()
Dim MySeries As ISeriesFormulaParts
'(simplified version without check for Bubble Chart)
Set MySeries = SeriesFormulaParts.Create( _
This.FullSeriesFormula, _
False _
)
With MySeries
Dim Element As eElement
For Element = eElement.[_First] To eElement.[_Last] - 1
This.SeriesParts(Element).FormulaPart = _
.PartSeriesFormula(Element)
Next
'---
'dummy which normally would be retrieved
'by 'MySeries.PartSeriesFormula(eElement.eXValues)'
This.SeriesParts(eElement.eXValues).FormulaPart = _
"Tabelle1!$A:$A"
'---
End With
Set MySeries = Nothing
End Sub
'more subs and functions ...
ISeriesPart.cls
'@Folder("ChartSeries")
'@Interface
Option Explicit
Public Enum eEntryType
eNotSet = -1
[_First] = 0
eInaccessible = eEntryType.[_First]
eEmpty
eInteger
eString
eArray
eRange
[_Last] = eEntryType.eRange
End Enum
Public Property Get FormulaPart() As String
End Property
Public Property Let FormulaPart(ByVal Value As String)
End Property
Public Property Get EntryType() As eEntryType
End Property
Public Property Get Range() As Range
End Property
'more properties ...
SeriesPart.cls
'@PredeclaredId
'@Folder("ChartSeries")
'@ModuleDescription("A class to handle each part of the 'Series' string.")
Option Explicit
Implements ISeriesPart
Private Type TSeriesPart
FormulaPart As String
EntryType As eEntryType
Range As Range
RangeString As String
RangeSheet As String
RangeBook As String
RangePath As String
End Type
Private This As TSeriesPart
Private Property Get ISeriesPart_FormulaPart() As String
ISeriesPart_FormulaPart = This.FormulaPart
End Property
Private Property Let ISeriesPart_FormulaPart(ByVal Value As String)
This.FormulaPart = Value
Call Run
End Property
Private Property Get ISeriesPart_EntryType() As eEntryType
ISeriesPart_EntryType = This.EntryType
End Property
Private Property Get ISeriesPart_Range() As Range
With This
If .EntryType = eEntryType.eRange Then
Set ISeriesPart_Range = .Range
Else
' Call RaiseError
End If
End With
End Property
Private Property Set ISeriesPart_Range(ByVal Value As Range)
Set This.Range = Value
End Property
'more properties ...
Private Sub Class_Initialize()
This.EntryType = eEntryType.eNotSet
End Sub
Private Sub Run()
'- set 'EntryType'
'- If it is a range then find the range parts ...
End Sub
'a lot more subs and functions ...
ISeriesParts.cls
'@Folder("ChartSeries")
'@Interface
Option Explicit
Public Enum eElement
[_First] = 1
eName = eElement.[_First]
eXValues
eYValues
ePlotOrder
eBubbleSizes
[_Last] = eElement.eBubbleSizes
End Enum
'@Description("fill me")
Public Property Get PartSeriesFormula(ByVal Element As eElement) As String
End Property
SeriesFormulaParts.cls
'@PredeclaredId
'@Exposed
'@Folder("ChartSeries")
Option Explicit
Implements ISeriesFormulaParts
Private Type TSeriesFormulaParts
FullSeriesFormula As String
IsSeriesInBubbleChart As Boolean
WasRunCalled As Boolean
SeriesFormula As String
RemainingFormulaPart(eElement.[_First] To eElement.[_Last]) As String
PartSeriesFormula(eElement.[_First] To eElement.[_Last]) As String
End Type
Private This As TSeriesFormulaParts
Public Function Create( _
ByVal FullSeriesFormula As String, _
ByVal IsSeriesInBubbleChart As Boolean _
) As ISeriesFormulaParts
'NOTE: I would like to run the 'Run' sub somewhere here (if possible)
With New SeriesFormulaParts
.FullSeriesFormula = FullSeriesFormula
.IsSeriesInBubbleChart = IsSeriesInBubbleChart
Set Create = .Self
End With
End Function
Public Property Get Self() As ISeriesFormulaParts
Set Self = Me
End Property
'@Description("Set the full series formula ('ChartSeries')")
Public Property Let FullSeriesFormula(ByVal Value As String)
This.FullSeriesFormula = Value
End Property
Public Property Let IsSeriesInBubbleChart(ByVal Value As Boolean)
This.IsSeriesInBubbleChart = Value
End Property
Private Property Get ISeriesFormulaParts_PartSeriesFormula(ByVal Element As eElement) As String
'NOTE: Instead of running 'Run' here, it would be better to run it in 'Create'
Call Run
ISeriesFormulaParts_PartSeriesFormula = This.PartSeriesFormula(Element)
End Property
'(replaced with a dummy)
Private Sub Run()
If This.WasRunCalled Then Exit Sub
'extract stuff from
This.WasRunCalled = True
End Sub
'a lot more subs and functions ...
你已经可以了!
Public Function Create(ByVal Value As Series) As IChartSeries
With New ChartSeries <~ With block variable has access to members of the ChartSeries class
.Series = Value
Set Create = .Self
End With
End Function
...只是,像 .Series
和 .Self
属性一样,它必须是 ChartSeries
interface/class 的 Public
成员( VBA 中的线条很模糊,因为每个 class 都有一个默认界面/也是一个界面)。
惯用对象分配
关于此的说明 属性:
Friend Property Let Series(ByVal Value As Series)
Set This.Series = Value
End Property
将 Property Let
成员用于 Set
对象引用将起作用 - 但它不再是惯用的 VBA 代码,如您在 .Create
中所见功能:
.Series = Value
如果我们在不知道 属性 的性质的情况下阅读这一行,这看起来就像任何其他赋值。唯一的问题是,我们没有分配 value,而是分配 reference - VBA 中的引用分配通常使用Set
关键字。如果我们在 Series
属性 定义中将 Let
更改为 Set
,我们将不得不这样做:
Set .Series = Value
这看起来更像是参考作业!没有它,似乎隐含的 let-coercion 正在发生,这使得代码变得模棱两可:VBA 需要一个 Set
关键字来进行引用赋值,因为任何给定的对象 都可以 有一个无参数的默认值 属性(例如 foo = Range("A1")
如何将 foo
隐式分配给 Range
的 Value
。
缓存和职责
现在,回到 Run
方法 - 如果它在 ChartSeries
class 上创建 Public
,但未在实现的 IChartSeries
接口上公开,则它是一个只能从 1) ChartSeries
默认实例或 2) 任何具有 ChartSeries
声明类型的对象变量调用的成员。由于我们的 "client code" 正在处理 IChartSeries
,我们可以防范 1 而摆脱 2.
请注意,Call
关键字是多余的,Run
方法实际上只是从封装的 Series
对象中提取元数据,并将其缓存在实例级别 - 我会给它起一个听起来更像 "refresh cached properties" 而不是 "run something".
的名字
您的预感很好:Property Get
应该是一个简单的 return 函数,没有任何副作用。在 Property Get
访问器中调用扫描对象并重置实例状态的方法会产生副作用,这在理论上是一种设计味道。
如果Run
在Create
函数return实例创建之前立即被调用,那么这个Run
方法归结为"parse the series and cache some metadata I'll reuse later",这并没有错:从 Create
调用它,并从 Property Get
访问器中删除它。
结果是一个状态为只读且定义更可靠的对象;对应的是你现在有一个对象,其状态可能与工作表上的实际 Excel Series
对象不同步:如果代码(或用户)调整 Series
IChartSeries
初始化后的对象,对象及其状态是陈旧的。
一个解决方案是不遗余力地识别系列何时过时,并确保保持缓存最新。
另一种解决方案是通过不再缓存状态来完全解决问题 - 这意味着以下两种情况之一:
在创建时生成一次对象图,有效地将缓存责任转移给调用者:调用代码得到一个只读的 "snapshot" 来处理。
每次调用代码需要它时,都会从系列元数据中生成一个新的对象图:有效地将缓存责任转移给调用者,这根本不是一个坏主意。
将内容设置为只读可以消除很多复杂性!我会选择第一个选项。
总的来说,代码看起来干净整洁(尽管不清楚为此 post 擦除了多少)并且您似乎已经理解 工厂方法 模式利用默认实例并公开外观界面 - 荣誉!命名总体上非常好(尽管 "Run" 突出了 IMO),并且每个对象看起来都有一个明确、明确的目的。干得好!
单元测试
I currently rely on real Charts/ChartObjects. How do I create a stub/mock/fake for a Series? (Sorry, I don't know the correct term.)
目前,您不能。当 if/when this PR gets merged 时,您将能够 模拟 Excel 的接口(以及更多,更多)并针对您的 classes 编写测试注入一个模拟 Excel.Series
对象,你可以为你的测试目的配置它......但在那之前,这就是墙所在的地方。
与此同时,你能做的最好的事情就是用你自己的接口包装它,然后 stub 它。换句话说,只要你的代码和 Excel 的对象模型之间存在接缝,我们就会在两者之间插入一个接口:你不会接受 Excel.Series
对象,而是接受一些 ISeriesWrapper
,然后实际代码将使用 ExcelSeriesWrapper
来处理 Excel.Series
,测试代码可能使用 StubSeriesWrapper
,其属性 return 或者硬编码值,或由测试配置的值:在 Excel
库和您的项目之间的接缝处工作的代码无法测试 - 我们无论如何都不想,因为那样我们会正在测试 Excel,而不是我们自己的代码。
您可以在下一篇即将发布的 RD 新闻文章的示例代码中看到这一点 here;那篇文章将使用 ADODB 连接来讨论这一点。原理是相同的:该项目中的 94 个单元测试中的 none 曾经打开任何实际连接,但是通过依赖注入和包装器接口,我们能够测试每一个功能,从打开数据库连接开始提交事务...而无需访问实际数据库。
前言
大约 10 年前,我开始重构和改进 John Walkenbach 的 ChartSeries
class。不幸的是,原来的它似乎不再在线。
跟随 Rubberduck Blog 一段时间后,我努力提高自己的 VBA 技能。但过去我只写过——我想专家们会称之为——“脚本般的上帝程序”(因为不知道更好)。所以我对 classes 很陌生,尤其是接口和工厂。
实际问题
我尝试通过将其分成多个 class 来重构整个 class,同时使用接口并添加单元测试。对于仅 阅读 公式的部分,获取 Series.Formula
然后进行所有处理就足够了。所以最好在 Create
函数中调用 Run
sub。但是到目前为止我尝试做的一切都失败了。因此,我目前 运行 Run
在所有 Get
属性等中(并测试,如果公式更改并退出 Run
比。这可能吗?如果是,如何?
其次,要添加单元测试——当然要为它们使用 rubberduck——我目前依赖 real Charts
/ChartObjects
.如何为 Series
创建 stub/mock/fake? (抱歉,我不知道正确的术语。)
这里是代码的简化版本。
非常感谢您的帮助。
普通模块
'@Folder("ChartSeries")
Option Explicit
Public Sub ExampleUsage()
Dim wks As Worksheet
Set wks = ThisWorkbook.Worksheets(1)
Dim crt As ChartObject
Set crt = wks.ChartObjects(1)
Dim srs As Series
Set srs = crt.Chart.SeriesCollection(3)
Dim MySeries As IChartSeries
Set MySeries = ChartSeries.Create(srs)
With MySeries
Debug.Print .XValues.FormulaPart
End With
End Sub
IChartSeries.cls
'@Folder("ChartSeries")
'@Interface
Option Explicit
Public Function IsSeriesAccessible() As Boolean
End Function
Public Property Get FullFormula() As String
End Property
Public Property Get XValues() As ISeriesPart
End Property
'more properties ...
ChartSeries.cls
'@PredeclaredId
'@Exposed
'@Folder("ChartSeries")
Option Explicit
Implements IChartSeries
Private Type TChartSeries
Series As Series
FullSeriesFormula As String
OldFullSeriesFormula As String
IsSeriesAccessible As Boolean
SeriesParts(eElement.[_First] To eElement.[_Last]) As ISeriesPart
End Type
Private This As TChartSeries
Public Function Create(ByVal Value As Series) As IChartSeries
'NOTE: I would like to run the 'Run' sub somewhere here (if possible)
With New ChartSeries
.Series = Value
Set Create = .Self
End With
End Function
Public Property Get Self() As IChartSeries
Set Self = Me
End Property
Friend Property Let Series(ByVal Value As Series)
Set This.Series = Value
End Property
Private Function IChartSeries_IsSeriesAccessible() As Boolean
Call Run
IChartSeries_IsSeriesAccessible = This.IsSeriesAccessible
End Function
Private Property Get IChartSeries_FullFormula() As String
Call Run
IChartSeries_FullFormula = This.FullSeriesFormula
End Property
Private Property Get IChartSeries_XValues() As ISeriesPart
Call Run
Set IChartSeries_XValues = This.SeriesParts(eElement.eXValues)
End Property
'more properties ...
Private Sub Class_Initialize()
With This
Dim Element As eElement
For Element = eElement.[_First] To eElement.[_Last]
Set .SeriesParts(Element) = New SeriesPart
Next
End With
End Sub
Private Sub Class_Terminate()
With This
Dim Element As LongPtr
For Element = eElement.[_First] To eElement.[_Last]
Set .SeriesParts(Element) = Nothing
Next
End With
End Sub
Private Sub Run()
If Not GetFullSeriesFormula Then Exit Sub
If Not HasFormulaChanged Then Exit Sub
Call GetSeriesFormulaParts
End Sub
'(simplified version)
Private Function GetFullSeriesFormula() As Boolean
GetFullSeriesFormula = False
With This
'---
'dummy to make it work
.FullSeriesFormula = _
"=SERIES(Tabelle1!$B,Tabelle1!$A:$A,Tabelle1!$B:$B,1)"
'---
.OldFullSeriesFormula = .FullSeriesFormula
.FullSeriesFormula = .Series.Formula
End With
GetFullSeriesFormula = True
End Function
Private Function HasFormulaChanged() As Boolean
With This
HasFormulaChanged = (.OldFullSeriesFormula <> .FullSeriesFormula)
End With
End Function
Private Sub GetSeriesFormulaParts()
Dim MySeries As ISeriesFormulaParts
'(simplified version without check for Bubble Chart)
Set MySeries = SeriesFormulaParts.Create( _
This.FullSeriesFormula, _
False _
)
With MySeries
Dim Element As eElement
For Element = eElement.[_First] To eElement.[_Last] - 1
This.SeriesParts(Element).FormulaPart = _
.PartSeriesFormula(Element)
Next
'---
'dummy which normally would be retrieved
'by 'MySeries.PartSeriesFormula(eElement.eXValues)'
This.SeriesParts(eElement.eXValues).FormulaPart = _
"Tabelle1!$A:$A"
'---
End With
Set MySeries = Nothing
End Sub
'more subs and functions ...
ISeriesPart.cls
'@Folder("ChartSeries")
'@Interface
Option Explicit
Public Enum eEntryType
eNotSet = -1
[_First] = 0
eInaccessible = eEntryType.[_First]
eEmpty
eInteger
eString
eArray
eRange
[_Last] = eEntryType.eRange
End Enum
Public Property Get FormulaPart() As String
End Property
Public Property Let FormulaPart(ByVal Value As String)
End Property
Public Property Get EntryType() As eEntryType
End Property
Public Property Get Range() As Range
End Property
'more properties ...
SeriesPart.cls
'@PredeclaredId
'@Folder("ChartSeries")
'@ModuleDescription("A class to handle each part of the 'Series' string.")
Option Explicit
Implements ISeriesPart
Private Type TSeriesPart
FormulaPart As String
EntryType As eEntryType
Range As Range
RangeString As String
RangeSheet As String
RangeBook As String
RangePath As String
End Type
Private This As TSeriesPart
Private Property Get ISeriesPart_FormulaPart() As String
ISeriesPart_FormulaPart = This.FormulaPart
End Property
Private Property Let ISeriesPart_FormulaPart(ByVal Value As String)
This.FormulaPart = Value
Call Run
End Property
Private Property Get ISeriesPart_EntryType() As eEntryType
ISeriesPart_EntryType = This.EntryType
End Property
Private Property Get ISeriesPart_Range() As Range
With This
If .EntryType = eEntryType.eRange Then
Set ISeriesPart_Range = .Range
Else
' Call RaiseError
End If
End With
End Property
Private Property Set ISeriesPart_Range(ByVal Value As Range)
Set This.Range = Value
End Property
'more properties ...
Private Sub Class_Initialize()
This.EntryType = eEntryType.eNotSet
End Sub
Private Sub Run()
'- set 'EntryType'
'- If it is a range then find the range parts ...
End Sub
'a lot more subs and functions ...
ISeriesParts.cls
'@Folder("ChartSeries")
'@Interface
Option Explicit
Public Enum eElement
[_First] = 1
eName = eElement.[_First]
eXValues
eYValues
ePlotOrder
eBubbleSizes
[_Last] = eElement.eBubbleSizes
End Enum
'@Description("fill me")
Public Property Get PartSeriesFormula(ByVal Element As eElement) As String
End Property
SeriesFormulaParts.cls
'@PredeclaredId
'@Exposed
'@Folder("ChartSeries")
Option Explicit
Implements ISeriesFormulaParts
Private Type TSeriesFormulaParts
FullSeriesFormula As String
IsSeriesInBubbleChart As Boolean
WasRunCalled As Boolean
SeriesFormula As String
RemainingFormulaPart(eElement.[_First] To eElement.[_Last]) As String
PartSeriesFormula(eElement.[_First] To eElement.[_Last]) As String
End Type
Private This As TSeriesFormulaParts
Public Function Create( _
ByVal FullSeriesFormula As String, _
ByVal IsSeriesInBubbleChart As Boolean _
) As ISeriesFormulaParts
'NOTE: I would like to run the 'Run' sub somewhere here (if possible)
With New SeriesFormulaParts
.FullSeriesFormula = FullSeriesFormula
.IsSeriesInBubbleChart = IsSeriesInBubbleChart
Set Create = .Self
End With
End Function
Public Property Get Self() As ISeriesFormulaParts
Set Self = Me
End Property
'@Description("Set the full series formula ('ChartSeries')")
Public Property Let FullSeriesFormula(ByVal Value As String)
This.FullSeriesFormula = Value
End Property
Public Property Let IsSeriesInBubbleChart(ByVal Value As Boolean)
This.IsSeriesInBubbleChart = Value
End Property
Private Property Get ISeriesFormulaParts_PartSeriesFormula(ByVal Element As eElement) As String
'NOTE: Instead of running 'Run' here, it would be better to run it in 'Create'
Call Run
ISeriesFormulaParts_PartSeriesFormula = This.PartSeriesFormula(Element)
End Property
'(replaced with a dummy)
Private Sub Run()
If This.WasRunCalled Then Exit Sub
'extract stuff from
This.WasRunCalled = True
End Sub
'a lot more subs and functions ...
你已经可以了!
Public Function Create(ByVal Value As Series) As IChartSeries With New ChartSeries <~ With block variable has access to members of the ChartSeries class .Series = Value Set Create = .Self End With End Function
...只是,像 .Series
和 .Self
属性一样,它必须是 ChartSeries
interface/class 的 Public
成员( VBA 中的线条很模糊,因为每个 class 都有一个默认界面/也是一个界面)。
惯用对象分配
关于此的说明 属性:
Friend Property Let Series(ByVal Value As Series) Set This.Series = Value End Property
将 Property Let
成员用于 Set
对象引用将起作用 - 但它不再是惯用的 VBA 代码,如您在 .Create
中所见功能:
.Series = Value
如果我们在不知道 属性 的性质的情况下阅读这一行,这看起来就像任何其他赋值。唯一的问题是,我们没有分配 value,而是分配 reference - VBA 中的引用分配通常使用Set
关键字。如果我们在 Series
属性 定义中将 Let
更改为 Set
,我们将不得不这样做:
Set .Series = Value
这看起来更像是参考作业!没有它,似乎隐含的 let-coercion 正在发生,这使得代码变得模棱两可:VBA 需要一个 Set
关键字来进行引用赋值,因为任何给定的对象 都可以 有一个无参数的默认值 属性(例如 foo = Range("A1")
如何将 foo
隐式分配给 Range
的 Value
。
缓存和职责
现在,回到 Run
方法 - 如果它在 ChartSeries
class 上创建 Public
,但未在实现的 IChartSeries
接口上公开,则它是一个只能从 1) ChartSeries
默认实例或 2) 任何具有 ChartSeries
声明类型的对象变量调用的成员。由于我们的 "client code" 正在处理 IChartSeries
,我们可以防范 1 而摆脱 2.
请注意,Call
关键字是多余的,Run
方法实际上只是从封装的 Series
对象中提取元数据,并将其缓存在实例级别 - 我会给它起一个听起来更像 "refresh cached properties" 而不是 "run something".
您的预感很好:Property Get
应该是一个简单的 return 函数,没有任何副作用。在 Property Get
访问器中调用扫描对象并重置实例状态的方法会产生副作用,这在理论上是一种设计味道。
如果Run
在Create
函数return实例创建之前立即被调用,那么这个Run
方法归结为"parse the series and cache some metadata I'll reuse later",这并没有错:从 Create
调用它,并从 Property Get
访问器中删除它。
结果是一个状态为只读且定义更可靠的对象;对应的是你现在有一个对象,其状态可能与工作表上的实际 Excel Series
对象不同步:如果代码(或用户)调整 Series
IChartSeries
初始化后的对象,对象及其状态是陈旧的。
一个解决方案是不遗余力地识别系列何时过时,并确保保持缓存最新。
另一种解决方案是通过不再缓存状态来完全解决问题 - 这意味着以下两种情况之一:
在创建时生成一次对象图,有效地将缓存责任转移给调用者:调用代码得到一个只读的 "snapshot" 来处理。
每次调用代码需要它时,都会从系列元数据中生成一个新的对象图:有效地将缓存责任转移给调用者,这根本不是一个坏主意。
将内容设置为只读可以消除很多复杂性!我会选择第一个选项。
总的来说,代码看起来干净整洁(尽管不清楚为此 post 擦除了多少)并且您似乎已经理解 工厂方法 模式利用默认实例并公开外观界面 - 荣誉!命名总体上非常好(尽管 "Run" 突出了 IMO),并且每个对象看起来都有一个明确、明确的目的。干得好!
单元测试
I currently rely on real Charts/ChartObjects. How do I create a stub/mock/fake for a Series? (Sorry, I don't know the correct term.)
目前,您不能。当 if/when this PR gets merged 时,您将能够 模拟 Excel 的接口(以及更多,更多)并针对您的 classes 编写测试注入一个模拟 Excel.Series
对象,你可以为你的测试目的配置它......但在那之前,这就是墙所在的地方。
与此同时,你能做的最好的事情就是用你自己的接口包装它,然后 stub 它。换句话说,只要你的代码和 Excel 的对象模型之间存在接缝,我们就会在两者之间插入一个接口:你不会接受 Excel.Series
对象,而是接受一些 ISeriesWrapper
,然后实际代码将使用 ExcelSeriesWrapper
来处理 Excel.Series
,测试代码可能使用 StubSeriesWrapper
,其属性 return 或者硬编码值,或由测试配置的值:在 Excel
库和您的项目之间的接缝处工作的代码无法测试 - 我们无论如何都不想,因为那样我们会正在测试 Excel,而不是我们自己的代码。
您可以在下一篇即将发布的 RD 新闻文章的示例代码中看到这一点 here;那篇文章将使用 ADODB 连接来讨论这一点。原理是相同的:该项目中的 94 个单元测试中的 none 曾经打开任何实际连接,但是通过依赖注入和包装器接口,我们能够测试每一个功能,从打开数据库连接开始提交事务...而无需访问实际数据库。