Go 创建模式|单例、构建者、工厂、原型和抽象工厂设计模式详解

我们定义了两种类型的汽车:豪华车和家用车。汽车制造厂必须返回我们将要涉及的第一组设计模式,即创造性模式。顾名思义,它对创建对象的常见实践进行了分组,因此对象创建更多地由需要这些对象的用户封装。主要是,创建模式尝试将现成的对象提供给用户,而不是要求用户创建对象,在某些情况下,创建对象可能很复杂,或者将代码与应该在接口中定义的功能的具体实现结合起来。

你曾经为软件工程师做过面试吗?有趣的是,当你问他们设计模式时,超过 80%的人会提到单例设计模式。为什么呢?可能是因为它是最常用的设计模式之一,或者是最容易掌握的设计模式之一。由于后一个原因,我们将开始我们的创意设计模式之旅。

说明

单例模式很容易记住。顾名思义,它将为您提供对象的单个实例,并保证没有重复的实例。

在第一次调用使用该实例时,将创建该实例,然后在应用程序中需要使用该特定行为的所有部分之间重用该实例。

您将在许多不同的情况下使用单例模式。例如:

可能性是无穷的,我们刚刚提到了其中的一些。

目标

作为一般指南,我们认为当以下规则适用时使用单模式:

  • 我们需要某种特定类型的单一共享值。
  • 我们需要在整个程序中将某些类型的对象创建限制为单个单元。

示例-唯一计数器

作为一个我们必须确保只有一个实例的对象的例子,我们将编写一个计数器,保存在程序执行期间调用它的次数。不管我们有多少个计数器实例,它们都必须计数相同的值,并且实例之间必须一致。

要求及验收标准

编写所述单个计数器有一些要求和验收标准。详情如下:

  • 以前未创建计数器时,将创建一个值为 0 的新计数器
  • 如果已经创建了计数器,则返回包含实际计数的实例
  • 如果调用方法AddOne,则计数必须增加 1

我们有一个包含三个测试的场景来检查单元测试。

先写单元测试

GO 的实现与您在纯对象语言(如 java 或 C++)中发现的有点不同,在那里有静态成员。在 Go 中,没有什么比静态成员更好的了,但我们有包范围来提供类似的结果。

要设置我们的项目,我们必须在$GOPATH/src目录中创建一个新文件夹。我们在第 1 章中提到的一般规则准备就绪。。。稳步的去是使用 VCS 提供商(如 GitHub)、用户名和项目名称创建子文件夹。

例如,在我的例子中,我使用 GitHub 作为我的 VCS,我的用户名是sayden,,因此我将创建路径$GOPATH/src/github.com/sayden/go-design-patterns/creational/singleton。路径中的go-design-patterns实例是项目名称,creational 子文件夹也将是我们的库名称,singleton 是这个特定包和子文件夹的名称:

mkdir -p $GOPATH/src/github.com/sayden/go-design-patterns/creational/singleton 
cd $GOPATH/src/github.com/sayden/go-design-
patterns/creational/singleton

在 singleton 文件夹中创建一个名为singleton.go的新文件,以反映包的名称,并为singleton类型编写以下包声明:

package singleton 

type Singleton interface { 
    AddOne() int 
} 

type singleton struct { 
    count int 
} 

var instance *singleton 

func GetInstance() Singleton { 
    return nil 
} 
func (s *singleton) AddOne() int { 
    return 0 
} 

当我们在编写代码时遵循 TDD 方法时,让我们编写使用我们刚刚声明的函数的测试。测试将按照我们之前编写的验收标准进行定义。按照测试文件中的约定,我们必须创建一个与要测试的文件同名的文件,后缀为_test.go。两者必须位于同一文件夹中:

package singleton 

import "testing" 

func TestGetInstance(t *testing.T) { 
   counter1 := GetInstance() 

   if counter1 == nil { 
         //Test of acceptance criteria 1 failed 
         t.Error("expected pointer to Singleton after calling GetInstance(), not nil") 
   } 

   expectedCounter := counter1 
} 

第一个测试在复杂的应用程序中检查一些明显但同样重要的东西。当我们请求计数器的实例时,我们实际上收到了一些东西。我们必须将其视为一种创造性模式——我们将对象的创建委托给一个未知的包,该包可能在创建或检索对象时失败。我们还将当前计数器存储在expectedCounter变量中,以便稍后进行比较:

currentCount := counter1.AddOne() 
if currentCount != 1 { 
     t.Errorf("After calling for the first time to count, the count must be 1 but it is %d\n", currentCount) 
} 

现在我们利用 Go 的零初始化特性。请记住,Go 中的整数类型不能为 nil,正如我们所知,这是对计数器的第一次调用,它是一个整数类型的变量,我们还知道它是零初始化的。因此,在第一次调用AddOne()函数之后,count 的值必须为 1。

检查第二个条件的测试证明,expectedConnection变量与我们稍后请求的返回连接没有什么不同。如果它们不同,则消息Singleton instances must be different将导致测试失败:

counter2 := GetInstance() 
if counter2 != expectedCounter { 
    //Test 2 failed 
    t.Error("Expected same instance in counter2 but it got a different instance") 
} 

最后一个测试只是使用第二个实例再次计算 1。之前的结果是 1,所以现在必须给出 2:

currentCount = counter2.AddOne() 
if currentCount != 2 { 
    t.Errorf("After calling 'AddOne' using the second counter, the current count must be 2 but was %d\n", currentCount) 
} 

要完成测试部分,我们必须做的最后一件事是执行测试,以确保它们在实现之前失败。如果其中一个测试没有失败,这意味着我们做错了什么,我们必须重新考虑这个测试。我们必须打开终端并导航到要执行的 singleton 包的路径:

$ go test -v .
=== RUN   TestGetInstance
--- FAIL: TestGetInstance (0.00s)
 singleton_test.go:9: expected pointer to Singleton after calling GetInstance(), not nil
 singleton_test.go:15: After calling for the first time to count, the count must be 1 but it is 0
 singleton_test.go:27: After calling 'AddOne' using the second counter, the current count must be 2 but was 0
FAIL
exit status 1
FAIL

实施

最后,我们必须实现单例模式。正如我们前面提到的,我们通常会编写一个方法来检索单实例,例如 java 或 C++。在 Go 中,我们没有关键字static,但是我们可以通过使用包的范围来实现相同的结果。首先,我们创建一个struct,其中包含我们希望在程序执行期间保证为单例的对象:

package creational 

type singleton struct{ 
    count int 
} 

var instance *singleton 

func GetInstance() *singleton { 
    if instance == nil { 
        instance = new(singleton) 
    }  
    return instance 
} 

func (s *singleton) AddOne() int { 
    s.count++ 
    return s.count 
} 

我们必须密切关注这段代码。在 java 或 C++语言中,变量实例在程序开始时会被初始化为 null。在 Go 中,可以将指向结构的指针初始化为nil,但不能将结构初始化为nil(相当于 NULL)。因此,var instance *singleton行将指向 Singleton 类型结构的指针定义为 nil,并将变量定义为instance

