Go 行为模式|策略、责任链和命令设计模式详解

我们将要看到的最后一组常见模式是行为模式。现在,我们不打算定义结构或封装对象创建,而是要处理行为。

在行为模式中要处理什么?现在我们将封装行为,例如,策略模式中的算法或命令模式中的执行。

正确的行为设计是了解如何处理对象创建和结构后的最后一步。正确定义行为是好的软件设计的最后一步,因为总的来说,好的软件设计让我们可以轻松地改进算法和修复错误,而最好的算法实现不会让我们免于糟糕的软件设计。

策略模式可能是最容易理解的行为模式。在开发之前的模式时,我们已经使用了它几次,但没有停下来讨论它。现在我们会的。

说明

策略模式使用不同的算法来实现某些特定功能。这些算法隐藏在接口后面,当然,它们必须是可互换的。所有算法都以不同的方式实现相同的功能。例如,我们可以有一个Sort接口和一些排序算法。结果是一样的,一些列表被排序,但是我们可以使用快速排序、合并排序等等。

你能猜到我们在前几章中什么时候使用了策略模式吗?三,二,一。。。嗯,我们在使用io.Writer界面时大量使用了策略模式。io.Writer接口定义了一个写策略,功能总是一样的——写东西。我们可以将它写入标准输出、某个文件或用户定义的类型,但最后我们做了同样的事情——写入。我们只是改变了写作的策略(在本例中,我们改变了写作的地点)。

目标

策略模式的目标非常明确。该模式应执行以下操作:

问题是这个定义涵盖了大量的可能性。这是因为策略模式实际上用于各种场景,许多软件工程解决方案中都包含某种策略。因此,最好用一个真实的例子来看待它的实际应用。

呈现图像或文本

对于这个例子,我们将做一些不同的事情。我们还将在文件上绘制对象,而不是仅在控制台上打印文本。

在本例中,我们将有两种策略:控制台和文件。但是图书馆的用户不必处理它们背后的复杂性。

关键特性是“调用者”不知道底层库是如何工作的,他只知道定义的策略上可用的信息。在下图中可以很好地看到这一点:

Rendering images or text

在这个图中,我们选择了打印到控制台,但我们不会直接处理控制台策略类型,我们将始终使用表示它的接口。控制台类别类型将在main函数中隐藏向调用者打印控制台的实现细节。文件策略隐藏了它的实现细节以及任何未来的策略。

验收标准

一项策略必须有一个非常明确的目标,我们将有两种方法来实现它。我们的目标如下:

  • 提供一种向用户显示文本或图像中的对象(正方形)的方法
  • 启动应用程序时,用户必须在图像或文本之间进行选择
  • 应用程序必须能够添加更多的可视化策略(例如音频)
  • 如果用户选择文本,则必须在控制台中打印单词Square
  • 如果用户选择图像,则黑色背景上的白色正方形图像将打印在文件上

实施

我们不打算为这个例子编写测试,因为检查屏幕上是否出现了图像会非常复杂(尽管使用OpenCV(一个令人印象深刻的计算机视觉库)并非不可能)。我们将直接从定义每个打印策略必须实现的策略接口开始(在本例中为文件和控制台类型):

type PrintStrategy interface { 
  Print() error 
} 

这就是全部。我们的策略定义了一个简单的Print()方法,该方法返回error(例如,在处理文件时,错误返回类型是必需的)。需要实现PrintStrategy的类型称为ConsoleSquareImageSquare类型:

type ConsoleSquare struct {} 

type ImageSquare struct { 
  DestinationFilePath string 
} 

ConsoleSquare结构不需要任何内部字段,因为它总是将单词Square打印到控制台。ImageSquare结构将存储一个字段,用于图像文件的目的地,我们将在其中打印正方形。我们将从ConsoleSquare类型的实现开始,因为它是最简单的:

func(c *ConsoleSquare) Print() error { 
  println("Square")  
  return nil 
} 

非常简单,但图像更复杂。我们不会花太多时间详细解释image包是如何工作的,因为代码很容易理解:

