VB.NET (WinForms) 中的装饰器模式






2.95/5 (9投票s)
在 WinForms 和 VB.NET 中使用装饰器模式
引言
VB.NET,WinForms 中的装饰器模式!GitHub 仓库在这里。在本文中,我将尝试利用 WinForms 和 SOLID 原则为我们带来的好处。
背景
这是我几年前参加 Pluralsight 课程“封装与 SOLID”(作者:Mark Seemann)的实现。这是一门非常好的课程,我强烈推荐。在此设计中,我还尝试遵循KISS原则。
装饰器 (Decorator)
装饰器模式“装饰”一个对象,在不修改对象或同一类的其他对象的情况下添加额外操作。我将从一个接口开始实现,然后将每个操作添加为一个实现该接口的类。此外,我将通过类似链表(linked list)的结构将类链接在一起。每个类将在其构造函数中接受一个接口。运行时,在执行其操作时,它将首先检查其构造函数中是否接收了操作。如果有,它将先执行它。这种结构可以用于链接任意数量所需的操作。下面是一张图:
Using the Code
在 Visual Studio 中启动一个新的 WinForm 项目,命名为 ToDoSample
,然后保存。为 WinForm 应用添加一个单元测试项目(文件 – 添加 – 新建项目…),命名为 ToDoSampleTests
。
设计用户界面
Winforms 提供了一个良好的拖放界面来帮助我们设计 UI。因此,我们首先利用这种能力来快速设计它。请将标题、文本框、按钮、dataGridView
等放置在 Form1
上。
太棒了!UI 完成!现在我们需要它做些事情。这就是SOLID原则发挥作用的地方。第一个原则,“单一职责原则”。每个类应该只有一个职责。我们思考一下当我们点击“添加”按钮时程序应该做什么。我们将所有这些事情分解成独立的单一职责。这有一定的个人解释空间,但这是我得出的结论:
- 从文本框中获取数据(截止日期和任务)
- 将数据写入
DataGridView
- 按截止日期对
DataGridView
进行排序
这将指导我们创建围绕这些操作的函数。我们将它们各自放入自己的类中。这使得我们可以独立测试它们,并且当我们在六个月后回来添加新功能时,它们易于理解。
设置
我们设计类的方式至关重要。我们遵循更多的 SOLID 原则。接下来的三个原则协同工作:“开闭原则”、“里氏替换原则”和“接口隔离原则”。
除了上述三个原则,我还将致力于通过组合将所有内容整合在一起。这将使函数非常独立,但能在运行时无缝地组合在一起。据我所知,以这种方式使用组合被称为“装饰器模式”。
与 SOLID 原则无关,但为了方便起见,我将把这些操作放在同一个类文件夹中。我称之为“AddToDos”。我们右键单击项目,选择“添加新项” - 类,将其命名为“AddToDos
”。
我们首先添加的是 interface
,“IAddToDos
”。在 interface
之后,我们添加一个值类 class
,命名为“AddToDoVals
”。值类是我为在可组合类之间持久化状态而设计的。现在它看起来是这样的:
Public Interface IAddToDos
End Interface
Public Class AddToDoVals
End Class
我们所有的类都将实现 interface
。这赋予了它们组合在一起的能力。所以我们在 interface
中添加函数“RunMe
”。RunMe
需要以“AddToDoVals
”作为参数,因为这是状态在操作之间保持的方式。所以我们像这样添加到 interface
中:
Public Interface IAddToDos
Function RunMe(ByVal dataObj As AddToDosVals) As AddToDosVals
End Interface
我们完成了 Interface
。接口隔离原则(Interface segregation principle)告诉我们需要小而专一的 interface
。就这样。
现在来处理值类。我不是里氏替换原则(Liskov substitution principle)的专家。我承认我发现它有点令人困惑。但根据我的理解,我们需要使每个类无论做什么,都能以相同的方式开始和结束。每个类都需要健壮。为此,我们首先在 AddToDosVals
值类中添加的内容是告诉我们是否发生了错误。我们创建下面的类并将其添加到 AddToDosVals
中。
Public Class ErrorObj
Public Sub New()
HasError = False
End Sub
Public Property HasError As Boolean
Public Property Message As String
End Class
Public Class AddToDosVals
Public Sub New()
ErrObj = New ErrorObj()
End Sub
Public Property ErrObj As ErrorObj
End Class
错误类提供了一种处理错误的一致方法。一致的错误处理告诉我们,无论类做什么,如果发生错误,它将表现相同。这在使我们的代码更健壮和模块化方面大有裨益。
函数从这里开始
现在我们准备编写函数的代码。从第一个开始,GetDataFromTextBoxes
。所以我们添加一个新类,让它实现 interface
。
Public Class GetDataFromTextBoxes
Implements IAddToDos
Public Function RunMe(dataObj As AddToDosVals) As AddToDosVals Implements IAddToDos.RunMe
Throw New NotImplementedException()
End Function
End Class
我们需要添加数据从 textbox
es 中持久化的位置,所以我们在 AddToDoVals
类中添加属性。
Public Class AddToDosVals
Public Sub New()
ErrObj = New ErrorObj()
End Sub
Public Property ErrObj As ErrorObj
Public Property DueDate As Date
Public Property ToDoTask As String
End Class
为了以可组合的方式链接这些函数,我们将 IAddToDos
接口作为构造函数的输入和一个 private
字段。
Private _runMeFirst As IAddToDos
Public Sub New(ByRef runMeFirst As IAddToDos)
_runMeFirst = runMeFirst
End Sub
接下来,我们希望将所有这个类依赖的东西,以及在“创建”它时可用的东西,都放在构造函数中。对于最后一个 SOLID 原则——依赖反转,我们会传递一个接口来连接数据库或类似的东西。在这个简单的例子中,我们会这样做,以便任何以后来的人都知道这个类依赖于该数据或功能。我们将 dueDate
和 toDo
参数添加到构造函数中,并将它们作为 private
字段。
Private _dueDate As Date
Private _toDo As String
Private _runMeFirst As IAddToDos
Public Sub New(ByVal dueDate As Date, ByVal toDo As String, ByRef runMeFirst As IAddToDos)
_dueDate = dueDate
_toDo = toDo
_runMeFirst = runMeFirst
End Sub
现在是 RunMe
函数。它当然会返回 dataObj
,所以我们添加它。此外,我们需要它检查任何之前的函数,并在运行当前函数之前运行它们。这是可组合性的一部分。我们可以这样处理:
Public Function RunMe(dataObj As AddToDosVals) As AddToDosVals Implements IAddToDos.RunMe
If Not IsNothing(_runMeFirst) Then
dataObj = _runMeFirst.RunMe(dataObj)
End If
Return dataObj
End Function
我们的函数现在会检查 ErrObj
,以确保之前的函数没有错误。在大多数情况下,如果之前发生过错误,为什么要运行这个函数?只需跳过代码,然后返回 dataObj
以便它能报告错误。所以我们添加以下检查:
If Not dataObj.ErrObj.HasError Then
End If
样板代码现在已经设置好了。该是时候写逻辑了。希望这个例子不会太简单。在这种情况下,我们真正需要做的就是验证值并将它们放入 dataObj
中,以便下游函数可以使用它们。在验证失败时,我们打开错误。完成的 RunMe
函数是这样的:
Public Function RunMe(dataObj As AddToDosVals) As AddToDosVals Implements IAddToDos.RunMe
If Not IsNothing(_runMeFirst) Then
dataObj = _runMeFirst.RunMe(dataObj)
End If
If Not dataObj.ErrObj.HasError Then
If Not _dueDate = Nothing AndAlso Not _toDo.Trim = String.Empty Then
dataObj.DueDate = _dueDate
dataObj.ToDoTask = _toDo
Else
dataObj.ErrObj.HasError = True
dataObj.ErrObj.Message = "Invalid input values"
End If
End If
Return dataObj
End Function
现在我们编写测试。(有些人会认为我们应该先写测试,让它失败,然后写逻辑。在实践中我两种方式都做过,所以无论你偏好哪种。)
这实际上是我们第一次实例化这种类型的类。这可能看起来有点奇怪,但请坚持下去,到最后,你就会明白为什么以这种方式编写类可以让我们组合这些独立的函数。
Imports ToDoSample
<testclass()> Public Class AddToDosTests
<testmethod()> Public Sub GetFromTextBoxesTests()
Dim expectedDate As Date = Date.Today()
Dim expectedToDo As String = "This is my test ToDo!"
Dim addToDo As IAddToDos = Nothing
addToDo = New GetDataFromTextBoxes(expectedDate, expectedToDo, addToDo)
Dim dataObj As New AddToDosVals() addToDo.RunMe(dataObj)
Dim actualDate As Date = dataObj.DueDate
Dim actualToDo As String = dataObj.ToDoTask
Assert.AreEqual(expectedDate, actualDate)
Assert.AreEqual(expectedToDo, actualToDo)
End Sub
End Class
太棒了!它奏效了!在实际生活中,你可以添加更多的测试来测试这个函数。例如,添加一些失败场景。但对于本文,我们将继续前进。我们对后面两个函数也做同样的事情。我们遵循相同的模式。设置类,添加样板代码,逻辑,然后测试。你可以在源代码中查看我的实现。
用所有函数装饰接口!
是时候组合所有这些类了。我将组合放在按钮的点击事件中。这样,六个月后,当我回来添加一个功能,或者当其他开发者来做这件事时,很容易理解发生了什么以及需要做什么来做出更改。
在组合时,有两个部分。组合,或者说把它们放在一起,然后实际运行代码。这是我们组合类的方式:
Dim addToDo As IAddToDos = Nothing
addToDo = New GetDataFromTextBoxes(Me.DateTimePicker1.Value, Me.TextBox2.Text, addToDo)
addToDo = New WriteToDataGridView(Me.dgvToDo, addToDo)
addToDo = New SortDataGridViewByDate(Me.dgvToDo, addToDo)
然后运行它,我们首先必须创建将存储任何状态的数据对象,然后运行它:
Dim dataObj As New AddToDosVals()
addToDo.RunMe(dataObj)
运行它,它奏效了!
添加更多功能!
所以我们有了应用程序,它运行了,但就像任何应用程序一样,它还没有完成。通常,一旦它到了用户手中,他们就会想要添加、调整、更改,尤其是在他们使用它之后。唯一确定的事情是会有变化。没问题!这就是我们使用装饰器模式的原因。我们可以在不接触其他代码或其他测试的情况下添加功能。例如,让我们在发生错误时通知 UI。我们遵循与上面相同的模式,添加类,添加样板代码,逻辑,然后测试。我将把这个类命名为“AlertOnError
”。
首先,我们在窗体上添加一个标签,将其 ForColor
设置为 Red
,并移除文本。我们将此标签命名为“lblError
”。为访问文本在窗体上添加一些属性。
我们添加新类“AlertOnError
”。它使用了与其他类相同的结构。我们需要更新 Form1
,所以我们将其作为参数添加。这是不带任何逻辑的类:
Public Class AlertOnError
Implements IAddToDos
Private _currForm As Form1
Private _runMeFirst As IAddToDos
Public Sub New(ByRef currForm As Form1, ByRef runMeFirst As IAddToDos)
_currForm = currForm
_runMeFirst = runMeFirst
End Sub
Public Function RunMe(dataObj As AddToDosVals) As AddToDosVals Implements IAddToDos.RunMe
If Not IsNothing(_runMeFirst) Then
dataObj = _runMeFirst.RunMe(dataObj)
End If
Return dataObj
End Function
End Class 'AlertOnError
我们添加简单的逻辑:
If dataObj.ErrObj.HasError Then
_currForm.ErrorMessage = "ERROR: " & dataObj.ErrObj.Message
Else
_currForm.ErrorMessage = ""
End If
然后是测试:
<testmethod()> Public Sub AlertOnErrorTest()
Dim frmTest As New Form1
Dim expected As String = "ERROR: Test Error Message"
Dim addToDo As IAddToDos = Nothing
addToDo = New AlertOnError(frmTest, addToDo)
Dim dataObj As New AddToDosVals()
dataObj.ErrObj.HasError = True
dataObj.ErrObj.Message = "Test Error Message"
addToDo.RunMe(dataObj)
Dim actual As String = frmTest.ErrorMessage
Assert.AreEqual(expected, actual)
End Sub
所有测试都通过了,所以我们将其添加到我们的装饰器按钮逻辑中:
Private Sub btnAdd_Click(sender As Object, e As EventArgs) Handles btnAdd.Click
Dim addToDo As IAddToDos = Nothing
addToDo = New GetDataFromTextBoxes(Me.DateTimePicker1.Value, Me.TextBox2.Text, addToDo)
addToDo = New WriteToDataGridView(Me.dgvToDo, addToDo)
addToDo = New SortDataGridViewByDate(Me.dgvToDo, addToDo)
addToDo = New AlertOnError(Me, addToDo)
Dim dataObj As New AddToDosVals()
addToDo.RunMe(dataObj)
End Sub
就这样。我们添加了一个功能,而无需重构任何现有代码或测试。这就像编写一个新项目一样。它也是自文档化的。
试试吧。告诉我你的想法。我发布了一个支持多线程和异步的版本初稿。它在这里。我会看看是否能发布关于 JavaScript 的版本。