我们创建了一个GetInstance方法来检查实例是否已经初始化(instance == nil,并在instance = new(singleton)行中已经分配的空间中创建一个实例。记住,当我们使用关键字new时,我们正在括号之间创建一个指向该类型实例的指针。

AddOne方法将获取变量实例的计数,并将其增加 1,然后返回计数器的当前值。

现在让我们再次运行单元测试:

$ go test -v -run=GetInstance
=== RUN   TestGetInstance
--- PASS: TestGetInstance (0.00s)
PASS
ok

关于单例设计模式的几句话

我们已经看到了一个非常简单的单例模式示例,部分应用于某些情况,即一个简单计数器。请记住,单例模式将使您能够在应用程序中拥有某个结构的唯一实例,并且任何包都不能创建该结构的任何克隆。

对于 Singleton,您还隐藏了创建对象的复杂性,以防它需要一些计算,以及在每次需要它的实例时创建它的陷阱(如果所有实例都相似)。所有这些代码编写、检查变量是否已经存在以及存储都封装在单例中,如果使用全局变量,则无需到处重复。

这里我们学习单线程上下文的经典单例实现。当我们读到关于并发的章节时,我们将看到一个并发的单例实现,因为这个实现不是线程安全的!

说到创造性设计模式,拥有构建器设计模式看起来很有语义。构建器模式帮助我们构建复杂对象,而无需直接实例化其结构或编写所需的逻辑。想象一个对象可能有几十个字段,而这些字段本身就是更复杂的结构。现在假设您有许多具有这些特征的对象,您可以拥有更多。我们不想编写逻辑来在只需要使用对象的包中创建所有这些对象。

说明

实例创建可以简单到提供大括号{}并将实例保留为零值,也可以复杂到需要进行一些 API 调用、检查状态并为其字段创建对象的对象。您还可以拥有一个由许多对象组成的对象,这在 Go 中是非常惯用的,因为它不支持继承。

同时,可以使用相同的技术创建多种类型的对象。例如,你将使用几乎与制造公共汽车相同的技术来制造汽车,只是它们的尺寸和座椅数量不同,所以我们为什么不重用构建过程呢?这就是 Builder 模式的作用所在。

目标

生成器设计模式尝试:

  • 抽象复杂的创建,以便对象创建与对象用户分离
  • 通过填充对象字段并创建嵌入对象,逐步创建对象
  • 在多个对象之间重用对象创建算法

示例-车辆制造

构建者设计模式通常被描述为一个主管、几个构建者和他们构建的产品之间的关系。继续我们的汽车示例,我们将创建一个车辆生成器。创建车辆(产品)的过程(广泛称为算法)对于每种车辆或多或少都是相同的——选择车辆类型、组装结构、放置车轮和座椅。如果你想一想,你可以用这个描述来建造一辆汽车和一辆摩托车(两个建筑商),所以我们在制造业中重复使用这个描述来创造汽车。在我们的示例中,director 由ManufacturingDirector类型表示。

要求及验收标准

正如我们所描述的,我们必须处理一个类型为CarMotorbike的建筑商,以及一个名为ManufacturingDirector的独特导演,以获取建筑商和建筑产品。因此Vehicle建筑商示例的要求如下:

  • 我必须有一个制造类型,它构造了车辆所需的一切
  • 使用汽车制造商时,必须返回具有四个车轮、五个座椅和定义为Car的结构的VehicleProduct
  • 当使用摩托车制造商时,必须返回具有两个车轮、两个座椅和定义为Motorbike的结构的VehicleProduct
  • 任何BuildProcess建造商建造的VehicleProduct必须可以修改

车辆制造商的单元测试

根据前面的验收标准,我们将创建一个控制器变量ManufacturingDirector类型,以使用汽车和摩托车的产品生成器变量所表示的构建过程。主管是负责建筑的人,但建筑商是归还实际车辆的人。因此,我们的建造商声明如下:

package creational 

type BuildProcess interface { 
    SetWheels() BuildProcess 
    SetSeats() BuildProcess 
    SetStructure() BuildProcess 
    GetVehicle() VehicleProduct 
} 

上述接口定义了制造车辆所需的步骤。如果制造商要使用这些interface,每个制造商都必须执行此interface。在每个Set步骤上,我们都返回相同的构建过程,因此我们可以在同一个语句中将不同的步骤链接在一起,我们将在后面看到。最后,我们需要一个GetVehicle方法从构建器中检索Vehicle实例:

type ManufacturingDirector struct {} 

func (f *ManufacturingDirector) Construct() { 
    //Implementation goes here 
} 

func (f *ManufacturingDirector) SetBuilder(b BuildProcess) { 
    //Implementation goes here 
} 

ManufacturingDirectordirector 变量是负责接受构建者的变量。它有一个Construct方法,将使用Manufacturing中存储的生成器,并将复制所需的步骤。SetBuilder方法将允许我们更改Manufacturing控制器中使用的生成器:

type VehicleProduct struct { 
    Wheels    int 
    Seats     int 
    Structure string 
} 

产品是我们在使用制造过程中要检索的最终对象。在这种情况下,车辆由车轮、座椅和结构组成:

type CarBuilder struct {} 

func (c *CarBuilder) SetWheels() BuildProcess { 
    return nil 
} 

func (c *CarBuilder) SetSeats() BuildProcess { 
    return nil 
} 

func (c *CarBuilder) SetStructure() BuildProcess { 
    return nil 
} 

func (c *CarBuilder) Build() VehicleProduct { 
    return VehicleProduct{} 
} 

第一个建设者是Car建设者。它必须实现BuildProcess接口中定义的每个方法。我们将在此处设置此特定生成器的信息:

type BikeBuilder struct {} 

func (b *BikeBuilder) SetWheels() BuildProcess { 
    return nil 
} 

func (b *BikeBuilder) SetSeats() BuildProcess { 
    return nil 
} 

func (b *BikeBuilder) SetStructure() BuildProcess { 
    return nil 
} 

func (b *BikeBuilder) Build() VehicleProduct { 
    return VehicleProduct{} 
} 

Motorbike结构必须与Car结构相同,因为它们都是构建器实现,但请记住,构建每个结构的过程可能会非常不同。使用此对象声明,我们可以创建以下测试:

package creational 

import "testing" 

func TestBuilderPattern(t *testing.T) { 
    manufacturingComplex := ManufacturingDirector{} 

    carBuilder := &CarBuilder{} 
    manufacturingComplex.SetBuilder(carBuilder) 
    manufacturingComplex.Construct() 

    car := carBuilder.Build() 

    //code continues here... 

我们将从Manufacturing主管和Car建造商开始,以满足前两个验收标准。在前面的代码中,我们正在创建我们的Manufacturing控制器,该控制器将负责在测试期间创建每个车辆。在创建了Manufacturing控制器之后,我们创建了一个CarBuilder,然后使用SetBuilder方法将其传递给制造部门。一旦Manufacturing导演知道它现在要构造什么,我们就可以调用Construct方法来使用CarBuilder创建VehicleProduct。最后,一旦我们有了汽车的所有部件,我们调用CarBuilder上的GetVehicle方法来检索Car实例:

if car.Wheels != 4 { 
    t.Errorf("Wheels on a car must be 4 and they were %d\n", car.Wheels) 
} 

if car.Structure != "Car" { 
    t.Errorf("Structure on a car must be 'Car' and was %s\n", car.Structure) 
} 

if car.Seats != 5 { 
    t.Errorf("Seats on a car must be 5 and they were %d\n", car.Seats) 
} 

我们已经编写了三个小测试来检查结果是否是一辆汽车。我们检查车辆有四个车轮,结构有描述Car,座椅数量为五个。我们有足够的数据来执行测试并确保测试失败,因此我们可以认为它们是可靠的:

$ go test -v -run=TestBuilder .
=== RUN   TestBuilderPattern
--- FAIL: TestBuilderPattern (0.00s)
 builder_test.go:15: Wheels on a car must be 4 and they were 0
 builder_test.go:19: Structure on a car must be 'Car' and was
 builder_test.go:23: Seats on a car must be 5 and they were 0
FAIL

完美的现在我们将为Motorbike建筑商创建测试,涵盖第三和第四个验收标准:

bikeBuilder := &BikeBuilder{} 

manufacturingComplex.SetBuilder(bikeBuilder) 
manufacturingComplex.Construct() 

motorbike := bikeBuilder.GetVehicle() 
motorbike.Seats = 1 

if motorbike.Wheels != 2 { 
    t.Errorf("Wheels on a motorbike must be 2 and they were %d\n", motorbike.Wheels) 
} 

if motorbike.Structure != "Motorbike" { 
    t.Errorf("Structure on a motorbike must be 'Motorbike' and was %s\n", motorbike.Structure) 
} 

前面的代码是汽车测试的延续。如您所见,我们现在通过将Motorbike构建器传递给自行车,重新使用先前创建的制造来创建自行车。然后我们再次点击construct按钮来创建必要的零件,并调用生成器GetVehicle方法来检索摩托车实例。

快速查看一下,因为我们已将此特定摩托车的默认座位数更改为 1。我们想在这里展示的是,即使有构建器,您也必须能够更改返回实例中的默认信息,以满足某些特定的需要。当我们手动设置控制盘时,我们不会测试此功能。

重新运行测试会触发预期行为:

$ go test -v -run=Builder .
=== RUN   TestBuilderPattern
--- FAIL: TestBuilderPattern (0.00s)
 builder_test.go:15: Wheels on a car must be 4 and they were 0
 builder_test.go:19: Structure on a car must be 'Car' and was
 builder_test.go:23: Seats on a car must be 5 and they were 0
 builder_test.go:35: Wheels on a motorbike must be 2 and they were 0
 builder_test.go:39: Structure on a motorbike must be 'Motorbike' and was
FAIL

实施

我们将开始实施制造业。正如我们前面所说的(以及我们在单元测试中设置的),Manufacturing主管必须接受一个构建者,并使用提供的构建者构建车辆。回想一下,BuildProcess界面将定义建造任何车辆所需的通用步骤,Manufacturing主管必须接受建造商并与他们一起建造车辆:

package creational 

type ManufacturingDirector struct { 
    builder BuildProcess 
} 

func (f *ManufacturingDirector) SetBuilder(b BuildProcess) { 
    f.builder = b 
} 

func (f *ManufacturingDirector) Construct() { 
    f.builder.SetSeats().SetStructure().SetWheels() 
} 

我们的ManufacturingDirector需要一个字段来存储正在使用的生成器;此字段将被称为builderSetBuilder方法将用参数中提供的生成器替换存储的生成器。最后,仔细看看Construct方法。它采用已存储的生成器并复制BuildProcess方法,该方法将创建某种未知类型的完整车辆。如您所见,由于在每个调用上返回了BuildProcess接口,我们在同一行中使用了所有设置调用。这样,代码更紧凑:

提示

您是否意识到 Builder 模式中的 director 实体也是单例模式的一个明显候选对象?在某些场景中,只有一个 Director 实例可用可能很关键,这就是您将仅为构建器的 Director 创建单例模式的地方。设计模式组合是一种非常常见的技术,也是一种非常强大的技术!

type CarBuilder struct { 
    v VehicleProduct 
} 

func (c *CarBuilder) SetWheels() BuildProcess { 
    c.v.Wheels = 4 
    return c 
} 

func (c *CarBuilder) SetSeats() BuildProcess { 
    c.v.Seats = 5 
    return c 
} 

func (c *CarBuilder) SetStructure() BuildProcess { 
    c.v.Structure = "Car" 
    return c 
} 

func (c *CarBuilder) GetVehicle() VehicleProduct { 
    return c.v 
} 

这是我们的第一个建筑商,car建筑商。构建器需要存储一个VehicleProduct对象,我们在这里将其命名为v。然后,我们设定了汽车在我们业务中的具体需求——四轮、五座和定义为Car的结构。在GetVehicle 方法中,我们只返回存储在生成器中的VehicleProduct,该生成器必须已经由ManufacturingDirector类型构造。

type BikeBuilder struct { 
    v VehicleProduct 
} 

func (b *BikeBuilder) SetWheels() BuildProcess { 
    b.v.Wheels = 2 
    return b 
} 

func (b *BikeBuilder) SetSeats() BuildProcess { 
    b.v.Seats = 2 
    return b 
} 

func (b *BikeBuilder) SetStructure() BuildProcess { 
    b.v.Structure = "Motorbike" 
    return b 
} 

func (b *BikeBuilder) GetVehicle() VehicleProduct { 
    return b.v 
} 

Motorbike构建器与car构建器相同。我们将摩托车定义为有两个轮子、两个座位和一个称为Motorbike的结构。它与car对象非常相似,但假设您想要区分运动型摩托车(只有一个座位)和巡航型摩托车(有两个座位)。您可以简单地为运动摩托车创建一个新的结构来实现构建过程。

您可以看到这是一个重复的模式,但是在BuildProcess接口的每个方法的范围内,您可以封装任意多的复杂性,以便用户不需要知道有关对象创建的详细信息。

根据所有对象的定义,让我们再次运行测试:

=== RUN   TestBuilderPattern
--- PASS: TestBuilderPattern (0.00s)
PASS
ok  _/home/mcastro/pers/go-design-patterns/creational 0.001s

做得好!想一想向ManufacturingDirector控制器添加新车辆是多么容易,只需创建一个封装新车辆数据的新类。例如,让我们添加一个BusBuilder结构:

type BusBuilder struct { 
    v VehicleProduct 
} 

func (b *BusBuilder) SetWheels() BuildProcess { 
    b.v.Wheels = 4*2 
    return b 
} 

func (b *BusBuilder) SetSeats() BuildProcess { 
    b.v.Seats = 30 
    return b 
} 

func (b *BusBuilder) SetStructure() BuildProcess { 
    b.v.Structure = "Bus" 
    return b 
} 

func (b *BusBuilder) GetVehicle() VehicleProduct { 
    return b.v 
} 

这就是全部;您的ManufacturingDirector将按照建筑商的设计模式使用新产品。

包装建筑商设计模式

Builder 设计模式通过使用主管使用的通用构造算法,帮助我们维护数量不可预测的产品。构建过程总是从产品的用户那里抽象出来的。

同时,当源代码的新手需要向管道中添加新产品时,定义一个构造模式会有所帮助。BuildProcess接口规定了他必须遵守哪些条件才能成为可能的构建者的一部分。

但是,当您不能完全确定算法是否稳定时,请尽量避免使用构建器模式,因为此界面中的任何微小更改都会影响所有构建器,如果您添加了一些构建器需要而其他构建器不需要的新方法,则可能会很尴尬。

工厂方法模式(或者简单地说,工厂)可能是业界第二著名和使用的设计模式。它的目的是将用户从为特定目的而需要实现的结构的知识中抽象出来,例如从 web 服务或数据库检索某些值。用户只需要提供此值的界面。通过将此决策委托给工厂,该工厂可以提供符合用户需求的界面。如果需要,它还简化了底层类型实现的降级或升级过程。

说明

当使用工厂方法设计模式时,我们获得了一层额外的封装,这样我们的程序就可以在受控环境中成长。使用工厂方法,我们将对象族的创建委托给不同的包或对象,以从可能使用的对象池的知识中抽象出来。想象一下,你想用旅行社组织你的假期。你不需要经营酒店和旅游,你只需要告诉旅行社你感兴趣的目的地,这样他们就能为你提供你所需要的一切。旅行社代表着一个旅行工厂。

目标

在前面的描述之后,您必须清楚 Factory 方法设计模式的以下目标:

  • 将新结构实例的创建委托给程序的不同部分
  • 在接口级别而不是具体实现上工作
  • 对对象族进行分组以获得族对象创建者

示例-一家商店的付款方式工厂

例如,我们将实现一个 PaymentMethodFactory,它将为我们在商店提供不同的付款方式。一开始,我们有两种支付方式——现金和信用卡。我们还将有一个与方法Pay的接口,每个想用作支付方法的结构都必须实现该接口。

验收标准

根据前面的描述,验收标准的要求如下:

  • 每个支付方式都有一个通用的方法,称为Pay
  • 能够将付款方法的创建委托给工厂
  • 通过将库添加到 factory 方法,可以向库中添加更多付款方法

首台机组试验

工厂方法具有非常简单的结构;我们只需要确定存储了多少个接口实现,然后提供一个方法GetPaymentMethod,您可以将一种付款类型作为参数传递:

type PaymentMethod interface { 
    Pay(amount float32) string 
} 

前几行定义了付款方式的接口。他们定义了在商店付款的方式。Factory 方法将返回实现此接口的类型的实例:

const ( 
    Cash      = 1 
    DebitCard = 2 
) 

我们必须将工厂确定的付款方式定义为常量,以便我们可以从包外调用和检查可能的付款方式。

func GetPaymentMethod(m int) (PaymentMethod, error) { 
    return nil, errors.New("Not implemented yet") 
} 

前面的代码是为我们创建对象的函数。它返回一个指针,该指针必须有一个实现PaymentMethod接口的对象,如果请求一个未注册的方法,则返回一个错误。

type CashPM struct{} 
type DebitCardPM struct{} 

func (c *CashPM) Pay(amount float32) string { 
    return "" 
} 

func (c *DebitCardPM) Pay(amount float32) string { 
    return "" 
} 

为了完成工厂的申报,我们创建了两种付款方式。如您所见,CashPMDebitCardPM结构通过声明一个方法Pay(amount float32) string来实现PaymentMethod接口。返回的字符串将包含有关付款的信息。

有了这个声明,我们将从编写第一个验收标准的测试开始:使用通用方法检索实现PaymentMethod接口的对象:

package creational 

import ( 
    "strings" 
    "testing" 
) 

func TestCreatePaymentMethodCash(t *testing.T) { 
    payment, err := GetPaymentMethod(Cash) 
    if err != nil { 
        t.Fatal("A payment method of type 'Cash' must exist") 
    } 

    msg := payment.Pay(10.30) 
    if !strings.Contains(msg, "paid using cash") { 
        t.Error("The cash payment method message wasn't correct") 
    } 
    t.Log("LOG:", msg) 
} 

现在,我们必须在几个测试函数中分离测试。GetPaymentMethod是检索付款方式的常用方法。我们使用常量Cash,这是我们在实现文件中定义的(如果我们在包的范围之外使用这个常量,我们将使用包的名称作为前缀来调用它,因此语法将是creational.Cash。我们还检查在要求付款方式时是否未收到错误。请注意,如果我们在请求付款方式时收到错误,我们将调用t.Fatal停止执行测试;如果我们像在前面的测试中那样调用t.Error,那么当我们试图访问 nil 对象的Pay方法时,在接下来的几行中就会出现问题,我们的测试将导致执行崩溃。我们继续使用接口的Pay方法,通过 10.30 作为数量。返回的消息必须包含文本paid using casht.Log(string)法是一种特殊的测试方法。如果我们通过了-v标志,这个结构允许我们在运行测试时写入一些日志。

func TestGetPaymentMethodDebitCard(t *testing.T) { 
    payment, err = GetPaymentMethod(Debit9Card) 

    if err != nil { 
        t.Error("A payment method of type 'DebitCard' must exist")
    } 

    msg = payment.Pay(22.30) 

    if !strings.Contains(msg, "paid using debit card") { 
        t.Error("The debit card payment method message wasn't correct") 
    } 

    t.Log("LOG:", msg) 
}

我们对借记卡方法重复相同的操作。我们要求使用常量DebitCard,定义的支付方式,返回的消息在使用借记卡支付时必须包含paid using debit card字符串。


func TestGetPaymentMethodNonExistent(t *testing.T) { 
    payment, err = GetPaymentMethod(20) 

    if err == nil { 
        t.Error("A payment method with ID 20 must return an error") 
    } 
    t.Log("LOG:", err) 
}

最后,当我们请求一种不存在的支付方式时,我们将测试这种情况(由数字 20 表示,它与工厂中任何公认的常数都不匹配)。我们将检查询问未知付款方式时是否返回错误消息(任何)。

让我们检查是否所有测试都失败:

$ go test -v -run=GetPaymentMethod .
=== RUN   TestGetPaymentMethodCash
--- FAIL: TestGetPaymentMethodCash (0.00s)
 factory_test.go:11: A payment method of type 'Cash' must exist
=== RUN   TestGetPaymentMethodDebitCard
--- FAIL: TestGetPaymentMethodDebitCard (0.00s)
 factory_test.go:24: A payment method of type 'DebitCard' must exist
=== RUN   TestGetPaymentMethodNonExistent
--- PASS: TestGetPaymentMethodNonExistent (0.00s)
 factory_test.go:38: LOG: Not implemented yet
FAIL
exit status 1
FAIL

正如您在本例中所看到的,我们只能看到返回PaymentMethod接口的测试失败。在这种情况下,我们必须实现代码的一部分,然后在继续之前再次测试。

实施

我们将从GetPaymentMethod方法开始。它必须接收一个与同一文件的某个已定义常量匹配的整数,才能知道它应该返回哪个实现。

package creational 

import ( 
    "errors" 
    "fmt" 
) 

type PaymentMethod interface { 
    Pay(amount float32) string 
} 

const ( 
    Cash      = 1 
    DebitCard = 2 
) 

type CashPM struct{} 
type DebitCardPM struct{} 

func GetPaymentMethod(m int) (PaymentMethod, error) { 
    switch m { 
        case Cash: 
        return new(CashPM), nil 
        case DebitCard: 
        return new(DebitCardPM), nil 
        default: 
        return nil, errors.New(fmt.Sprintf("Payment method %d not recognized\n", m)) 
    } 
} 

我们使用一个普通开关来检查参数m(方法)的内容。如果它匹配任何已知的方法——现金或借记卡,它将返回它们的一个新实例。否则,它将返回一个 nil 和一个错误,表明尚未识别付款方法。现在,我们可以再次运行测试来检查单元测试的第二部分:

$go test -v -run=GetPaymentMethod .
=== RUN   TestGetPaymentMethodCash
--- FAIL: TestGetPaymentMethodCash (0.00s)
 factory_test.go:16: The cash payment method message wasn't correct
 factory_test.go:18: LOG:
=== RUN   TestGetPaymentMethodDebitCard
--- FAIL: TestGetPaymentMethodDebitCard (0.00s)
 factory_test.go:28: The debit card payment method message wasn't correct
 factory_test.go:30: LOG:
=== RUN   TestGetPaymentMethodNonExistent
--- PASS: TestGetPaymentMethodNonExistent (0.00s)
 factory_test.go:38: LOG: Payment method 20 not recognized
FAIL
exit status 1
FAIL

现在我们没有收到错误,说它找不到付款方式的类型。相反,当它尝试使用它涵盖的任何方法时,我们会收到一个message not correct错误。我们还删除了当我们询问未知付款方式时返回的Not implemented消息。现在让我们实现结构:

type CashPM struct{} 
type DebitCardPM struct{} 

func (c *CashPM) Pay(amount float32) string { 
     return fmt.Sprintf("%0.2f paid using cash\n", amount) 
} 

func (c *DebitCardPM) Pay(amount float32) string { 
     return fmt.Sprintf("%#0.2f paid using debit card\n", amount) 
} 

我们只需获取金额,并将其打印在格式良好的消息中。通过此实现,现在所有测试都将通过:

$ go test -v -run=GetPaymentMethod .
=== RUN   TestGetPaymentMethodCash
--- PASS: TestGetPaymentMethodCash (0.00s)
 factory_test.go:18: LOG: 10.30 paid using cash
=== RUN   TestGetPaymentMethodDebitCard
--- PASS: TestGetPaymentMethodDebitCard (0.00s)
 factory_test.go:30: LOG: 22.30 paid using debit card
=== RUN   TestGetPaymentMethodNonExistent
--- PASS: TestGetPaymentMethodNonExistent (0.00s)
 factory_test.go:38: LOG: Payment method 20 not recognized
PASS
ok

你看到LOG:消息了吗?它们不是错误,我们只是打印使用测试包时收到的一些信息。除非将-v标志传递给测试命令,否则可以忽略这些消息:

$ go test -run=GetPaymentMethod .
ok

将 Debitcard 方法升级到新平台

现在假设您的DebitCard支付方式由于某种原因发生了变化,您需要一个新的结构。要实现此场景,您只需在用户要求DebitCard支付方式时创建新结构并替换旧结构:

type CreditCardPM struct {} 
 func (d *CreditCardPM) Pay(amount float32) string { 
   return fmt.Sprintf("%#0.2f paid using new credit card implementation\n", amount) 
} 

这是我们的新型结构,将取代DebitCardPM结构。CreditCardPM实现与借记卡相同的PaymentMethod接口。我们没有删除前一个,以防将来需要它。唯一的区别在于返回的消息现在包含关于新类型的信息。我们还必须修改方法以检索付款方法:

func GetPaymentMethod(m int) (PaymentMethod, error) { 
    switch m { 
        case Cash: 
        return new(CashPM), nil 
        case DebitCard: 
        return new(CreditCardPM), nil 
        default: 
        return nil, errors.New(fmt.Sprintf("Payment method %d not recognized\n", m)) 
   } 
} 

唯一的修改是在我们创建新借记卡的那一行,它现在指向新创建的结构。让我们运行测试,看看一切是否仍然正确:

$ go test -v -run=GetPaymentMethod .
=== RUN   TestGetPaymentMethodCash
--- PASS: TestGetPaymentMethodCash (0.00s)
 factory_test.go:18: LOG: 10.30 paid using cash
=== RUN   TestGetPaymentMethodDebitCard
--- FAIL: TestGetPaymentMethodDebitCard (0.00s)
 factory_test.go:28: The debit card payment method message wasn't correct
 factory_test.go:30: LOG: 22.30 paid using new debit card implementation
=== RUN   TestGetPaymentMethodNonExistent
--- PASS: TestGetPaymentMethodNonExistent (0.00s)
 factory_test.go:38: LOG: Payment method 20 not recognized
FAIL
exit status 1
FAIL

哦,哦!出了点问题。使用信用卡付款时预期的消息与返回的消息不匹配。这是否意味着我们的代码不正确?一般来说,是的,你不应该为了让你的程序工作而修改你的测试。在定义测试时,您还应该注意不要定义太多,因为您可以在测试中实现一些代码中没有的耦合。通过消息限制,我们对消息有一些语法正确的可能性,因此我们将其更改为以下内容:

return fmt.Sprintf("%#0.2f paid using debit card (new)\n", amount) 

我们现在再次运行测试:

$ go test -v -run=GetPaymentMethod .
=== RUN   TestGetPaymentMethodCash
--- PASS: TestGetPaymentMethodCash (0.00s)
 factory_test.go:18: LOG: 10.30 paid using cash
=== RUN   TestGetPaymentMethodDebitCard
--- PASS: TestGetPaymentMethodDebitCard (0.00s)
 factory_test.go:30: LOG: 22.30 paid using debit card (new)
=== RUN   TestGetPaymentMethodNonExistent
--- PASS: TestGetPaymentMethodNonExistent (0.00s)
 factory_test.go:38: LOG: Payment method 20 not recognized
PASS
ok

一切又好起来了。这只是如何编写好的单元测试的一个小例子。当我们想要检查借记卡支付方法是否返回包含paid using debit card字符串的消息时,我们可能有点限制,最好单独检查这些单词,或者为返回的消息定义更好的格式。

关于工厂方法,我们学到了什么

通过 Factory 方法模式,我们学习了如何对对象族进行分组,以便它们的实现超出我们的范围。我们还了解了当需要升级已用结构的实现时应该做什么。最后,我们已经看到,如果您不想将自己绑定到某些与测试没有直接关系的实现,那么编写测试时必须小心。

在了解了 factory 设计模式(我们在案例支付方法中对一系列相关对象进行了分组)之后,我们很快就会想到——如果我将对象系列分组到一个更结构化的系列层次结构中会怎么样?

说明

抽象工厂设计模式是一个新的分组层,用于实现更大(更复杂)的复合对象,该对象通过其接口使用。在族中对对象进行分组和对族进行分组背后的思想是,要有可以互换且更容易成长的大型工厂。在开发的早期阶段,使用工厂和抽象工厂也比等到所有具体实现完成后才启动代码更容易。此外,除非您知道特定字段的对象库存将非常大,并且可以轻松地将其分组到族中,否则您不会从一开始就编写抽象工厂。

目标

当对象数量增长如此之快,以至于创建一个唯一的点来获取所有对象似乎是获得运行时对象创建灵活性的唯一方法时,对相关对象族进行分组非常方便。您必须清楚抽象工厂方法的以下目标:

  • 为工厂方法提供一个新的封装层,为所有工厂返回一个公共接口
  • 将普通工厂分组为超级工厂(也称工厂之工厂)

车辆制造厂的例子,再说一遍?

对于我们的示例,我们将重用在 Builder 设计模式中创建的工厂。我们希望展示使用不同方法解决相同问题的相似性,以便您可以看到每种方法的优缺点。这将向您展示 Go 中隐式接口的威力,因为我们几乎不需要接触任何东西。最后,我们将创建一个新工厂来创建发货订单。

验收标准

以下是使用Vehicle对象工厂方法的验收标准:

  • 我们必须使用抽象工厂返回的工厂来检索Vehicle对象。
  • 车辆必须是实现两个接口(VehicleCarVehicleMotorbikeMotorbikeCar的具体实现。

单元测试

这将是一个很长的例子,请注意。我们将拥有以下实体:

  • Vehicle: The interface that all objects in our factories must implement:
    • 摩托车:运动型(单座)和巡航型(双座)摩托车的接口。
    • 轿车:豪华型(四门)和家庭型(五门)轿车的接口。
  • VehicleFactory: An interface (the Abstract Factory) to retrieve factories that implement the VehicleFactory method:
    • 摩托车工厂:实现VehicleFactory接口的工厂,返回实现VehicleMotorbike接口的车辆。
    • 车辆工厂:另一个实现VehicleFactory接口的工厂,返回实现VehicleCar接口的车辆。

为了清楚起见,我们将把每个实体分割成不同的文件。我们将从Vehicle接口开始,该接口将位于vehicle.go文件中:

package abstract_factory 

type Vehicle interface { 
    NumWheels() int 
    NumSeats() int 
} 

CarMotorbike接口将分别位于car.gomotorbike.go文件中:

// Package abstract_factory file: car.go 
package abstract_factory 

type Car interface { 
    NumDoors() int 
} 
// Package abstract_factory file: motorbike.go 
package abstract_factory 

type Motorbike interface { 
    GetMotorbikeType() int 
} 

我们还有最后一个接口,每个工厂都必须实现。这将在vehicle_factory.go文件中:

package abstract_factory 

type VehicleFactory interface { 
    NewVehicle(v int) (Vehicle, error) 
} 

所以,现在我们要申报汽车工厂。必须实现前面定义的VehicleFactory接口才能返回Vehicles实例:

const ( 
    LuxuryCarType = 1 
    FamilyCarType = 2 
) 

type CarFactory struct{} 
func (c *CarFactory) NewVehicle(v int) (Vehicle, error) { 
    switch v { 
        case LuxuryCarType: 
        return new(LuxuryCar), nil 
        case FamilyCarType: 
        return new(FamilyCar), nil 
        default: 
        return nil, errors.New(fmt.Sprintf("Vehicle of type %d not recognized\n", v)) 
    } 
} 

我们定义了两种类型的汽车——豪华型和家用型。car工厂将不得不退回实现CarVehicle接口的车辆,因此我们需要两个具体实现:

//luxury_car.go 
package abstract_factory 

type LuxuryCar struct{} 

func (*LuxuryCar) NumDoors() int { 
    return 4 
} 
func (*LuxuryCar) NumWheels() int { 
    return 4 
} 
func (*LuxuryCar) NumSeats() int { 
    return 5 
} 

package abstract_factory 

type FamilyCar struct{} 

func (*FamilyCar) NumDoors() int { 
    return 5 
} 
func (*FamilyCar) NumWheels() int { 
    return 4 
} 
func (*FamilyCar) NumSeats() int { 
    return 5 
} 

汽车就这些了。现在我们需要摩托车厂,它和汽车厂一样,必须实现VehicleFactory接口:

const ( 
    SportMotorbikeType = 1 
    CruiseMotorbikeType = 2 
) 

type MotorbikeFactory struct{} 

func (m *MotorbikeFactory) Build(v int) (Vehicle, error) { 
    switch v { 
        case SportMotorbikeType: 
        return new(SportMotorbike), nil 
        case CruiseMotorbikeType: 
        return new(CruiseMotorbike), nil 
        default: 
        return nil, errors.New(fmt.Sprintf("Vehicle of type %d not recognized\n", v)) 
    } 
} 

对于摩托车厂,我们还使用const关键字定义了两种类型的摩托车:SportMotorbikeTypeCruiseMotorbikeType。我们将切换Build方法中的v参数,以了解应该返回哪种类型。让我们来写两辆混凝土摩托车:

//sport_motorbike.go 
package abstract_factory 

type SportMotorbike struct{} 

func (s *SportMotorbike) NumWheels() int { 
    return 2 
} 
func (s *SportMotorbike) NumSeats() int { 
    return 1 
} 
func (s *SportMotorbike) GetMotorbikeType() int { 
    return SportMotorbikeType 
} 

//cruise_motorbike.go 
package abstract_factory 

type CruiseMotorbike struct{} 

func (c *CruiseMotorbike) NumWheels() int { 
    return 2 
} 
func (c *CruiseMotorbike) NumSeats() int { 
    return 2 
} 
func (c *CruiseMotorbike) GetMotorbikeType() int { 
    return CruiseMotorbikeType 
} 

为了完成,我们需要抽象工厂本身,我们将其放入先前创建的vehicle_factory.go文件中:

package abstract_factory 

import ( 
    "fmt" 
    "errors" 
) 

type VehicleFactory interface { 
    Build(v int) (Vehicle, error) 
} 

const ( 
    CarFactoryType = 1 
    MotorbikeFactoryType = 2 
) 

func BuildFactory(f int) (VehicleFactory, error) { 
    switch f { 
        default: 
        return nil, errors.New(fmt.Sprintf("Factory with id %d not recognized\n", f)) 
    } 
}

我们将编写足够的测试来进行可靠的检查,因为本书的范围并没有涵盖 100%的语句。对于读者来说,完成这些测试将是一个很好的练习。首先,一个motorbike工厂测试:

package abstract_factory 

import "testing" 

func TestMotorbikeFactory(t *testing.T) { 
    motorbikeF, err := BuildFactory(MotorbikeFactoryType) 
    if err != nil { 
        t.Fatal(err) 
    } 

    motorbikeVehicle, err := motorbikeF.Build(SportMotorbikeType) 
    if err != nil { 
        t.Fatal(err) 
    } 

    t.Logf("Motorbike vehicle has %d wheels\n", motorbikeVehicle.NumWheels()) 

    sportBike, ok := motorbikeVehicle.(Motorbike) 
    if !ok { 
        t.Fatal("Struct assertion has failed") 
    } 
    t.Logf("Sport motorbike has type %d\n", sportBike.GetMotorbikeType()) 
} 

我们使用包方法BuildFactory检索摩托车工厂(在参数中传递MotorbikeFactoryID),并检查是否有任何错误。然后,已经在摩托车厂,我们要求一辆类型为SportMotorbikeType的车辆,并再次检查错误。对于返回的车辆,我们可以询问车辆接口的方法(NumWheelsNumSeats。我们知道它是一辆摩托车,但是如果不使用类型断言,我们就无法询问摩托车的类型。我们使用车辆上的类型断言检索代码行sportBike, found := motorbikeVehicle.(Motorbike)motorbikeVehicle表示的摩托车,我们必须检查收到的类型是否正确。

最后,现在我们有一个摩托车实例,我们可以使用GetMotorbikeType方法询问自行车类型。现在我们将编写一个测试,以同样的方式检查汽车工厂:

func TestCarFactory(t *testing.T) { 
    carF, err := BuildFactory(CarFactoryType) 
    if err != nil { 
        t.Fatal(err) 
    } 

    carVehicle, err := carF.Build(LuxuryCarType) 
    if err != nil { 
        t.Fatal(err) 
    } 

    t.Logf("Car vehicle has %d seats\n", carVehicle.NumWheels()) 

    luxuryCar, ok := carVehicle.(Car) 
    if !ok { 
        t.Fatal("Struct assertion has failed") 
    } 
    t.Logf("Luxury car has %d doors.\n", luxuryCar.NumDoors()) 
} 

同样,我们使用BuildFactory方法通过参数中的CarFactoryType检索Car工厂。对于这个工厂,我们需要一辆Luxury类型的汽车,这样它就会返回一个vehicle实例。我们再次执行类型断言以指向一个 car 实例,这样我们就可以使用NumDoors方法询问门的数量。

让我们运行单元测试:

go test -v -run=Factory .
=== RUN   TestMotorbikeFactory
--- FAIL: TestMotorbikeFactory (0.00s)
 vehicle_factory_test.go:8: Factory with id 2 not recognized
=== RUN   TestCarFactory
--- FAIL: TestCarFactory (0.00s)
 vehicle_factory_test.go:28: Factory with id 1 not recognized
FAIL
exit status 1
FAIL 

完成。它无法识别任何工厂,因为它们的实现尚未完成。

实施

为了简洁起见,每个工厂的实现都已经完成了。它们与 Factory 方法非常相似,唯一的区别是在 Factory 方法中,我们不使用 Factory 方法的实例,因为我们直接使用包函数。vehicle工厂的实施情况如下:

func BuildFactory(f int) (VehicleFactory, error) { 
    switch f { 
        case CarFactoryType: 
        return new(CarFactory), nil 
        case MotorbikeFactoryType: 
        return new(MotorbikeFactory), nil 
        default: 
        return nil, errors.New(fmt.Sprintf("Factory with id %d not recognized\n", f)) 
    } 
} 

像在任何工厂一样,我们在工厂的各种可能性之间切换,以返回所需的产品。由于我们已经实施了所有混凝土车辆,测试也必须运行:

go test -v -run=Factory -cover .
=== RUN   TestMotorbikeFactory
--- PASS: TestMotorbikeFactory (0.00s)
 vehicle_factory_test.go:16: Motorbike vehicle has 2 wheels
 vehicle_factory_test.go:22: Sport motorbike has type 1
=== RUN   TestCarFactory
--- PASS: TestCarFactory (0.00s)
 vehicle_factory_test.go:36: Car vehicle has 4 seats
 vehicle_factory_test.go:42: Luxury car has 4 doors.
PASS
coverage: 45.8% of statements
ok

他们都通过了。仔细观察并注意,我们在运行测试时使用了-cover标志来返回包的覆盖率:45.8%。这告诉我们,45.8%的行被我们编写的测试覆盖,但 54.2%的行仍然没有被测试。这是因为我们还没有对巡航摩托车和家用车进行测试。如果你编写这些测试,结果应该会上升到 70.8%左右。

提示

类型断言在其他语言中也称为强制转换。当您有一个接口实例(本质上是指向结构的指针)时,您就可以访问接口方法。使用类型断言,您可以告诉编译器指向的结构的类型,这样您就可以访问整个结构字段和方法。

关于抽象工厂法的几行

我们已经学习了如何编写一个工厂工厂,为我们提供一个非常通用的车辆类型对象。此模式通常用于许多应用程序和库,例如跨平台 GUI 库。设想一个按钮、一个通用对象和按钮工厂,它为您提供 Microsoft Windows 按钮的工厂,而为 Mac OS X 按钮提供另一个工厂。您不想处理每个平台的实现细节,只想实现按钮引发的某些特定行为的操作。

此外,我们还看到了使用两种不同的解决方案处理同一问题时的差异——抽象工厂和构建器模式。正如您所看到的,使用 Builder 模式,我们有一个非结构化的对象列表(在同一工厂中有摩托车的汽车)。此外,我们鼓励在 Builder 模式中重用构建算法。在抽象工厂中,我们有一个非常结构化的车辆列表(摩托车工厂和汽车工厂)。我们也没有将汽车和摩托车的创作混为一谈,在创作过程中提供了更多的灵活性。抽象工厂模式和构建器模式都可以解决相同的问题,但您的特殊需求将帮助您找到细微的差异,这些差异将引导您采取一种或另一种解决方案。

我们将在本章中看到的最后一个模式是原型模式。像所有的创造模式一样,这在创建对象时也很方便,而且很常见的情况是原型模式被更多的模式包围。

在使用 Builder 模式时,我们正在处理重复的构建算法,在工厂中,我们正在简化许多类型对象的创建;对于原型模式,我们将使用已经创建的某种类型的实例来克隆它,并根据每个上下文的特定需求来完成它。让我们详细看看。

说明

原型模式的目标是拥有一个或一组对象,这些对象在编译时已经创建,但可以在运行时根据需要进行多次克隆。例如,对于刚注册到您的网页的用户或某些服务中的默认定价计划,这是非常有用的。此模式与构建器模式之间的关键区别在于为用户克隆对象,而不是在运行时构建对象。您还可以构建类似缓存的解决方案,使用原型存储信息。

目标

原型设计模式的主要目标是避免重复创建对象。设想一个由几十个字段和嵌入类型组成的默认对象。我们不希望每次使用该对象时都写入该类型所需的所有内容,特别是如果我们可以通过创建具有不同基础的实例将其搞糟:

  • 维护一组将被克隆以创建新实例的对象
  • 提供某种类型的默认值以在其上开始工作
  • 释放复杂对象初始化的 CPU 以占用更多内存资源

示例

我们将建立一个虚构的定制衬衫店的一个小组成部分,该店将有一些默认颜色和价格的衬衫。每件衬衫还将有一个库存单元(SKU),一个识别存储在特定位置的物品的系统),需要更新。

验收标准

为了实现示例中描述的功能,我们将使用衬衫原型。每次我们需要一件新衬衫时,我们都会把这个原型复制下来,然后使用它。具体而言,这些是本示例中使用原型图案设计方法的验收标准:

  • 有一个 shirt cloner 对象和界面来请求不同类型的衬衫(白色、黑色和蓝色分别为 15.00 美元、16.00 美元和 17.00 美元)
  • 当您请求白色衬衫时,必须复制白色衬衫,并且新实例必须与原始实例不同
  • 已创建对象的 SKU 不应影响新对象的创建
  • info 方法必须提供实例字段上的所有可用信息,包括更新的 SKU

单元测试

首先,我们需要一个ShirtCloner接口和一个实现它的对象。此外,我们还需要一个名为GetShirtsCloner的包级函数来检索克隆器的新实例:

type ShirtCloner interface { 
    GetClone(s int) (ItemInfoGetter, error) 
} 

const ( 
    White = 1 
    Black = 2 
    Blue  = 3 
) 

func GetShirtsCloner() ShirtCloner { 
    return nil 
} 

type ShirtsCache struct {} 
func (s *ShirtsCache)GetClone(s int) (ItemInfoGetter, error) { 
    return nil, errors.New("Not implemented yet") 
} 

现在我们需要一个要克隆的对象结构,它实现了一个接口来检索其字段的信息。我们将调用对象ShirtItemInfoGetter接口:

type ItemInfoGetter interface { 
    GetInfo() string 
} 

type ShirtColor byte 

type Shirt struct { 
    Price float32 
    SKU   string 
    Color ShirtColor 
} 
func (s *Shirt) GetInfo()string { 
    return "" 
} 

func GetShirtsCloner() ShirtCloner { 
    return nil 
} 

var whitePrototype *Shirt = &Shirt{ 
    Price: 15.00, 
    SKU:   "empty", 
    Color: White, 
} 

func (i *Shirt) GetPrice() float32 { 
    return i.Price 
} 

提示

您是否意识到我们定义的名为ShirtColor的类型只是byte类型?也许你想知道为什么我们没有简单地使用字节type。我们可以,但通过这种方式我们创建了一个易于阅读的结构,如果需要的话,我们可以在将来用一些方法升级它。例如,我们可以编写一个以字符串格式返回颜色的String()方法(White用于类型 1,Black用于类型 2,Blue用于类型 3)。

有了这段代码,我们就可以编写第一个测试了:

func TestClone(t *testing.T) { 
    shirtCache := GetShirtsCloner() 
    if shirtCache == nil { 
        t.Fatal("Received cache was nil") 
    } 

    item1, err := shirtCache.GetClone(White) 
    if err != nil { 
        t.Error(err) 
} 

//more code continues here... 

我们将介绍场景的第一种情况,其中需要一个 cloner 对象,我们可以使用它来请求不同的衬衫颜色。

对于第二种情况,我们将获取原始对象(我们可以访问它,因为我们在包的范围内),并将其与我们的shirt1实例进行比较。

if item1 == whitePrototype { 
    t.Error("item1 cannot be equal to the white prototype"); 
} 

现在,第三个案例。首先,我们将为衬衫键入 assertitem1,以便设置 SKU。我们将创建第二件衬衫,也是白色的,我们也将键入 assert,以检查 SKU 是否不同:

shirt1, ok := item1.(*Shirt) 
if !ok { 
    t.Fatal("Type assertion for shirt1 couldn't be done successfully") 
} 
shirt1.SKU = "abbcc" 

item2, err := shirtCache.GetClone(White) 
if err != nil { 
    t.Fatal(err) 
} 

shirt2, ok := item2.(*Shirt) 
if !ok { 
    t.Fatal("Type assertion for shirt1 couldn't be done successfully") 
} 

if shirt1.SKU == shirt2.SKU { 
    t.Error("SKU's of shirt1 and shirt2 must be different") 
} 

if shirt1 == shirt2 { 
    t.Error("Shirt 1 cannot be equal to Shirt 2") 
} 

最后,对于第四种情况,我们记录第一件和第二件衬衫的信息:

t.Logf("LOG: %s", shirt1.GetInfo()) 
t.Logf("LOG: %s", shirt2.GetInfo()) 

我们将打印两件衬衫的内存位置,因此我们在更物理的层面上做出此断言:

t.Logf("LOG: The memory positions of the shirts are different %p != %p \n\n", &shirt1, &shirt2) 

Finally, we run the tests so we can check that it fails:

go test -run=TestClone . 
--- FAIL: TestClone (0.00s) 
prototype_test.go:10: Not implemented yet 
FAIL 
FAIL

我们必须停在那里,这样当我们试图使用由GetShirtsCloner函数返回的 nil 对象时,测试就不会恐慌。

实施

我们将从GetClone方法开始。此方法应返回指定类型的项,我们有三种类型:白色、黑色和蓝色:

var whitePrototype *Shirt = &Shirt{ 
    Price: 15.00, 
    SKU:   "empty", 
    Color: White, 
} 

var blackPrototype *Shirt = &Shirt{ 
    Price: 16.00, 
    SKU:   "empty", 
    Color: Black, 
} 

var bluePrototype *Shirt = &Shirt{ 
    Price: 17.00, 
    SKU:   "empty", 
    Color: Blue, 
} 

现在我们有了三个原型,我们可以实现GetClone(s int)方法:

type ShirtsCache struct {} 
func (s *ShirtsCache)GetClone(s int) (ItemInfoGetter, error) { 
    switch m { 
        case White: 
            newItem := *whitePrototype 
            return &newItem, nil 
        case Black: 
            newItem := *blackPrototype 
            return &newItem, nil 
        case Blue: 
            newItem := *bluePrototype 
            return &newItem, nil 
        default: 
            return nil, errors.New("Shirt model not recognized") 
    } 
} 

Shirt结构还需要GetInfo实现来打印实例的内容。

type ShirtColor byte 

type Shirt struct { 
    Price float32 
    SKU   string 
    Color ShirtColor 
} 

func (s *Shirt) GetInfo() string { 
    return fmt.Sprintf("Shirt with SKU '%s' and Color id %d that costs %f\n", s.SKU, s.Color, s.Price) 
} 

最后,让我们运行测试,看看现在一切正常:

go test -run=TestClone -v . 
=== RUN   TestClone 
--- PASS: TestClone (0.00s) 
prototype_test.go:41: LOG: Shirt with SKU 'abbcc' and Color id 1 that costs 15.000000 
prototype_test.go:42: LOG: Shirt with SKU 'empty' and Color id 1 that costs 15.000000 
prototype_test.go:44: LOG: The memory positions of the shirts are different 0xc42002c038 != 0xc42002c040  

PASS 
ok

在日志中,(运行测试时记得设置-v标志),您可以检查shirt1shirt2是否有不同的 SKU。此外,我们还可以看到两个对象的记忆位置。考虑到计算机上显示的位置可能会有所不同。

关于原型设计模式我们学到了什么

原型模式是构建缓存和默认对象的强大工具。您可能也已经意识到,某些模式可能会有一些重叠,但它们有一些细微的差异,这使得它们在某些情况下更合适,而在其他情况下则不太合适。

我们已经看到了软件行业常用的五种主要的创造性设计模式。它们的目的是为了复杂性或可维护性,从对象的创建中抽象出用户。自 20 世纪 90 年代以来,它们一直是数以千计的应用程序和图书馆的基础,而我们现在使用的大多数软件在引擎盖下都有许多这样的创作模式。

值得一提的是,这些模式不是无线程的。在更高级的一章中,我们将看到 Go 中的并发编程,以及如何使用并发方法创建一些更关键的设计模式。

教程来源于Github,感谢apachecn大佬的无私奉献,致敬!

技术教程推荐

Java并发编程实战 -〔王宝令〕

玩转webpack -〔程柳锋〕

Node.js开发实战 -〔杨浩〕

MongoDB高手课 -〔唐建法(TJ)〕

后端技术面试 38 讲 -〔李智慧〕

MySQL 必知必会 -〔朱晓峰〕

高并发系统实战课 -〔徐长龙〕

零基础GPT应用入门课 -〔林健(键盘)〕

云时代的JVM原理与实战 -〔康杨〕