func (t *ImageSquare) Print() error { 
  width := 800 
  height := 600 

  origin := image.Point{0, 0} 

  bgImage := image.NewRGBA(image.Rectangle{ 
    Min: origin, 
    Max: image.Point{X: width, Y: height}, 
  }) 

  bgColor := image.Uniform{color.RGBA{R: 70, G: 70, B: 70, A:0}} 
  quality := &jpeg.Options{Quality: 75} 

  draw.Print(bgImage, bgImage.Bounds(), &bgColor, origin, draw.Src) 

然而,这里有一个简短的解释:

  • 我们为图像定义了一个宽度为 800 像素、高度为 600 像素的大小(widthheight变量)。这些将是我们图像的大小限制,我们在该大小之外写的任何东西都将不可见。
  • origin变量存储一个image.Point,一种表示任何二维空间中位置的类型。我们将该点的位置设置为图像左上角的(0,0)
  • 我们需要一个位图来表示我们的背景,这里我们称之为bgImage。我们在映像包中有一个非常方便的函数来创建名为image.NewRGBAimage.RGBA类型。我们需要向这个函数传递一个矩形,以便它知道图像的边界。矩形由两种image.Point类型表示——其左上角点(Min字段)和右下角点(Max字段)。我们使用origin作为左上角,一个新点的值为widthheight作为右下角点。
  • 图像将具有灰色背景色(bgColor。这是通过实例化一种表示统一颜色(因此得名)的类型image.Uniformimage.Uniform类型需要color.Color接口的实例。color.Color类型是实现RGBA() (r, g, b, a uint32) 方法以返回红、绿、蓝和阿尔法颜色(RGBA)的uint32值的任何类型。Alpha 是像素透明度的值。color包为此方便地提供了一个名为color.RGBA的类型(以防我们不需要实现自己的,这就是我们的情况)。
  • 当以特定格式存储图像时,我们必须指定图像的质量。当然,这不仅会影响文件的质量,还会影响文件的大小。这里,它被定义为 75;100 是我们能设定的最高质量。如您所见,我们在这里使用jpeg包来设置一个名为Options的类型的值,该类型只存储质量值,没有更多的值可应用。
  • 最后,draw.Print函数将具有我们在同一图像定义的边界上定义的特征的像素写入所提供的图像(bgImagedraw.Print方法的第一个参数获取目标图像,我们在其中使用了bgImage。第二个参数是要在目标图像中绘制的对象的边界,我们使用了图像的相同边界,但是如果我们想要一个较小的矩形,我们可以使用任何其他边界。第三个参数是用于为边界着色的颜色。Origin变量用于告诉绑定的左上角必须放置在哪里。在这种情况下,边界与图像大小相同,因此我们需要将其设置为原点。指定的最后一个参数是操作类型;把它放在draw.Src参数中。

现在我们必须画正方形。该操作基本上与绘制背景相同,但在本例中,我们在先前绘制的bgImage上绘制了一个正方形:

  squareWidth := 200 
  squareHeight := 200 
  squareColor := image.Uniform{color.RGBA{R: 255, G: 0, B: 0, A: 1}} 
  square := image.Rect(0, 0, squareWidth, squareHeight) 
  square = square.Add(image.Point{ 
    X: (width / 2) - (squareWidth / 2), 
    Y: (height / 2) - (squareHeight / 2), 
  }) 
  squareImg := image.NewRGBA(square) 

  draw.Print(bgImage, squareImg.Bounds(), &squareColor, origin, draw.Src) 

正方形将是 200*200 像素的红色。使用Add方法时,将Rect型原点转换到供应点;这是为了使正方形在图像上居中。我们使用正方形Rect创建一个图像,并再次调用bgImage图像上的Print函数,在其上绘制红方块:

  w, err := os.Create(t.DestinationFilePath) 
  if err != nil { 
    return fmt.Errorf("Error opening image") 
  } 
  defer w.Close() 

  if err = jpeg.Encode(w, bgImage, quality); err != nil { 
    return fmt.Errorf("Error writing image to disk") 
  } 

  return nil 
} 

最后,我们将创建一个文件来存储图像的内容。该文件将存储在ImageSquare结构的DestinationFilePath字段中提供的路径中。为了创建一个文件,我们使用返回*os.Fileos.Create。与每个文件一样,它必须在使用后关闭,因此不要忘记使用defer关键字以确保在方法完成时关闭它。

提示

推迟还是不推迟?

有人问为什么要使用defer?在函数末尾没有defer的情况下简单地编写它不是一样吗?其实不是。如果在方法执行期间发生任何错误,并且您返回此错误,Close方法如果在函数末尾,则不会执行。您可以在返回之前关闭该文件,但必须在每次错误检查中执行此操作。使用defer,时,您不必担心这一点,因为延迟函数总是被执行(无论有无错误)。这样,我们可以确保文件已关闭。

要解析参数,我们将使用flag包。我们以前使用过它,但让我们回忆一下它的用法。标志是用户在执行我们的应用程序时可以传递的命令。我们可以使用flag包中定义的flag.[type]方法来定义标志。我们希望从控制台读取用户想要使用的输出。此标志将被称为output。一个标志可以有一个默认值;在这种情况下,它将具有打印到控制台时使用的值console。因此,如果用户在没有参数的情况下执行程序,它会打印到控制台:

var output = flag.String("output", "console", "The output to use between 'console' and 'image' file") 

最后一步是编写主函数:

func main(){ 
    flag.Parse() 

请记住,在使用标志时,主要要做的第一件事是使用flag.Parse()方法解析它们!忘记这一步是很常见的:

var activeStrategy PrintStrategy 

switch *output { 
case "console": 
  activeStrategy = &TextSquare{} 
case "image": 
  activeStrategy = &ImageSquare{"/tmp/image.jpg"} 
default: 
  activeStrategy = &TextSquare{} 
} 

我们为用户选择的策略定义了一个变量,称为activeStrategy。但请检查activeStrategy变量是否具有PrintStrategy类型,以便可以使用PrintStrategy变量的任何实现填充它。当用户编写 **--output=console** 命令时,我们将activeStrategy设置为TextSquare的一个新实例;当我们编写 **--output=image** 命令时,我们将ImageSquare设置为一个新实例。

最后,这里是设计模式的执行:

  err := activeStrategy.Print() 
  if err != nil { 
    log.Fatal(err) 
  } 
}

我们的activeStrategy变量是实现PrintStrategyTextSquareImageSquare类的类型。用户将在运行时为每个特定情况选择要使用的策略。此外,我们还可以编写一个工厂方法模式来创建策略,这样策略创建也将与主功能解耦,并抽象到不同的独立包中。想想看:如果我们在一个不同的包中创建策略,它还将允许我们将此项目用作库,而不仅仅是作为一个独立的应用程序。

现在我们将执行这两种策略;TextSquare实例会在控制台上打印Square字,给我们一个正方形:

$ go run main.go --output=console
Square

它如预期的那样发挥了作用。回想一下标志是如何工作的,我们必须在本例中使用--(双破折号)和定义的标志output。然后有两个选项——使用=(等于)并立即写入标志的值,或者写入<space>和标志的值。在本例中,我们定义了控制台输出的默认值,因此以下三个执行是等效的:

$ go run main.go --output=console
Square
$ go run main.go --output console
Square
$ go run main.go
Square

现在我们必须尝试文件策略。如前所述,文件策略将以深灰色背景的图像形式在文件中打印一个红方块:

$ go run main.go --output image

什么都没发生?但一切正常。这实际上是一种不好的做法。用户在使用您的应用程序或库时必须始终有某种反馈。另外,如果他们将您的代码用作库,可能他们有特定的输出格式,因此直接打印到控制台并不好。我们稍后会解决这个问题。现在,用你最喜欢的文件浏览器打开文件夹/tmp,你会看到一个名为image.jpg的文件,背景为深灰色的红色方块。

在我们图书馆解决小问题

我们的代码中有几个问题:

  • It cannot be used as a library. We have critical code written in the main package (strategy creation).

    解决方案:将从命令行应用程序创建的策略抽象为两个不同的包。

  • None of the strategies are doing any logging to file or console. We must provide a way to read some logs that an external user can integrate in their logging strategies or formats.

    解决方案:注入io.Writer接口作为依赖项,作为日志接收器。

  • Our TextSquare class is always writing to the console (an implementer of the io.Writer interface) and the ImageSquare is always writing to file (another implementer of the io.Writer interface). This is too coupled.

    解决方案:注入io.Writer接口,以便TextSquareImageSquare可以写入任何可用的io.Writer实现(文件和控制台,还有字节缓冲区、二进制编码器、JSON处理程序……几十个包)。

因此,要将其用作库并解决第一个问题,我们将遵循应用程序和库的 Go 文件结构中的通用方法。首先,我们将把主包和函数放在根包之外;在本例中,位于名为cli的文件夹中。将此文件夹称为cmd甚至app也很常见。然后,我们将把我们的PrintStrategy接口放在根包中,现在它将被称为strategy包。最后,我们将在一个同名文件夹中创建一个shapes包,我们将在其中放置文本和图像策略。因此,我们的文件结构如下:

  • Root package: strategy

    档案:print_strategy.go

  • SubPackage: shapes

    档案:image.gotext.gofactory.go

  • SubPackage: cli

    档案:main.go

我们将稍微修改我们的界面,以满足我们之前编写的需求:

type PrintStrategy interface { 
  Print() error 
  SetLog(io.Writer) 
  SetWriter(io.Writer) 
} 

我们已经添加了SetLog(io.Writer)方法,以便在我们的类型中添加记录器策略;这是为了向用户提供反馈。此外,它还有一个SetWriter方法来设置io.Writer策略。此接口将位于print_strategy.go文件的根包上。最后的模式如下所示:

Solving small issues in our library

TextSquareImageSquare策略都必须满足SetLogSetWriter方法,这两种方法只是在它们的字段中存储一些对象,因此,我们可以创建一个实现它们的结构,并将该结构嵌入到策略中,而不是实现两次。顺便说一下,这将是我们之前看到的复合模式:

type PrintOutput struct { 
  Writer    io.Writer 
  LogWriter io.Writer 
} 

func(d *PrintOutput) SetLog(w io.Writer) { 
  d.LogWriter = w 
} 

func(d *PrintOutput) SetWriter(w io.Writer) { 
  d.Writer = w 
} 

因此,如果我们想要修改它们的Writerlogger字段,那么现在每个策略都必须嵌入PrintOutput结构。

我们还需要修改我们的策略实施。TextSquare结构现在需要一个字段来存储输出io.Writer(它将在此处写入而不是总是写入控制台)和log写入器。这两个字段可以通过嵌入PrintOutput结构来提供。TextSquare结构也存储在 shapes 包中的文件text.go中。因此,结构现在是这样的:

package shapes 

type TextSquare struct { 
  strategy.PrintOutput 
} 

所以现在的Print()方法略有不同,因为我们不需要使用println函数直接写入控制台,而是需要写入Writer字段中存储的io.Writer

func (t *TextSquare) Print() error { 
  r := bytes.NewReader([]byte("Circle")) 
  io.Copy(t.Writer, r) 
  return nil 
} 

bytes.NewReader是一个非常有用的函数,它接受字节数组并将它们转换为io.Reader接口。我们需要一个io.Reader接口来使用io.Copy功能。io.Copy函数也非常有用,因为它接受io.Reader(作为第二个参数)并将其传输到io.Writer(其第一个参数)。因此,我们在任何情况下都不会返回错误。但是,直接使用t.WriterWrite方法更容易做到:

func (t *TextSquare) Print() error { 
  t.Writer.Write([]byte("Circle")) 
  return nil 
} 

你可以用你更喜欢的方法。通常,您会使用Write方法,但也很高兴了解bytes.NewReader函数。

您是否意识到,当我们使用t.Writer时,我们实际上正在访问PrintOutput.WriterTextSquare类型有一个Writer字段,因为PrintOutput结构有它,并且它嵌入在TextSquare结构上。

提示

嵌入不是继承。我们已经在TextSquare结构上嵌入了PrintOutput结构。现在我们可以访问PrintOutput字段,就像它们在TextSquare字段中一样。这感觉有点像继承,但这里有一个非常重要的区别:TextSquare不是PrintOutput值,但它的组成中有PrintOutput。这是什么意思?如果你有一个需要PrintOutput的函数,你不能仅仅因为它有一个PrintOutput嵌入就通过TextSquare

但是,如果您有一个接受PrintOutput实现的接口的函数,那么您可以传递TextSquare,如果它嵌入了PrintOutput。这就是我们在示例中所做的。

ImageSquare结构现在类似于TextSquare,嵌入了一个PrintOutput

type ImageSquare struct { 
  strategy.PrintOutput 
} 

Print方法也需要修改。现在,我们不是从Print方法创建文件,因为它违反了单一责任原则。一个文件实现了一个io.Writer,因此我们将在ImageSquare结构外部打开该文件,并将其注入Writer字段。因此,我们只需要修改写入文件的Print()方法的结尾:

draw.Print(bgImage, squareImg.Bounds(), &squareColor, origin, draw.Src) 

if i.Writer == nil { 
  return fmt.Errorf("No writer stored on ImageSquare") 
} 
if err := jpeg.Encode(i.Writer, bgImage, quality); err != nil { 
  return fmt.Errorf("Error writing image to disk") 
} 

if i.LogWriter != nil { 
  io.Copy(i.LogWriter, "Image written in provided writer\n") 
} 

return nil 

如果您查看我们之前的实现,在使用了draw之后,您可以看到我们使用了Print方法,我们用os.Create创建了一个文件,并将其传递给jpeg.Encode函数。我们删除了关于创建文件的这一部分,并将其替换为在字段(if i.Writer != nil中查找Writer的复选框。然后,在jpeg.Encode上,我们可以将之前使用的文件值替换为i.Writer字段的内容。最后,如果提供了日志记录策略,我们将再次使用io.Copy将一些消息记录到LogWriter

我们还必须从用户那里提取所需的知识,以创建PrintStrategy实现者的实例,我们将使用工厂方法:

const ( 
  TEXT_STRATEGY  = "text" 
  IMAGE_STRATEGY = "image" 
) 

func NewPrinter(s string) (strategy.Output, error) { 
  switch s { 
  case TEXT_STRATEGY: 
    return &TextSquare{ 
      PrintOutput: strategy.PrintOutput{ 
        LogWriter: os.Stdout, 
      }, 
    }, nil 
  case IMAGE_STRATEGY: 
    return &ImageSquare{ 
      PrintOutput: strategy.PrintOutput{ 
        LogWriter: os.Stdout, 
      }, 
    }, nil 
  default: 
    return nil, fmt.Errorf("Strategy '%s' not found\n", s) 
  } 
} 

我们有两个常量,每个策略都有一个:TEXT_STRATEGYIMAGE_STRATEGY。这些常量必须提供给工厂,以检索每个方形抽屉策略。我们的工厂方法接收一个参数s,该参数是一个包含前面某个常量的字符串。

每个策略都有一个嵌入了默认记录器的PrintOutput类型stdout,但您可以稍后使用SetLog(io.Writer)方法覆盖它。这种方法可以被视为原型工厂。如果它不是可识别的策略,则将返回正确的消息错误。

我们现在拥有的是一个图书馆。我们拥有strategyshapes包之间所需的所有功能。现在我们将在名为cli的新文件夹中编写main包和函数:

var output = flag.String("output", "text", "The output to use between "+ 
  "'console' and 'image' file") 

func main() { 
  flag.Parse() 

与前面一样,main函数首先解析控制台上的输入参数,以收集所选策略。我们现在可以使用变量输出创建无工厂策略:

activeStrategy, err := shapes.NewPrinter(*output) 
if err != nil { 
  log.Fatal(err) 
} 

有了这个片段,我们就有了我们的策略,或者如果发现任何错误(比如无法识别的策略),我们就用log.Fatal方法停止程序执行。

现在,我们将使用我们的库来实现业务需求。为了TextStrategy的目的,我们想要写,例如,到stdout。为了图像的目的,我们将写信给/tmp/image.jpg。就像以前一样。因此,在前面的陈述之后,我们可以写:

switch *output { 
case shapes.TEXT_STRATEGY: 
  activeStrategy.SetWriter(os.Stdout) 
case shapes.IMAGE_STRATEGY: 
  w, err := os.Create("/tmp/image.jpg") 
  if err != nil { 
    log.Fatal("Error opening image") 
  } 
  defer w.Close() 

  activeStrategy.SetWriter(w) 
} 

TEXT_STRATEGY的情况下,我们使用SetWriterio.Writer设置为os.Stdout。在IMAGE_STRATEGY的情况下,我们在任何文件夹中创建一个图像,并将文件变量传递给SetWriter方法。记住os.File实现了io.Readerio.Writer接口,因此将其作为io.Writer传递给SetWriter方法是完全合法的:

err = activeStrategy.Print() 
if err != nil { 
  log.Fatal(err) 
} 

最后,我们调用用户选择的任何策略的Print方法,并检查可能的错误。现在让我们试试这个程序:

$ go run main.go --output text
Circle

它如预期的那样发挥了作用。那么形象策略呢?

$ go run main.go --output image
Image written in provided writer

如果我们签入/tmp/image.jpg,我们可以在黑暗的背景上找到我们的红场。

关于策略模式的最后几句话

我们已经学会了一种将算法封装在不同结构中的强大方法。我们还使用嵌入而不是继承来提供类型之间的交叉功能,这在我们的应用程序中非常有用。您会发现自己在这里和那里结合了策略,正如我们在第二个示例中所看到的,其中我们有使用io.Writer接口进行日志记录和写入的策略,这是一种字节流操作的策略。

我们的下一个模式称为责任链。顾名思义,它由一个链条组成,在我们的例子中,链条的每个环节都遵循单一责任原则。

说明

单一责任原则意味着一个类型、函数、方法或任何类似的抽象必须只有一个责任,而且必须做得很好。通过这种方式,我们可以将许多函数应用于某些结构、切片、映射等,每个函数实现一个特定的功能。

当我们经常以逻辑的方式应用这些抽象时,我们可以将它们按顺序(例如,日志记录链)链接起来执行。

记录链是一组类型,用于将某些程序的输出记录到多个io.Writer接口。我们可以有一个记录到控制台的类型,一个记录到文件的类型,以及一个记录到远程服务器的类型。每次你想做一些日志记录的时候,你可以打三个电话,但如果只打一个电话就会引起连锁反应,那就更优雅了。

但是,我们也可以有一个检查链,如果其中一个检查失败,我们可以打破检查链并退回一些东西。这是认证和授权中间件的工作原理。

目标

责任链的目标是为开发人员提供一种在运行时链接操作的方法。这些操作相互链接,每个链接将执行一些操作并将请求传递给下一个链接(或不传递)。以下是该模式遵循的目标:

  • 在运行时根据某些输入动态链接操作
  • 通过处理器链传递请求,直到其中一个处理器可以处理它,在这种情况下,该链可以停止

多记录器链

我们将开发一个多记录器解决方案,以我们想要的方式链接。我们将使用两个不同的控制台记录器和一个通用记录器:

  1. 我们需要一个简单的记录器,它用前缀First logger记录请求的文本,并将其传递给链中的下一个链接。
  2. 如果输入的文本有单词hello,第二个记录器将在控制台上写入,并将请求传递给第三个记录器。但是,如果不是这样,链条就会断开,它会立即返回。
  3. 第三种记录器类型是名为WriterLogger的通用记录器,它使用io.Writer接口进行日志记录。
  4. WriterLogger的一个具体实现写入一个文件,表示链中的第三个链接。

下图描述了这些步骤的实施:

A multi-logger chain

单元测试

与往常一样,链要做的第一件事是定义接口。责任链接口通常至少有一个Next()方法。Next()方法是执行链中下一个链接的方法,当然:

type ChainLogger interface { 
  Next(string) 
} 

我们示例界面上的Next方法获取我们想要记录的消息,并将其传递给链中的以下链接。如验收标准所述,我们需要三名伐木工人:

type FirstLogger struct { 
  NextChain ChainLogger 
} 

func (f *FirstLogger) Next(s string) {} 

type SecondLogger struct { 
  NextChain ChainLogger 
} 

func (f *SecondLogger) Next(s string) {} 

type WriterLogger struct { 
  NextChain ChainLogger 
  Writer    io.Writer 
} 
func (w *WriterLogger) Next(s string) {} 

The FirstLogger and SecondLogger types have exactly the same structure--both implement ChainLogger and have a NextChain field that points to the next ChainLogger. The WriterLogger type is equal to the FirstLogger and SecondLogger types but also has a field to write its data to, so you can pass any io.Writer interface to it.

正如我们之前所做的,我们将实现一个用于测试的io.Writer结构。在我们的测试文件中,我们定义了以下结构:

type myTestWriter struct { 
  receivedMessage string 
} 

func (m *myTestWriter) Write(p []byte) (int, error) { 
  m.receivedMessage += string(p) 
  return len(p), nil 
} 

func(m *myTestWriter) Next(s string){ 
  m.Write([]byte(s)) 
} 

我们将把myTestWriter结构的一个实例传递给WriterLogger,这样我们就可以跟踪测试中记录的内容。myTestWriter类从io.Writer接口实现公共Write([]byte) (int, error)方法。记住,如果它有Write方法,它可以用作io.WriterWrite方法只是将字符串参数存储到receivedMessage字段中,以便稍后在测试中检查其值。

这是第一个测试功能的开始:

func TestCreateDefaultChain(t *testing.T) { 
  //Our test ChainLogger 
  myWriter := myTestWriter{} 

  writerLogger := WriterLogger{Writer: &myWriter} 
  second := SecondLogger{NextChain: &writerLogger} 
  chain := FirstLogger{NextChain: &second} 

让我们稍微描述一下这几行,因为它们非常重要。我们创建了一个具有默认myTestWriter类型的变量,我们将在链的最后一个链接中将其用作io.Writer接口。然后我们创建链接链的最后一块,writerLogger接口。在执行链时,通常从链接上的最后一块开始,在我们的例子中,它是一个WriterLoggerWriterLogger写入io.Writer,所以我们将myWriter作为io.Writer接口传递。

然后我们创建了一个SecondLogger,我们链中的中间链接,带有指向writerLogger的指针。正如我们前面提到的,SecondLogger只是记录并传递消息,以防它包含单词hello。在生产应用程序中,它可能只是一个错误记录器。

最后,链中的第一个链接具有变量名 chain。它指向第二个记录器。所以,要继续,我们的链看起来是这样的:FirstLogger``SecondLogger``WriterLogger

这将是我们测试的默认设置:

t.Run("3 loggers, 2 of them writes to console, second only if it founds " + 
  "the word 'hello', third writes to some variable if second found 'hello'", 
  func(t *testing.T){ 
    chain.Next("message that breaks the chain\n") 

    if myWriter.receivedMessage != "" { 
      t.Fatal("Last link should not receive any message") 
    } 

    chain.Next("Hello\n") 

    if !strings.Contains(myWriter.receivedMessage, "Hello") { 
      t.Fatal("Last link didn't received expected message") 
    } 
}) 

继续 Go 1.7 或更高版本的测试签名,我们定义了一个内部测试,描述如下:三个记录器,其中两个写入控制台,第二个仅在找到单词“hello”时写入,第三个在第二个发现“hello”时写入某个变量。如果其他人必须维护此代码,那么它非常具有描述性,并且非常容易理解。

首先,我们在Next方法上使用的消息不会到达链中的第三个链接,因为它不包含hello这个词。我们检查receivedMessage变量的内容,默认情况下该变量为空,以查看它是否因不应更改而发生了更改。

接下来,我们再次使用链变量,即链中的第一个链接,并传递消息"Hello\n"。根据测试的描述,它应该使用FirstLogger记录,然后在SecondLogger中记录,最后在WriterLogger中记录,因为它包含hello一词,SecondLogger会让它通过。

测试检查myWriter(链中的最后一个链接,它将过去的消息存储在一个名为receivedMessage的变量中)是否具有我们在链中首先传递的单词:hello。让我们运行它,使其失败:

go test -v .
=== RUN   TestCreateDefaultChain
=== RUN   TestCreateDefaultChain/3_loggers,_2_of_them_writes_to_console,_second_only_if_it_founds_the_word_'hello',_third_writes_to_some_variable_if_second_found_'hello'
--- FAIL: TestCreateDefaultChain (0.00s)
--- FAIL: TestCreateDefaultChain/3_loggers,_2_of_them_writes_to_console,_second_only_if_it_founds_the_word_'hello',_third_writes_to_some_variable_if_second_found_'hello' (0.00s)
 chain_test.go:33: Last message didn't received expected message
FAIL
exit status 1
FAIL

测试第一次检查通过,第二次检查未通过。好理想情况下,在任何实现完成之前,都不应通过任何检查。请记住,在测试驱动的开发中,测试必须在第一次启动时失败,因为他们正在测试的代码尚未实现。Go zero 初始化误导我们通过了测试检查。我们可以通过两种方式解决这个问题:

  • ChainLogger进行签名返回错误:Next(string)错误。这样,我们将断开返回错误的链。一般来说,这是一种更方便的方法,但现在它将引入很多样板文件。
  • receivedMessage字段更改为指针。指针的默认值是 nil,而不是空字符串。

我们现在将使用第二个选项,因为它更简单,也非常有效。那么让我们将myTestWriter结构的签名更改为以下内容:

type myTestWriter struct { 
  receivedMessage *string 
} 

func (m *myTestWriter) Write(p []byte) (int, error) { 
  if m.receivedMessage == nil { 
         m.receivedMessage = new(string) 
} 
  tempMessage := fmt.Sprintf("%s%s", m.receivedMessage, p) 
  m.receivedMessage = &tempMessage 
  return len(p), nil 
} 

func (m *myTestWriter) Next(s string) { 
  m.Write([]byte(s)) 
} 

现在检查receivedMessage类型是否有星号(*,以指示它是指向字符串的指针。Write功能也需要改变。现在我们必须检查receivedMessage字段的内容,因为与每个指针一样,它被初始化为 nil。然后我们必须先将消息存储在一个变量中,这样我们就可以在赋值(m.receivedMessage = &tempMessage)的下一行中获取地址。

因此,现在我们的测试代码也应该有所改变:

t.Run("3 loggers, 2 of them writes to console, second only if it founds "+ 
"the word 'hello', third writes to some variable if second found 'hello'", 
func(t *testing.T) { 
  chain.Next("message that breaks the chain\n") 

  if myWriter.receivedMessage != nil { 
    t.Error("Last link should not receive any message") 
  } 

  chain.Next("Hello\n") 

  if myWriter.receivedMessage == "" || !strings.Contains(*myWriter.receivedMessage, "Hello") { 
    t.Fatal("Last link didn't received expected message") 
  } 
}) 

现在我们正在检查myWriter.receivedMessage实际上是nil,因此没有在变量上确定写入任何内容。此外,我们必须更改第二个 if,首先检查成员在检查其内容之前不是 nil,否则它会在测试中引发恐慌。让我们再次测试它:

go test -v . 
=== RUN   TestCreateDefaultChain 
=== RUN   TestCreateDefaultChain/3_loggers,_2_of_them_writes_to_console,_second_only_if_it_founds_the_word_'hello',_third_writes_to_some_variable_if_second_found_'hello' 
--- FAIL: TestCreateDefaultChain (0.00s) 
--- FAIL: TestCreateDefaultChain/3_loggers,_2_of_them_writes_to_console,_second_only_if_it_founds_the_word_'hello',_third_writes_to_some_variable_if_second_found_'hello' (0.00s) 
        chain_test.go:40: Last link didn't received expected message 
FAIL 
exit status 1 
FAIL

它一次又一次地失败,测试的前半部分在没有实现代码的情况下正确通过。那我们现在该怎么办呢?我们已经更改了myWriter类型的签名,以使测试在两次检查中都失败,并且再次在第二次检查中失败。那么在这种情况下我们可以通过这个小问题。在编写测试时,我们必须非常小心,不要对它们太疯狂;单元测试是帮助我们编写和维护代码的工具,但我们的目标是编写功能,而不是测试。记住这一点很重要,因为您可能会得到非常疯狂的工程单元测试。

实施

现在我们必须实现第一个、第二个和第三个记录器,分别称为FirstLoggerSecondLoggerWriterLoggerFirstLogger记录器是第一个接受标准中描述的最简单的记录器:我们需要一个简单的记录器,它记录带有前缀 first logger:的请求文本,并将其传递给链中的下一个链接。那么让我们开始吧:

type FirstLogger struct { 
  NextChain ChainLogger 
} 

func (f *FirstLogger) Next(s string) { 
  fmt.Printf("First logger: %s\n", s) 

  if f.NextChain != nil { 
    f.NextChain.Next(s) 
  } 
} 

实现起来相当容易。使用fmt.Printf方法格式化和打印传入字符串,我们添加了文本First Logger:文本。然后,我们检查NextChain类型是否确实有一些内容,并通过调用其Next(string)方法将控制传递给它。测试应该还没有通过,所以我们将继续使用SecondLogger记录器:

type SecondLogger struct { 
  NextChain ChainLogger 
} 

func (se *SecondLogger) Next(s string) { 
  if strings.Contains(strings.ToLower(s), "hello") { 
    fmt.Printf("Second logger: %s\n", s) 

    if se.NextChain != nil { 
      se.NextChain.Next(s) 
    } 

    return 
  } 

  fmt.Printf("Finishing in second logging\n\n") 
} 

如第二个验收标准中所述,SecondLogger描述为:如果传入文本有“hello”字样,则第二个记录器将在控制台上写入,并将请求传递给第三个记录器。首先,它检查传入的文本是否包含文本hello。如果为 true,它将消息打印到控制台,并附加文本Second logger:并将消息传递到链中的下一个链接(检查前一个实例是否存在第三个链接)。

但如果它不包含文本hello,则链断开并打印消息Finishing in second logging

我们将使用WriterLogger类型完成:

type WriterLogger struct { 
  NextChain ChainLogger 
  Writer    io.Writer 
} 

func (w *WriterLogger) Next(s string) { 
  if w.Writer != nil { 
    w.Writer.Write([]byte("WriterLogger: " + s)) 
  } 

  if w.NextChain != nil { 
    w.NextChain.Next(s) 
  } 
} 

WriterLogger结构的Next方法检查Writer成员中是否存储了一个现有的io.Writer接口,并将传入消息写入其中,将文本WriterLogger:附加到该接口上。然后,像前面的链接一样,检查是否有更多的链接来传递消息。

现在,测试将成功通过:

go test -v .
=== RUN   TestCreateDefaultChain
=== RUN   TestCreateDefaultChain/3_loggers,_2_of_them_writes_to_console,_second_only_if_it_founds_the_word_'hello',_third_writes_to_some_variable_if_second_found_'hello'
First logger: message that breaks the chain
Finishing in second logging
First logger: Hello
Second logger: Hello
--- PASS: TestCreateDefaultChain (0.00s)
 --- PASS: TestCreateDefaultChain/3_loggers,_2_of_them_writes_to_console,_second_only_if_it_founds_the_word_'hello',_third_writes_to_some_variable_if_second_found_'hello' (0.00s)
PASS
ok

测试的前半部分打印两条消息——断链的First logger:消息,这是FirstLogger的预期消息。但是它在SecondLogger中停止,因为在传入消息中没有找到hello字;这就是它打印Finishing in second logging字符串的原因。  

测试的后半部分收到消息Hello。所以FirstLoggerSecondLogger也会打印。第三个记录器根本不打印到控制台,而是打印到测试中定义的myWriter.receivedMessage行。

停业怎么样?

有时,在链中定义一个更灵活的链接以进行快速调试是很有用的。我们可以为此使用闭包,以便由调用方定义链接功能。闭包链接是什么样子的?与WriterLogger记录器类似:

type ClosureChain struct { 
  NextChain ChainLogger 
  Closure   func(string) 
} 

func (c *ClosureChain) Next(s string) { 
  if c.Closure != nil { 
    c.Closure(s) 
  } 

  if c.NextChain != nil { 
    c.Next(s) 
  } 
} 

ClosureChain 类型通常有一个NextChain和一个Closure 成员。请看Closure: func(string)的签名。这意味着它是一个接受string且不返回任何内容的函数。

ClosureChainNext(string)方法检查Closure成员是否已存储并使用传入字符串执行。像往常一样,链接会检查更多的链接以作为链中的每个链接传递消息。

那么,我们现在如何使用它呢?我们将定义一个新的测试来显示其功能:

t.Run("2 loggers, second uses the closure implementation", func(t *testing.T) { 
  myWriter = myTestWriter{} 
  closureLogger := ClosureChain{ 
    Closure: func(s string) { 
      fmt.Printf("My closure logger! Message: %s\n", s) 
      myWriter.receivedMessage = &s 
    }, 
  } 

  writerLogger.NextChain = &closureLogger 

  chain.Next("Hello closure logger") 

  if *myWriter.receivedMessage != "Hello closure logger" { 
    t.Fatal("Expected message wasn't received in myWriter") 
  } 
}) 

这个测试的描述很清楚:"2 loggers, second uses the closure implementation".我们只使用两个ChainLogger实现,我们在第二个链接中使用closureLogger。我们已经创建了一个新的myTestWriter来存储消息的内容。在定义ClosureChain时,我们在创建closureLogger时直接在Closure成员上定义了一个匿名函数。它打印"My closure logger! Message: %s\n" with the incoming message replacing "%s"。然后,我们将传入的消息存储在myWriter上,以便稍后检查。

定义这个新链接后,我们使用上一个测试中的第三个链接,添加闭包作为第四个链接,并传递消息Hello closure logger。我们在开头使用Hello一词,以确保消息将通过SecondLogger

最后,myWriter.receivedMessage的内容必须包含 pased 文本:Hello closure logger。这是一种非常灵活的方法,但有一个缺点:当定义这样的闭包时,我们无法以非常优雅的方式测试它的内容。让我们再次运行测试:

go test -v . 
=== RUN   TestCreateDefaultChain 
=== RUN   TestCreateDefaultChain/3_loggers,_2_of_them_writes_to_console,_second_only_if_it_founds_the_word_'hello',_third_writes_to_some_variable_if_second_found_'hello' 
First logger: message that breaks the chain 
Finishing in second logging 

First logger: Hello 
Second logger: Hello 
=== RUN   TestCreateDefaultChain/2_loggers,_second_uses_the_closure_implementation 
First logger: Hello closure logger 
Second logger: Hello closure logger 
My closure logger! Message: Hello closure logger 
--- PASS: TestCreateDefaultChain (0.00s) 
    --- PASS: TestCreateDefaultChain/3_loggers,_2_of_them_writes_to_console,_second_only_if_it_founds_the_word_'hello',_third_writes_to_some_variable_if_second_found_'hello' (0.00s) 
    --- PASS: TestCreateDefaultChain/2_loggers,_second_uses_the_closure_implementation (0.00s) 
PASS 
ok

看看第三个RUN:消息正确地通过第一、第二和第三个链接,到达打印预期My closure logger! Message: Hello closure logger消息的闭包。

向某些接口添加闭包方法实现非常有用,因为它在使用库时提供了很大的灵活性。您可以在 Go 代码中经常找到这种方法,这是包net/http中最为人所知的方法。HandleFunc函数,我们之前在结构模式中使用它来定义 HTTP 请求的处理程序。

拼凑

我们学习了一个强大的工具来实现动作的动态处理和状态处理。责任链模式被广泛使用,也用于创建有限状态机FSM)。它还可以与 Decorator 模式互换使用,不同之处在于,在装饰时,可以更改对象的结构,而在链中,可以为链中的每个链接定义行为,这些链接也可以打断对象。

在本章结束时,我们还将看到命令模式——一个很小的设计模式,但仍然经常使用。您需要一种方法来连接真正不相关的类型吗?因此,为他们设计一个命令。

说明

命令设计模式与策略设计模式非常相似,但存在关键差异。在策略模式中,我们关注的是改变算法,而在命令模式中,我们关注的是对某些东西的调用或某种类型的抽象。

命令模式通常被视为容器。您将类似于用户交互信息的内容放在一个可以是click on login的 UI 上,并将其作为命令传递。您不需要在命令中具有与click on login操作相关的复杂性,只需要操作本身。

有机世界的一个例子是快递公司的盒子。我们可以在上面放任何东西,但作为一家快递公司,我们感兴趣的是管理盒子,而不是直接管理盒子的内容。

在处理通道时,将大量使用命令模式。对于通道,您可以通过它发送任何消息,但是,如果我们需要通道接收端的响应,一种常见的方法是创建一个命令,该命令在我们正在侦听的位置附加了第二个响应通道。

类似地,一个很好的例子是多人视频游戏,其中每个用户的每一个笔划都可以通过网络作为命令发送给其他用户。

目标

当使用命令设计模式时,我们试图将某种操作或信息封装在一个必须在其他地方处理的轻量级包中。这与策略模式类似,但事实上,命令可能会在其他地方触发预配置的策略,因此它们不一样。以下是此设计模式的目标:

  • 把一些信息放进盒子里。只有听筒会打开盒子,知道里面的内容。
  • 将某些操作委托给其他地方。

下图中还解释了该行为:

Objectives

在这里,我们有一个带有Get()接口{}方法的命令接口。我们有a型和B型。其思想是AB实现命令接口,将自身作为interface{}返回。由于现在它们实现了命令,因此可以在命令处理程序中使用,该处理程序不太关心底层类型。现在AB可以通过处理命令或自由存储命令的函数进行传输。但是B处理程序可以从任何命令处理程序中获取一个对象来“展开”,并获取其B内容以及A命令处理程序及其A内容。

我们将信息放在一个框中(即命令),并将处理该信息的任务委托给命令处理程序。

简单的队列

我们的第一个例子将非常小。我们将把一些信息放入一个命令实现器,我们将有一个队列。我们将创建一个实现命令模式的类型的多个实例,并将它们传递到一个队列,该队列将存储命令,直到其中三个在队列中,此时它将处理这些命令。

验收标准

因此,为了更好地理解命令的含义,理想的接受标准应该在某种程度上反映出可以接受不相关类型的框的创建以及命令本身的执行:

  • 我们需要一个控制台打印命令的构造函数。当将此构造函数与string一起使用时,它将返回一个命令来打印它。在本例中,处理程序位于命令内部,该命令充当一个框和一个处理程序。
  • 我们需要一个数据结构,将传入的命令存储在队列中,并在队列长度达到三个时打印它们。

实施

这个模式非常简单,我们将编写几个不同的示例,因此我们将直接实现这个库,以使内容简洁明了。经典的命令设计模式通常有一个带有Execute方法的通用类型结构。我们还将使用这种结构,因为它非常灵活和简单:

type Command interface { 
  Execute() 
} 

这足够通用,可以填充许多不相关的类型!想想看——我们将创建一个类型,在使用Execute()方法时打印到控制台,但它也可以打印数字或发射火箭!这里的关键是关注调用,因为处理程序也在命令中。因此,我们需要某种类型来实现此接口,并向控制台打印某种消息:

type ConsoleOutput struct { 
  message string 
} 

func (c *ConsoleOutput) Execute() { 
  fmt.Println(c.message) 
} 

ConsoleOutput类型实现Command接口,并将名为message的成员打印到控制台。

按照第一个接受标准中的定义,我们需要一个Command构造函数来接受消息字符串并返回Command接口。它将有签名func CreateCommand(s string) Command

 func CreateCommand(s string) Command { 
   fmt.Println("Creating command") 

   return &ConsoleOutput{ 
         message: s, 
   } 
} 

对于命令queue,我们将定义一个非常简单的类型CommandQueue,将实现Command接口的任何类型存储在队列中:

type CommandQueue struct { 
  queue []Command 
} 

func (p *CommandQueue) AddCommand(c Command) { 
  p.queue = append(p.queue, c) 

  if len(p.queue) == 3 { 
    for _, command := range p.queue { 
      command.Execute() 
    } 

    p.queue = make([]Command, 3) 
  } 
} 

CommandQueue类型存储Commands接口的数组。当队列数组到达三个项目时,它将执行队列字段中存储的所有命令。如果尚未达到所需的长度,则只存储命令。

我们将创建五个足以触发命令队列机制的命令,并将它们添加到队列中。每次创建命令时,消息Creating command将打印到控制台。当我们创建第三个命令时,将启动自动命令执行器,打印前三条消息。我们还创建并添加了两个命令,但由于我们没有再次访问第三个命令,因此不会打印它们,只打印Creating command消息:

func main() { 
  queue := CommandQueue{} 

  queue.AddCommand(CreateCommand("First message")) 
  queue.AddCommand(CreateCommand("Second message")) 
  queue.AddCommand(CreateCommand("Third message")) 

  queue.AddCommand(CreateCommand("Fourth message")) 
  queue.AddCommand(CreateCommand("Fifth message")) 
} 

让我们运行main程序。我们的定义是每三条消息处理一次命令,我们将创建总共五条消息。必须打印前三条消息,但不能打印第四条和第五条消息,因为我们没有到达触发命令处理的第六条消息:

$go run command.go
Creating command
Creating command
Creating command
First message
Second message
Third message
Creating command
Creating command

正如您所看到的,第四条和第五条消息没有像预期的那样打印出来,但是我们知道这些命令是在数组中创建和存储的。它们只是没有被处理,因为队列正在等待一个或多个命令触发处理器。

更多例子

前面的示例演示了如何使用执行命令内容的命令处理程序。但是使用命令模式的一种常见方法是将信息委托给不同的对象,而不是执行。

例如,我们将创建一个提取信息的命令,而不是打印到控制台:

type Command interface { 
  Info() string 
} 

在这种情况下,我们的Command接口将有一个名为Info的方法,该方法将从其实现者那里检索一些信息。我们将创建两个实现;将返回从创建命令到执行的时间:

type TimePassed struct { 
  start time.Time 
} 

func (t *TimePassed) Info() string { 
  return time.Since(t.start).String() 
} 

time.Since函数返回从所提供参数中存储的时间开始经过的时间。我们通过调用time.Time类型上的String()方法,返回所经过时间的字符串表示形式。我们新的Command的第二次实现将返回信息Hello World!

type HelloMessage struct{} 

func (h HelloMessage) Info() string { 
  return "Hello world!" 
} 

而我们的main函数只需创建每种类型的一个实例,然后等待一秒钟并打印从每个Command返回的信息:

func main() { 
  var timeCommand Command 
  timeCommand = &TimePassed{time.Now()} 

  var helloCommand Command 
  helloCommand = &HelloMessage{} 

  time.Sleep(time.Second) 

  fmt.Println(timeCommand.Info()) 
  fmt.Println(helloCommand.Info()) 
} 

time.Sleep函数在指定的时间段(一秒钟)内停止当前 goroutine 的执行。回想一下,timeCommand变量存储程序启动的时间,它的Info()方法返回一个字符串,表示自从我们给类型赋值到调用Info()方法的那一刻起经过的时间。当我们调用其Info()方法时,helloCommand变量返回消息Hello World!。在这里,我们还没有实现一个 AutoT7AI 处理程序来简化操作,但是我们可以把控制台看作是处理程序,因为我们只能在其上打印 ASCII 字符,就像用 Tyr8T8 方法检索的一样。

让我们运行main函数:

go run command.go
1.000216755s
Hello world!

我们到了。在本例中,我们使用命令模式检索一些信息。一种类型存储time信息,而另一种类型不存储任何信息,只返回相同的简单字符串。每次运行main函数都会返回不同的运行时间,因此如果时间与示例中的时间不匹配,请不要担心。

命令责任链

您还记得责任链设计模式吗?我们在链接之间传递string消息以打印其内容。但是我们可以使用前面的命令来检索用于登录到控制台的信息。我们将主要重用已经编写的代码。

Command接口将来自返回上一示例中string的类型接口:

type Command interface { 
  Info() string 
} 

我们也将使用TimePassed类型的Command实现:

type TimePassed struct { 
  start time.Time 
} 

func (t *TimePassed) Info() string { 
  return time.Since(t.start).String() 
} 

请记住,此类型在其Info() string方法上返回从对象创建开始经过的时间。我们还需要本章责任链设计模式部分的ChainLogger接口,但这次它将通过Next方法而不是string传递命令:

type ChainLogger interface { 
  Next(Command) 
} 

为了简单起见,我们将对链中的两个链接使用相同的类型。此链接与责任链示例中的FirstLogger类型非常相似,但这次它将附加消息Elapsed time from creation:,并在打印前等待 1 秒。我们将其命名为Logger而不是FirstLogger

type Logger struct { 
  NextChain ChainLogger 
} 

func (f *Logger) Next(c Command) { 
  time.Sleep(time.Second) 

  fmt.Printf("Elapsed time from creation: %s\n", c.Info()) 

  if f.NextChain != nil { 
    f.NextChain.Next(c) 
  } 
} 

最后,我们需要一个main函数来执行接受Command指针的链:

func main() { 
  second := new(Logger) 
  first := Logger{NextChain: second} 

  command := &TimePassed{start: time.Now()} 

  first.Next(command) 
} 

逐行创建一个名为second的变量,其指针指向一个Logger;这将是我们链条中的第二个环节。然后我们创建一个名为first的变量,它将是链中的第一个链接。第一个链接指向second变量,即链中的第二个链接。

然后,我们创建一个TimePassed实例,将其用作Command类型。此命令的开始时间是执行时间(方法返回执行时刻的时间)。

最后,我们将Command接口传递给first.Next(command)语句上的链。该程序的输出如下:

go run chain_command.go
Elapsed time from creation: 1.0003419s
Elapsed time from creation: 2.000682s

结果输出反映在下图中:带有时间字段的命令被推送到知道如何执行任何类型命令的第一个链接。然后将命令传递给第二个链接,该链接也知道如何执行命令:

这种方法对每个链接上的命令处理程序隐藏了每个Command执行背后的复杂性。隐藏在命令后面的功能可能很简单,也可能非常复杂,但这里的想法是重用处理程序来管理许多类型的无关实现。

将命令模式向上取整

命令是一种非常微小的设计模式;它的功能很容易理解,但因其简单性而被广泛使用。它看起来与 Strategy 模式非常相似,但请记住 Strategy 是指有许多算法来实现某些特定任务,但它们都实现相同的任务。在命令模式中,有许多任务要执行,但并非所有任务都需要相等。

因此,简而言之,命令模式是关于执行封装和委托的,因此只有一个或多个接收器触发该执行。

我们已经在行为模式上迈出了第一步。本章的目的是向读者介绍使用适当接口和结构的算法和执行封装的概念。使用该策略,我们封装了算法、责任处理程序链和命令设计模式执行。

现在,利用我们获得的有关策略模式的知识,我们可以将应用程序与其算法严重解耦,仅用于测试,这是一个非常有用的功能,可以将模拟注入到几乎无法测试的不同类型中。但对于任何可能需要基于某些上下文的不同方法的情况(例如缩短列表;一些算法根据列表的分布执行得更好)。

责任链模式打开了任何类型的中间件和插件库的大门,以改进某些部分的功能。许多开源项目使用一个责任链来处理 HTTP 请求和响应,以向最终用户提取信息(如 cookies 信息)或检查身份验证详细信息(只有在数据库中有您的情况下,我才允许您传递到下一个链接)。

最后,命令模式是 UI 处理的最常见模式,但在许多其他场景中也非常有用,在这些场景中,我们需要在代码中传递的许多不相关类型(例如通过通道传递的消息)之间进行某种类型的处理。

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

技术教程推荐

Linux性能优化实战 -〔倪朋飞〕

Vue开发实战 -〔唐金州〕

从0开发一款iOS App -〔朱德权〕

研发效率破局之道 -〔葛俊〕

现代C++编程实战 -〔吴咏炜〕

Selenium自动化测试实战 -〔郭宏志〕

Flink核心技术与实战 -〔张利兵〕

大厂晋升指南 -〔李运华〕

后端工程师的高阶面经 -〔邓明〕