在本章中,我们将看到接下来的三种行为设计模式。困难正在增加,因为现在我们将使用结构模式和创造性模式的组合来更好地解决某些行为模式的目标。
我们将从模板设计模式开始,该模式看起来与策略模式非常相似,但提供了更大的灵活性。Memento 设计模式在我们每天使用的 99%的应用程序中使用,以实现撤销功能和事务操作。最后,我们将编写一个反向波兰符号解释程序来执行简单的数学运算。
让我们从模板设计模式开始。
模板模式是广泛使用的模式之一,非常有用,尤其是在编写库和框架时。其思想是为用户提供某种在算法中执行代码的方法。
在本节中,我们将了解如何编写惯用的 Go 模板模式,并了解一些 Go 源代码在哪些地方得到了明智的使用。我们将编写一个由三个步骤组成的算法,其中第二步委托给用户,而第一步和第三步不委托给用户。算法的第一步和第三步表示模板。
使用策略模式时,我们将算法实现封装在不同的策略中,使用模板模式时,我们将尝试实现类似的功能,但只使用算法的一部分。
模板设计模式允许用户编写算法的一部分,而其余部分由抽象执行。当创建库以简化某些复杂任务时,或者当某些算法的可重用性仅受到其一部分的影响时,这是很常见的。
例如,假设我们有一个 HTTP 请求的长事务。我们必须执行以下步骤:
每当用户需要修改数据库中的某些内容时,重复用户代码中的步骤 1 到 5 是没有意义的。相反,步骤 1、2、3 和 5 将抽象为同一个算法,该算法接收第五步完成事务所需的接口。它也不需要是一个接口,它可以是一个回调。
模板设计模式是关于可重用性的,并将责任赋予用户。因此,这种模式的目标如下:
在第一个示例中,我们将编写一个由三个步骤组成的算法,每个步骤都返回一条消息。第一步和第三步由模板控制,第二步由用户执行。
模板模式必须做的一个简要描述是为三个步骤的算法定义一个模板,将第二个步骤的实现推迟到用户:
first()
的方法,返回字符串hello
。third()
的方法,返回字符串template
。Message() string
方法的MessageRetriever
接口定义的。ExecuteAlgorithm
的方法顺序执行,并返回每个步骤返回的字符串,每个步骤通过一个空格连接到一个字符串中。我们将只专注于测试公共方法。这是一种非常常见的方法。总而言之,若您的私有方法并没有从公共方法的某个级别调用,那个么它们就根本不会被调用。这里我们需要两个接口,一个用于模板实现者,一个用于算法的抽象步骤:
type MessageRetriever interface {
Message()string
}
type Template interface {
first() string
third() string
ExecuteAlgorithm(MessageRetriever) string
}
模板实现者将接受MessageRetriever
接口作为其执行算法的一部分执行。我们需要一个实现此接口的类型,称为Template
,我们将其称为TemplateImpl
:
type TemplateImpl struct{}
func (t *TemplateImpl) first() string {
return ""
}
func (t *TemplateImpl) third() string {
return ""
}
func (t *TemplateImpl) ExecuteAlgorithm(m MessageRetriever) string {
return ""
}
因此,我们的第一个测试检查第四和第五个验收标准。我们将创建实现返回字符串world
的MessageRetriever
接口并嵌入模板的TestStruct
类型,以便调用ExecuteAlgorithm
方法。它将作为模板和抽象:
type TestStruct struct {
Template
}
func (m *TestStruct) Message() string {
return "world"
}
首先,我们将定义TestStruct
类型。在这种情况下,延迟给我们的算法部分将返回world
文本。这是我们稍后将在测试中查找的字符串,检查类型为“此字符串上是否存在单词world
?”。
仔细看,TestStruct
嵌入了一个名为Template
的类型,它代表了我们算法的模板模式。
当我们实现Message()
方法时,我们隐式地实现了MessageRetriever
接口。所以现在我们可以使用TestStruct
类型作为指向MessageRetriever
接口的指针:
func TestTemplate_ExecuteAlgorithm(t *testing.T) {
t.Run("Using interfaces", func(t *testing.T){
s := &TestStruct{}
res := s.ExecuteAlgorithm(s)
expected := "world"
if !strings.Contains(res, expected) {
t.Errorf("Expected string '%s' wasn't found on returned string\n", expected)
}
})
}
在测试中,我们将使用刚刚创建的类型。调用ExecuteAlgorithm
方法时,需要传递MessageRetriever
接口。由于TestStruct
类型也实现了MessageRetriever
接口,我们可以将其作为参数传递,但这当然不是强制性的。
第五个验收标准中定义的ExecuteAlgorithm
方法的结果必须返回一个字符串,该字符串包含first()
方法的返回值、TestStruct
方法的返回值(world
字符串)和third()
方法的返回值,并用空格分隔。我们的执行是在第二位;这就是为什么我们检查字符串world
上是否有空格前缀和后缀。
因此,如果调用ExecuteAlgorithm
方法时返回的字符串不包含字符串world
,则测试失败。
这足以使项目编译并运行应失败的测试:
go test -v .
=== RUN TestTemplate_ExecuteAlgorithm
=== RUN TestTemplate_ExecuteAlgorithm/Using_interfaces
--- FAIL: TestTemplate_ExecuteAlgorithm (0.00s)
--- FAIL: TestTemplate_ExecuteAlgorithm/Using_interfaces (0.00s)
template_test.go:47: Expected string ' world ' was not found on returned string
FAIL
exit status 1
FAIL
现在是实现此模式的时候了。
根据验收标准的定义,我们必须在first()
方法中返回字符串hello
,在third()
方法中返回字符串template
。这很容易实现:
type Template struct{}
func (t *Template) first() string {
return "hello"
}
func (t *Template) third() string {
return "template"
}
在这个实现中,我们应该覆盖第二个和第三个验收标准,部分覆盖第一个标准(算法中的每个步骤都必须返回一个字符串)。
为了涵盖第五个接受标准,我们定义了一个ExecuteAlgorithm
方法,该方法接受MessageRetriever
接口作为参数,并返回完整的算法:将first()
、Message() string
和third()
方法返回的字符串连接起来,形成一个字符串:
func (t *Template) ExecuteAlgorithm(m MessageRetriever) string {
return strings.Join([]string{t.first(), m.Message(), t.third()}, " ")
}
strings.Join
函数具有以下签名:
func Join([]string,string) string
它接受一个字符串数组并连接它们,将第二个参数放置在数组中的每个项之间。在本例中,我们动态创建一个字符串数组,将其作为第一个参数传递。然后我们传递一个空格作为第二个参数。
使用此实现,测试必须已经通过:
go test -v .
=== RUN TestTemplate_ExecuteAlgorithm
=== RUN TestTemplate_ExecuteAlgorithm/Using_interfaces
--- PASS: TestTemplate_ExecuteAlgorithm (0.00s)
--- PASS: TestTemplate_ExecuteAlgorithm/Using_interfaces (0.00s)
PASS
ok
测试通过了。测试已检查返回结果中是否存在字符串world
,即hello world template
消息。hello
文本是first()
方法返回的字符串,world
字符串是我们的MessageRetriever
实现返回的字符串,template
是third()
方法返回的字符串。空格由 Go 的strings.Join
函数插入。但是任何使用TemplateImpl.ExecuteAlgorithm
类型的方法都会在结果中返回“hello[something]template”。
这不是实现模板设计模式的唯一方法。我们还可以使用匿名函数来实现ExecuteAlgorithm
方法。
让我们用之前在测试之后使用的相同方法编写一个测试(用粗体标记):
func TestTemplate_ExecuteAlgorithm(t *testing.T) {
t.Run("Using interfaces", func(t *testing.T){
s := &TestStruct{}
res := s.ExecuteAlgorithm(s)
expectedOrError(res, " world ", t)
})
t.Run("Using anonymous functions", func(t *testing.T)
{
m := new(AnonymousTemplate)
res := m.ExecuteAlgorithm(func() string {
return "world"
})
expectedOrError(res, " world ", t)
})
}
func expectedOrError(res string, expected string, t *testing.T){
if !strings.Contains(res, expected) {
t.Errorf("Expected string '%s' was not found on returned string\n", expected)
}
}
我们的新测试名为,使用匿名函数。我们还将测试中的检查提取到一个外部函数,以便在测试中重用它。我们之所以调用此函数expectedOrError
,是因为如果未收到预期值,它将失败并出现错误。
在我们的测试中,我们将创建一个名为AnonymousTemplate
的类型,它将替换以前的Template
类型。这个新类型的ExecuteAlgorithm
方法接受func()
方法string
类型,我们可以在测试中直接实现该方法返回字符串world
。
AnonymousTemplate
型应具有以下结构:
type AnonymousTemplate struct{}
func (a *AnonymousTemplate) first() string {
return ""
}
func (a *AnonymousTemplate) third() string {
return ""
}
func (a *AnonymousTemplate) ExecuteAlgorithm(f func() string) string {
return ""
}
与Template
类型的唯一区别在于ExecuteAlgorithm
方法接受返回字符串的函数,而不是MessageRetriever
接口。让我们运行新的测试:
go test -v .
=== RUN TestTemplate_ExecuteAlgorithm
=== RUN TestTemplate_ExecuteAlgorithm/Using_interfaces
=== RUN TestTemplate_ExecuteAlgorithm/Using_anonymous_functions
--- FAIL: TestTemplate_ExecuteAlgorithm (0.00s)
--- PASS: TestTemplate_ExecuteAlgorithm/Using_interfaces (0.00s)
--- FAIL: TestTemplate_ExecuteAlgorithm/Using_anonymous_functions (0.00s)
template_test.go:47: Expected string ' world ' was not found on returned string
FAIL
exit status 1
FAIL
正如您在测试执行的输出中所看到的,使用匿名函数测试在上抛出错误,这是我们所期望的。现在,我们将编写以下实现:
type AnonymousTemplate struct{}
func (a *AnonymousTemplate) first() string {
return "hello"
}
func (a *AnonymousTemplate) third() string {
return "template"
}
func (a *AnonymousTemplate) ExecuteAlgorithm(f func() string) string {
return strings.Join([]string{a.first(), f(), a.third()}, " ")
}
该实现与Template
类型中的实现非常相似。但是,现在我们已经传递了一个名为f
的函数,我们将它用作Join
函数上使用的字符串数组中的第二项。由于f
只是一个返回字符串的函数,我们只需要在适当的位置(数组中的第二个位置)执行它。
再次运行测试:
go test -v .
=== RUN TestTemplate_ExecuteAlgorithm
=== RUN TestTemplate_ExecuteAlgorithm/Using_interfaces
=== RUN TestTemplate_ExecuteAlgorithm/Using_anonymous_functions
--- PASS: TestTemplate_ExecuteAlgorithm (0.00s)
--- PASS: TestTemplate_ExecuteAlgorithm/Using_interfaces (0.00s)
--- PASS: TestTemplate_ExecuteAlgorithm/Using_anonymous_functions (0.00s)
PASS
ok
令人惊叹的现在我们知道了两种实现模板设计模式的方法。
前一种方法的问题是,现在我们有两个模板需要维护,我们可以结束重复代码。在无法更改正在使用的接口的情况下,我们可以做什么?我们的接口是MessageRetriever
,但我们现在想使用匿名函数。
你还记得适配器的设计模式吗?我们只需要创建一个Adapter
类型,接受func() string
类型,返回MessageRetriever
接口的实现。我们将此类型称为TemplateAdapter
:
type TemplateAdapter struct {
myFunc func() string
}
func (a *TemplateAdapter) Message() string {
return ""
}
func MessageRetrieverAdapter(f func() string) MessageRetriever {
return nil
}
如您所见,TemplateAdapter
类型有一个名为myFunc
的字段,属于func() string
类型。我们还将适配器定义为 private,因为如果没有在myFunc
字段中定义的函数,就不应该使用它。我们创建了一个名为MessageRetrieverAdapter
的公共函数来实现这一点。我们的测试应该大致如下所示:
t.Run("Using anonymous functions adapted to an interface", func(t *testing.T){
messageRetriever := MessageRetrieverAdapter(func() string {
return "world"
})
if messageRetriever == nil {
t.Fatal("Can not continue with a nil MessageRetriever")
}
template := Template{}
res := template.ExecuteAlgorithm(messageRetriever)
expectedOrError(res, " world ", t)
})
请看我们称之为MessageRetrieverAdapter
方法的语句。我们传递了一个匿名函数作为参数,该参数定义为func()
字符串。然后,我们重用前面定义的第一个测试中的Template
类型,以通过messageRetriever
变量。最后,我们用expectedOrError
方法再次检查。看看MessageRetrieverAdapter
方法,它将返回一个具有 nil 值的函数。如果严格遵循测试驱动的开发规则,我们必须首先进行测试,并且在实现完成之前不能通过测试。这就是我们在MessageRetrieverAdapter
函数中返回 nil 的原因。
那么,让我们运行测试:
go test -v .
=== RUN TestTemplate_ExecuteAlgorithm
=== RUN TestTemplate_ExecuteAlgorithm/Using_interfaces
=== RUN TestTemplate_ExecuteAlgorithm/Using_anonymous_functions
=== RUN TestTemplate_ExecuteAlgorithm/Using_anonymous_functions_adapted_to_an_interface
--- FAIL: TestTemplate_ExecuteAlgorithm (0.00s)
--- PASS: TestTemplate_ExecuteAlgorithm/Using_interfaces (0.00s)
--- PASS: TestTemplate_ExecuteAlgorithm/Using_anonymous_functions (0.00s)
--- FAIL: TestTemplate_ExecuteAlgorithm/Using_anonymous_functions_adapted_to_an_interface (0.00s)
template_test.go:39: Can not continue with a nil MessageRetriever
FAIL
exit status 1
FAIL
代码的行 39上的测试失败,测试无法继续(同样,根据您编写代码的方式,表示错误的行可能在其他地方)。我们停止测试执行,因为我们在调用ExecuteAlgorithm
方法时需要一个有效的MessageRetriever
接口。
对于模板模式适配器的实现,我们将从MessageRetrieverAdapter
方法开始:
func MessageRetrieverAdapter(f func() string) MessageRetriever {
return &adapter{myFunc: f}
}
这很容易,对吧?您可能想知道,如果我们为f
参数传递nil
值,会发生什么。那么我们将通过调用myFunc
函数来解决这个问题。
adapter
类型完成此实现:
type adapter struct {
myFunc func() string
}
func (a *adapter) Message() string {
if a.myFunc != nil {
return a.myFunc()
}
return ""
}
调用Message()
函数时,我们会在调用之前检查myFunc
函数中是否存储了某些内容。如果未存储任何内容,则返回一个空字符串。
现在,我们使用适配器模式完成了Template
类型的第三个实现:
go test -v .
=== RUN TestTemplate_ExecuteAlgorithm
=== RUN TestTemplate_ExecuteAlgorithm/Using_interfaces
=== RUN TestTemplate_ExecuteAlgorithm/Using_anonymous_functions
=== RUN TestTemplate_ExecuteAlgorithm/Using_anonymous_functions_adapted_to_an_interface
--- PASS: TestTemplate_ExecuteAlgorithm (0.00s)
--- PASS: TestTemplate_ExecuteAlgorithm/Using_interfaces (0.00s)
--- PASS: TestTemplate_ExecuteAlgorithm/Using_anonymous_functions (0.00s)
--- PASS: TestTemplate_ExecuteAlgorithm/Using_anonymous_functions_adapted_to_an_interface (0.00s)
PASS
ok
Go 源代码中的Sort
包可以被视为排序算法的模板实现。正如包本身所定义的,Sort
包提供用于排序切片和用户定义集合的原语。
在这里,我们还可以找到一个很好的例子,说明为什么 Go 作者不担心实现泛型。对列表进行排序可能是其他语言中通用用法的最好例子。Go 处理此问题的方式也非常优雅,它通过一个界面处理此问题:
type Interface interface {
Len() int
Less(i, j int) bool
Swap(i, j int)
}
这是需要使用sort
包进行排序的列表的界面。用 Go 的作者的话说:
“类型通常是满足排序的集合。接口可以通过此包中的例程进行排序。这些方法要求集合的元素由整数索引枚举。”
换句话说,编写一个实现这个Interface
的类型,这样Sort
包就可以用来对任何片进行排序。排序算法就是模板,我们必须定义如何在切片中检索值。
如果我们查看sort
包,我们还可以找到如何使用排序模板的示例,但我们将创建自己的示例:
package main
import (
"sort"
"fmt"
)
type MyList []int
func (m MyList) Len() int {
return len(m)
}
func (m MyList) Swap(i, j int) {
m[i], m[j] = m[j], m[i]
}
func (m MyList) Less(i, j int) bool {
return m[i] < m[j]
}
首先,我们做了一个非常简单的类型,它存储一个int
列表。这可以是任何类型的列表,通常是某种结构的列表。然后我们通过定义Len
、Swap
和Less
方法实现了sort.Interface
接口。
最后,main
函数创建一个无序的MyList
类型的数字列表:
func main() {
var myList MyList = []int{6,4,2,8,1}
fmt.Println(myList)
sort.Sort(myList)
fmt.Println(myList)
}
我们打印我们创建的列表(无序),然后对其进行排序(sort.Sort
方法实际上修改了我们的变量,而不是返回一个新列表,所以要小心!)。最后,我们再次打印结果列表。此main
方法的控制台输出如下:
go run sort_example.go
[6 4 2 8 1]
[1 2 4 6 8]
sort.Sort
函数以透明的方式对列表进行排序。它编写了大量代码,并将Len
、Swap
和Less
方法委托给接口,就像我们在模板委托给MessageRetriever
接口时所做的那样。
我们想把重点放在这个模式上,因为它在开发库和框架时非常重要,并且允许我们库的用户有很大的灵活性和控制。
我们还再次看到,混合模式为用户提供灵活性是非常常见的,不仅在行为方式上,而且在结构上。在处理并发应用程序时,这将非常方便,因为我们需要限制对部分代码的访问以避免竞争。
现在让我们看一个具有奇特名称的图案。如果我们查字典查看memento的含义,我们会发现以下描述:
“作为一个人或事件的提醒而保存的物品。”
这里的关键词是提醒,因为我们将记住这种设计模式下的动作。
memento 的含义与它在设计模式中提供的功能非常相似。基本上,我们将有一个带有一些状态的类型,我们希望能够保存其状态的里程碑。保存了有限数量的状态后,如果需要执行各种任务、撤消操作、历史记录等,我们可以恢复这些状态。
备忘录设计模式通常有三名玩家(通常称为演员:
Memento 是随着时间推移的一系列操作,比如撤销一个或两个操作,或者为某个应用程序提供某种事务性。
Memento 为许多任务提供了基础,但其主要目标可定义如下:
我们将开发一个简单的示例,使用字符串作为要保存的状态。通过这种方式,我们将重点讨论常见的 Memento 模式实现,然后使用一个新的示例使其变得更复杂。
存储在State
实例字段中的字符串将被修改,我们将能够撤消在此状态下执行的操作。
我们一直在谈论国家;总之,Memento 模式是关于存储和检索状态的。我们的验收标准必须与国家有关:
有了这两个简单的需求,我们就可以开始为这个例子编写一些测试了。
如前所述,Memento 设计模式通常由三个参与者组成:国家、Memento 和发起人。因此,我们需要三种类型来代表这些参与者:
type State struct {
Description string
}
State
类型是我们将在本例中使用的核心业务对象。它是我们想要跟踪的任何类型的对象:
type memento struct {
state State
}
memento
类型有一个名为state
的字段,表示State
类型的单个值。我们的states
在储存到care taker
型之前,将在该型中进行集装箱运输。你可能想知道为什么我们不直接存储State
实例。基本上,因为它将originator
和careTaker
耦合到业务对象,我们希望耦合尽可能少。正如我们将在第二个示例中看到的那样,它的灵活性也会降低:
type originator struct {
state State
}
func (o *originator) NewMemento() memento {
return memento{}
}
func (o *originator) ExtractAndStoreState(m memento) {
//Does nothing
}
originator
类型还存储状态。originator
结构的对象将从 Memento 获取状态,并使用存储状态创建新的 Memento。
发起者对象和备忘录模式之间有什么区别?为什么我们不直接使用发起者模式的对象呢?那么,如果 Memento 包含一个特定的状态,originator
类型包含当前加载的状态。此外,保存某个对象的状态可以简单到获取某个值,也可以复杂到维护某个分布式应用程序的状态。
发起人将有两种公开的方法--NewMemento()
方法和ExtractAndStoreState(m memento)
方法。NewMemento
方法将返回一个使用originator
当前State
值构建的新备忘录。ExtractAndStoreState
方法将获取备忘录的状态并将其存储在Originator
的状态字段中:
type careTaker struct {
mementoList []memento
}
func (c *careTaker) Add(m memento) {
//Does nothing
}
func (c *careTaker) Memento(i int) (memento, error) {
return memento{}, fmt.Errorf("Not implemented yet")
}
careTaker
类型存储了我们需要保存的所有状态的备忘录列表。它还存储了一个Add
方法,用于在列表中插入一个新的 Memento,以及一个 Memento 检索器,该检索器在 Memento 列表中获取索引。
所以让我们从careTaker
类型的Add
方法开始。Add
方法必须将memento
对象添加到careTaker
对象的备忘录列表中:
func TestCareTaker_Add(t *testing.T) {
originator := originator{}
originator.state = State{Description:"Idle"}
careTaker := careTaker{}
mem := originator.NewMemento()
if mem.state.Description != "Idle" {
t.Error("Expected state was not found")
}
在我们的测试开始时,我们为 memento 创建了两个基本角色--originator
和careTaker
。我们使用描述Idle
对发起人设置了第一个状态。
然后,我们创建了第一个 Memento,称为NewMemento
方法。这应该将当前发起人的状态包装为memento
类型。我们的第一个检查非常简单——返回备忘录的状态描述必须与我们传递给发起者的状态描述类似,即Idle
描述。
检查我们的 MementoAdd
方法是否正确的最后一步是查看添加一项后 Memento 列表是否增长:
currentLen := len(careTaker.mementoList)
careTaker.Add(mem)
if len(careTaker.mementoList) != currentLen+1 {
t.Error("No new elements were added on the list")
}
我们还必须测试Memento(int) memento
方法。这应该从careTaker
列表中获取memento
值。它采用您希望从列表中检索的索引,因此,与列表一样,我们必须检查它在负数和超出索引值时的行为是否正确:
func TestCareTaker_Memento(t *testing.T) {
originator := originator{}
careTaker := careTaker{}
originator.state = State{"Idle"}
careTaker.Add(originator.NewMemento())
我们必须像之前的测试一样开始创建一个originator
和careTaker
对象,并将第一个备忘录添加到caretaker
:
mem, err := careTaker.Memento(0)
if err != nil {
t.Fatal(err)
}
if mem.state.Description != "Idle" {
t.Error("Unexpected state")
}
一旦我们在careTaker
对象上有了第一个对象,我们就可以使用careTaker.Memento(0)
来请求它。Memento(int)
方法上的索引0
检索切片上的第一项(记住切片以0
开头)。不应返回任何错误,因为我们已经向caretaker
对象添加了一个值。
然后,在检索第一个备忘录后,我们检查了描述是否与我们在测试开始时通过的描述匹配:
mem, err = careTaker.Memento(-1)
if err == nil {
t.Fatal("An error is expected when asking for a negative number but no error was found")
}
}
此测试的最后一步涉及使用负数检索某些值。在这种情况下,必须返回一个错误,表明不能使用负数。传递负数时也可以返回第一个索引,但这里我们将返回一个错误。
最后一个要检查的功能是ExtractAndStoreState
方法。此函数必须获取一个 Memento 并提取其所有状态信息,以便在Originator
对象中进行设置:
func TestOriginator_ExtractAndStoreState(t *testing.T) {
originator := originator{state:State{"Idle"}}
idleMemento := originator.NewMemento()
originator.ExtractAndStoreState(idleMemento)
if originator.state.Description != "Idle" {
t.Error("Unexpected state found")
}
}
这个测试很简单。我们创建了一个带有Idle
状态的默认originator
变量。然后,我们检索一个新的 Memento 对象以供以后使用。我们将originator
变量的状态更改为Working
状态,以确保写入新状态。
最后,我们必须使用idleMemento
变量调用ExtractAndStoreState
方法。这应该将发起者的状态恢复为idleMemento
状态的值,这是我们在上一个if
语句中检查的。
现在是运行测试的时候了:
go test -v .
=== RUN TestCareTaker_Add
--- FAIL: TestCareTaker_Add (0.00s)
memento_test.go:13: Expected state was not found
memento_test.go:20: No new elements were added on the list
=== RUN TestCareTaker_Memento
--- FAIL: TestCareTaker_Memento (0.00s)
memento_test.go:33: Not implemented yet
=== RUN TestOriginator_ExtractAndStoreState
--- FAIL: TestOriginator_ExtractAndStoreState (0.00s)
memento_test.go:54: Unexpected state found
FAIL
exit status 1
FAIL
因为这三个测试都失败了,我们可以继续执行。
如果不太疯狂,Memento 模式的实现通常非常简单。这三个参与者(memento
、originator
和care taker
在模式中有一个非常明确的角色,它们的实现非常简单:
type originator struct {
state State
}
func (o *originator) NewMemento() memento {
return memento{state: o.state}
}
func (o *originator) ExtractAndStoreState(m memento) {
o.state = m.state
}
Originator
对象在调用NewMemento
方法时需要返回 Memento 类型的新值。它还需要根据ExtractAndStoreState
方法的需要在结构的状态字段中存储memento
对象的值:
type careTaker struct {
mementoList []memento
}
func (c *careTaker) Push(m memento) {
c.mementoList = append(c.mementoList, m)
}
func (c *careTaker) Memento(i int) (memento, error) {
if len(c.mementoList) < i || i < 0 {
return memento{}, fmt.Errorf("Index not found\n")
}
return c.mementoList[i], nil
}
careTaker
类型也很简单。当我们调用Add
方法时,我们通过使用参数中传递的值调用append
方法来覆盖mementoList
字段。这将创建包含新值的新列表。
调用Memento
方法时,我们必须事先做一些检查。在这种情况下,我们检查索引是否不在切片范围之外,并且索引在if
语句中不是负数,在这种情况下,我们返回一个错误。如果一切正常,它只返回指定的memento
对象,没有错误。
关于方法和函数命名约定的说明。你可能会发现一些人喜欢给方法起更具描述性的名字,比如Memento
。一个例子是使用一个名称,例如MementoOrError
方法,清楚地表明在调用此函数甚至GetMementoOrError
方法时返回两个对象。这可能是一种非常明确的命名方法,也不一定不好,但在 Go 的源代码中不会很常见。
Time to check the test results:
go test -v .
=== RUN TestCareTaker_Add
--- PASS: TestCareTaker_Add (0.00s)
=== RUN TestCareTaker_Memento
--- PASS: TestCareTaker_Memento (0.00s)
=== RUN TestOriginator_ExtractAndStoreState
--- PASS: TestOriginator_ExtractAndStoreState (0.00s)
PASS
ok
这足以达到 100%的覆盖率。虽然这远不是一个完美的指标,但至少我们知道,我们正在触及源代码的每一个角落,并且我们没有在测试中作弊来实现它。
前面的示例非常简单,足以理解 Memento 模式的功能。但是,它通常与命令模式和简单的外观模式结合使用。
其思想是使用命令模式来封装一组不同类型的状态(实现Command
接口的状态),并提供一个小的外观来自动插入caretaker
对象。
我们将开发一个假设的音频混音器的小示例。我们将使用相同的 Memento 模式保存两种状态:Volume
和Mute
。Volume
状态将是字节类型,Mute
状态将是布尔类型。我们将使用两种完全不同的类型来展示这种方法的灵活性(及其缺点)。
作为补充说明,我们还可以在接口上为每个Command
接口提供各自的序列化方法。这样,我们就可以让管理员在不知道存储的内容的情况下,将状态存储在某种存储中。
我们的Command
接口将有一个方法返回其实现者的值。非常简单,我们想要撤销的音频混音器中的每个命令都必须实现以下接口:
type Command interface {
GetValue() interface{}
}
这个界面中有一些有趣的东西。GetValue
方法返回一个值的接口。这也意味着此方法的返回类型是。。。好非类型化?不是真的,但它返回一个可以是任何类型表示的接口,如果我们想使用它的特定类型,我们需要稍后对其进行类型转换。现在我们需要定义Volume
和Mute
类型并实现Command
接口:
type Volume byte
func (v Volume) GetValue() interface{} {
return v
}
type Mute bool
func (m Mute) GetValue() interface{} {
return m
}
它们都是非常简单的实现。但是,Mute
类型将在GetValue()
方法上返回bool
类型,Volume
将返回byte
类型。
如前一个示例中所示,我们需要一个包含Command
的Memento
类型。换句话说,它将存储指向Mute
或Volume
类型的指针:
type Memento struct {
memento Command
}
originator
类型的工作方式与上一示例相同,但使用Command
关键字而不是state
关键字:
type originator struct {
Command Command
}
func (o *originator) NewMemento() Memento {
return Memento{memento: o.Command}
}
func (o *originator) ExtractAndStoreCommand(m Memento) {
o.Command = m.memento
}
而caretaker
对象几乎相同,但这次我们将使用堆栈而不是简单的列表,并且我们将存储命令而不是状态:
type careTaker struct {
mementoList []Memento
}
func (c *careTaker) Add(m Memento) {
c.mementoList = append(c.mementoList, m)
}
func (c *careTaker) Pop() Memento {
if len(c.mementoStack) > 0 {
tempMemento := c.mementoStack[len(c.mementoStack)-1]
c.mementoStack = c.mementoStack[0:len(c.mementoStack)-1]
return tempMemento
}
return Memento{}
}
然而,我们的Memento
列表被Pop
方法取代。它还返回一个memento
对象,但它将返回它们作为堆栈(最后进入,首先退出)。因此,我们获取堆栈上的最后一个元素并将其存储在tempMemento
变量中。然后我们用一个新版本替换堆栈,该版本不包含下一行的最后一个元素。最后,我们返回tempMemento
变量。
到目前为止,所有内容都与上一个示例中的内容几乎相同。我们还讨论了使用 Facade 模式自动化一些任务,所以让我们来做吧。这将被称为MementoFacade
类型,将有SaveSettings
和RestoreSettings
方法。SaveSettings
方法获取Command
,将其存储在内部发起人中,并将其保存在内部careTaker
字段中。RestoreSettings
方法使反向流恢复careTaker
的索引,并返回Memento
对象内的Command
:
type MementoFacade struct {
originator originator
careTaker careTaker
}
func (m *MementoFacade) SaveSettings(s Command) {
m.originator.Command = s
m.careTaker.Add(m.originator.NewMemento())
}
func (m *MementoFacade) RestoreSettings(i int) Command {
m.originator.ExtractAndStoreCommand(m.careTaker.Memento(i))
return m.originator.Command
}
我们的 Facade 模式将保存发起人和管理员的内容,并将提供这两种易于使用的方法来保存和恢复设置。
那么,我们如何使用这个呢?
func main(){
m := MementoFacade{}
m.SaveSettings(Volume(4))
m.SaveSettings(Mute(false))
首先,我们得到一个带有 Facade 模式的变量。零值初始化将为我们提供零值的originator
和caretaker
对象。它们没有任何意外字段,因此所有内容都将正确初始化(例如,如果其中任何一个有指针,它将被初始化为nil
,如第 1 章中的零初始化部分所述,准备就绪……稳定……开始!。
我们用Volume(4)
创建Volume
值,是的,我们使用了括号。Volume
类型没有任何类似于结构的内部字段,因此我们不能使用大括号来设置其值。设置它的方法是使用括号(或者创建指向类型Volume
的指针,然后设置指向的空格的值)。我们还使用 Facade 模式保存类型为Mute
的值。
我们不知道这里返回了什么Command
类型,所以我们需要做一个类型断言。我们将制作一个小函数来帮助我们检查类型并打印适当的值:
func assertAndPrint(c Command){
switch cast := c.(type) {
case Volume:
fmt.Printf("Volume:\t%d\n", cast)
case Mute:
fmt.Printf("Mute:\t%t\n", cast)
}
}
assertAndPrint
方法采用Command
类型,并将其强制转换为两种可能的类型—Volume
或Mute
。在每种情况下,它都会向控制台打印一条带有个性化消息的消息。现在我们可以继续并完成main
函数,它将如下所示:
func main() {
m := MementoFacade{}
m.SaveSettings(Volume(4))
m.SaveSettings(Mute(false))
assertAndPrint(m.RestoreSettings(0))
assertAndPrint(m.RestoreSettings(1))
}
粗体突出显示的部分显示了main
功能中的新变化。我们从careTaker
对象中获取索引 0,并将其传递给新函数,与索引1
相同。运行这个小程序,我们应该在控制台上得到Volume
和Mute
的值:
$ go run memento_command.go
Mute: false
Volume: 4
伟大的在这个小示例中,我们结合了三种不同的设计模式,以保持使用各种模式的舒适性。请记住,我们也可以将Volume
和Mute
状态的创建抽象为一个工厂模式,因此这不是停止的地方。
通过 Memento 模式,我们学习了一种创建可撤销操作的强大方法,这种方法在编写 UI 应用程序时非常有用,在开发事务性操作时也非常有用。在任何情况下,情况都是一样的:你需要一个Memento
、一个Originator
和一个caretaker
演员。
事务操作是一组必须全部完成或失败的原子操作。换句话说,如果您有一个由五个操作组成的事务,而其中只有一个操作失败,那么该事务将无法完成,而其他四个操作所做的所有修改都必须撤消。
现在我们要深入研究一个非常复杂的模式。事实上,解释器模式被广泛用于解决业务案例,在这些案例中,使用一种语言来执行常见操作非常有用。让我们看看语言是什么意思。
我们可以谈论的最著名的解释器可能是 SQL。它被定义为一种特殊用途的编程语言,用于管理关系数据库中的数据。SQL 非常复杂和庞大,但总而言之,它是一组字和运算符,允许我们执行插入、选择或删除等操作。
另一个典型的例子是记谱法。它本身就是一种语言,口译员是一位音乐家,他知道音符和他们演奏的乐器上的音符之间的联系。
在计算机科学中,出于各种原因设计一种小型语言可能很有用:重复性任务、非开发人员的高级语言,或接口定义语言(IDL),如协议缓冲区或Apache Thrift。
设计一种新的语言,无论大小,都可能是一项耗时的任务,因此在投入时间和资源编写语言的解释程序之前,明确目标非常重要:
SQL 允许以非常易于使用的语法使用关系数据库(也可能变得非常复杂),但其思想是不需要编写自己的函数来进行插入和搜索。
解释器的一个非常典型的例子是创建一个反向波兰符号计算器。对于那些不知道波兰语符号是什么的人来说,这是一种数学符号,可以先写运算(求和),然后写值(34),因此+34相当于更常见的3+4,其结果将是7。因此,对于反向波兰符号,您首先输入值,然后输入操作,因此34+也将是7。
对于我们的计算器,我们应该通过的验收标准考虑如下:
sum
表示和,mul
表示乘法,sub
表示减法,div
表示除法。因此,3 4 sum 2 sub
符号与(3+4)-2符号相同,结果为5。
在这种情况下,我们只有一个名为Calculate
的公共方法,它接受一个值定义为字符串的操作,并返回一个值或一个错误:
func Calculate(o string) (int, error) {
return 0, fmt.Errorf("Not implemented yet")
}
因此,我们将向Calculate
方法发送一个类似于"3 4 +"
的字符串,它应该返回7,nil。还有两个测试将检查正确的实现:
func TestCalculate(t *testing.T) {
tempOperation = "3 4 sum 2 sub"
res, err = Calculate(tempOperation)
if err != nil {
t.Error(err)
}
if res != 5 {
t.Errorf("Expected result not found: %d != %d\n", 5, res)
}
首先,我们将以我们使用的操作为例。3 4 sum 2 sub
符号是我们语言的一部分,我们在Calculate
函数中使用它。如果返回错误,则测试失败。最后,结果必须等于5
,我们在最后几行检查它。下一个测试将检查其余操作员的稍微复杂的操作:
tempOperation := "5 3 sub 8 mul 4 sum 5 div"
res, err := Calculate(tempOperation)
if err != nil {
t.Error(err)
}
if res != 4 {
t.Errorf("Expected result not found: %d != %d\n", 4, res)
}
}
在这里,我们用更长的操作重复前面的过程,((5-3)8)+4)/5表示法,它等于4*。从左到右,如下所示:
(((5 - 3) * 8) + 4) / 5
((2 * 8) + 4) / 5
(16 + 4) / 5
20 / 5
4
当然,考试一定会失败!
$ go test -v .
interpreter_test.go:9: Not implemented yet
interpreter_test.go:13: Expected result not found: 4 != 0
interpreter_test.go:19: Not implemented yet
interpreter_test.go:23: Expected result not found: 5 != 0
exit status 1
FAIL
这次的实现将比测试更长。首先,我们将在常量中定义可能的运算符:
const (
SUM = "sum"
SUB = "sub"
MUL = "mul"
DIV = "div"
)
解释器模式通常使用抽象语法树实现,这通常是使用堆栈实现的。我们在本书之前已经创建了堆栈,因此读者应该已经熟悉了:
type polishNotationStack []int
func (p *polishNotationStack) Push(s int) {
*p = append(*p, s)
}
func (p *polishNotationStack) Pop() int {
length := len(*p)
if length > 0 {
temp := (*p)[length-1]
*p = (*p)[:length-1]
return temp
}
return 0
}
我们有两种方法——将元素添加到堆栈顶部的Push
方法和删除元素并返回它们的Pop
方法。如果你认为这一行*p = (*p)[:length-1]
有点晦涩,我们会解释它。
存储在p
方向上的值将被p (*p)
方向上的实际值覆盖,但只取数组(:length-1)
从开始到倒数第二个元素的元素。
因此,现在我们将逐步使用Calculate
函数,根据需要创建更多函数:
func Calculate(o string) (int, error) {
stack := polishNotationStack{}
operators := strings.Split(o, " ")
我们需要做的前两件事是创建堆栈并从传入操作中获取所有不同的符号(在本例中,我们不检查它是否为空)。我们将传入的字符串操作按空间分割,以获得一个很好的符号片段(值和运算符)。
接下来,我们将使用 range 迭代每个符号,但我们需要一个函数来知道传入符号是值还是运算符:
func isOperator(o string) bool {
if o == SUM || o == SUB || o == MUL || o == DIV {
return true
}
return false
}
如果传入符号是常量中定义的任何符号,则传入符号是一个运算符:
func Calculate(o string) (int, error) {
stack := polishNotationStack{}
operators := strings.Split(o, " ")
for _, operatorString := range operators {
if isOperator(operatorString) {
right := stack.Pop()
left := stack.Pop()
}
else
{
//Is a value
}
}
如果它是一个运算符,我们认为我们已经通过了两个值,所以我们要做的是从栈中获取这两个值。第一个值是最右边的,第二个值是最左边的(记住,在减法和除法中,操作数的顺序很重要)。然后,我们需要一些函数来获取要执行的操作:
func getOperationFunc(o string) func(a, b int) int {
switch o {
case SUM:
return func(a, b int) int {
return a + b
}
case SUB:
return func(a, b int) int {
return a - b
}
case MUL:
return func(a, b int) int {
return a * b
}
case DIV:
return func(a, b int) int {
return a / b
}
}
return nil
}
getOperationFunc
函数返回一个返回整数的双参数函数。我们检查传入运算符并返回执行指定操作的匿名函数。那么现在我们的for range
是这样继续的:
func Calculate(o string) (int, error) {
stack := polishNotationStack{}
operators := strings.Split(o, " ")
for _, operatorString := range operators {
if isOperator(operatorString) {
right := stack.Pop()
left := stack.Pop()
mathFunc := getOperationFunc(operatorString)
res := mathFunc(left, right)
stack.Push(res)
} else {
//Is a value
}
}
函数返回mathFunc
变量。我们立即使用它对从堆栈中获取的左右值执行操作,并将其结果存储在一个名为res
的新变量中。最后,我们需要将这个新值推送到堆栈中,以便以后继续使用它进行操作。
现在,以下是传入符号为值时的实现:
func Calculate(o string) (int, error) {
stack := polishNotationStack{}
operators := strings.Split(o, " ")
for _, operatorString := range operators {
if isOperator(operatorString) {
right := stack.Pop()
left := stack.Pop()
mathFunc := getOperationFunc(operatorString)
res := mathFunc(left, right)
stack.Push(res)
} else {
val, err := strconv.Atoi(operatorString)
if err != nil {
return 0, err
}
stack.Push(val)
}
}
每当我们得到一个符号时,我们需要做的就是把它推到堆栈中。我们必须将字符串符号解析为可用的int
类型。这通常是通过使用strconv
包的Atoi
功能来完成的。Atoi
函数接受一个字符串并从中返回一个整数或一个错误。如果一切顺利,该值将被推入堆栈。
在range
语句的末尾,只需要存储一个值,所以我们只需要返回它,函数就完成了:
func Calculate(o string) (int, error) {
stack := polishNotationStack{}
operators := strings.Split(o, " ")
for _, operatorString := range operators {
if isOperator(operatorString) {
right := stack.Pop()
left := stack.Pop()
mathFunc := getOperationFunc(operatorString)
res := mathFunc(left, right)
stack.Push(res)
} else {
val, err := strconv.Atoi(operatorString)
if err != nil {
return 0, err
}
stack.Push(val)
}
}
return int(stack.Pop()), nil
}
再次运行测试的时间:
$ go test -v .
ok
伟大的我们刚刚以一种非常简单的方式创建了一个反向波兰符号解释器(我们仍然缺少解析器,但这是另一回事)。
在本例中,我们没有使用任何接口。这并不是解释器设计模式在更多面向对象语言中的定义方式。然而,这个例子是理解语言目标的最简单的例子,下一个层次不可避免地要复杂得多,不适合初学者。
在一个更复杂的示例中,我们必须定义一个类型,该类型包含更多的自身类型、一个值或什么都不包含。使用解析器,您可以创建这个抽象语法树,以便稍后对其进行解释。
通过使用接口完成的相同示例将如以下描述部分所示。
我们将要使用的主接口称为Interpreter
接口。此接口有一个Read()
方法,每个符号(值或运算符)必须实现:
type Interpreter interface {
Read() int
}
我们将只实现运算符的求和和和减法,并为数字实现一种称为Value
的类型:
type value int
func (v *value) Read() int {
return int(*v)
}
Value
是一种int
类型,在实现Read
方法时,只返回其值:
type operationSum struct {
Left Interpreter
Right Interpreter
}
func (a *operationSum) Read() int {
return a.Left.Read() + a.Right.Read()
}
operationSum
结构具有Left
和Right
字段,其Read
方法返回其Read
方法的总和。operationSubtract
结构相同,但减去:
type operationSubtract struct {
Left Interpreter
Right Interpreter
}
func (s *operationSubtract) Read() int {
return s.Left.Read() - s.Right.Read()
}
我们还需要一个工厂模式来创建运营商;我们称之为operatorFactory
方法。现在的区别在于,它不仅接受符号,还接受从堆栈中获取的Left
和Right
值:
func operatorFactory(o string, left, right Interpreter) Interpreter {
switch o {
case SUM:
return &operationSum{
Left: left,
Right: right,
}
case SUB:
return &operationSubtract{
Left: left,
Right: right,
}
}
return nil
}
正如我们刚才提到的,我们还需要一个堆栈。我们可以通过更改其类型来重用上一示例中的一个:
type polishNotationStack []Interpreter
func (p *polishNotationStack) Push(s Interpreter) {
*p = append(*p, s)
}
func (p *polishNotationStack) Pop() Interpreter {
length := len(*p)
if length > 0 {
temp := (*p)[length-1]
*p = (*p)[:length-1]
return temp
}
return nil
}
现在堆栈使用解释器指针而不是int
,但其功能是相同的。最后,我们的main
方法看起来也类似于前面的示例:
func main() {
stack := polishNotationStack{}
operators := strings.Split("3 4 sum 2 sub", " ")
for _, operatorString := range operators {
if operatorString == SUM || operatorString == SUB {
right := stack.Pop()
left := stack.Pop()
mathFunc := operatorFactory(operatorString, left, right)
res := value(mathFunc.Read())
stack.Push(&res)
} else {
val, err := strconv.Atoi(operatorString)
if err != nil {
panic(err)
}
temp := value(val)
stack.Push(&temp)
}
}
println(int(stack.Pop().Read()))
}
与前面一样,我们首先检查符号是运算符还是值。当它是一个值时,它会将其推入堆栈。
当符号是运算符时,我们也从堆栈中获取右值和左值,我们使用当前运算符和刚从堆栈中获取的左值和右值调用工厂模式。一旦我们有了操作符类型,我们只需要调用它的Read
方法,将返回的值也推送到堆栈中。
最后,堆栈上必须只剩下一个示例,因此我们打印它:
$ go run interpreter.go
5
这种模式非常强大,但也必须小心使用。为了创建一种语言,它在用户和它提供的功能之间产生了强烈的耦合。人们可能会犯这样的错误:试图创建一种过于灵活、难以使用和维护的语言。此外,人们还可以创建一种相当小且有用的语言,这种语言有时无法正确解释,这可能会让用户感到痛苦。
在我们的示例中,我们忽略了很多错误检查,而将重点放在解释器的实现上。但是,您需要大量的错误检查和详细的错误输出来帮助用户纠正语法错误。所以,在编写语言时要开心,但要善待用户。
本章讨论了三种非常强大的模式,在将它们用于生产代码之前需要大量的实践。通过模拟典型的生产问题,与他们进行一些练习是一个非常好的主意:
我们的想法是练习编码并重读任何部分,直到您对每个模式都感到满意为止。