Chapter 5. Behavioral Patterns - Strategy, Chain of Responsibility, and Command Design Patterns

The last group of common patterns we are going to see are the behavioral patterns. Now, we aren't going to define structures or encapsulate object creation but we are going to deal with behaviors.

What's to deal with in behavior patterns? Well, now we will encapsulate behaviors, for example, algorithms in the Strategy pattern or executions in the command pattern.

Correct Behavior design is the last step after knowing how to deal with object creation and structures. Defining the behavior correctly is the last step of good software design because, all in all, good software design lets us improve algorithms and fix errors easily while the best algorithm implementation will not save us from bad software design.

Strategy design pattern

The Strategy pattern is probably the easiest to understand of the Behavioral patterns. We have used it a few times while developing the previous patterns but without stopping to talk about it. Now we will.

Description

The Strategy pattern uses different algorithms to achieve some specific functionality. These algorithms are hidden behind an interface and, of course, they must be interchangeable. All algorithms achieve the same functionality in a different way. For example, we could have a Sort interface and few sorting algorithms. The result is the same, some list is sorted, but we could have used quick sort, merge sort, and so on.

Can you guess when we used a Strategy pattern in the previous chapters? Three, two, one... Well, we heavily used the strategy pattern when we used the io.Writer interface. The io.Writer interface defines a strategy to write, and the functionality is always the same--to write something. We could write it to the standard out, to some file or to a user-defined type, but we do the same thing at the end--to write. We just change the strategy to write (in this case, we change the place where we write).

Objectives

The objectives of the Strategy pattern are really clear. The pattern should do the following:

  • Provide a few algorithms to achieve some specific functionality
  • All types achieve the same functionality in a different way but the client of the strategy isn't affected

The problem is that this definition covers a huge spectrum of possibilities. This is because Strategy pattern is actually used for a variety of scenarios and many software engineering solutions come with some kind of strategy within. Therefore it's better to see it in action with a real example.

Rendering images or text

We are going to do something different for this example. Instead of printing text on the console only, we are also going to paint objects on a file.

In this case, we will have two strategies: console and file. But the user of the library won't have to deal with the complexity behind them.

The key feature is that the "caller" doesn´t know how the underlying library is working and he just knows the information available on the defined strategy. This is nicely seen on the following diagram:

Rendering images or text

In this diagram, we have chosen to print to console but we won´t deal with the ConsoleStrategy type directly, we´ll always use an interface that represents it. The ConsoleStrategy type will hide the implementation details of printing to console to caller in main function. FileStrategy hides its implementation details as well as any future strategy.

Acceptance criteria

A strategy must have a very clear objective and we will have two ways to achieve it. Our objectives will be as follows:

  • Provide a way to show to the user an object (a square) in text or image
  • The user must choose between image or text when launching the app
  • The app must be able to add more visualization strategies (audio, for example)
  • If the user selects text, the word Square must be printed in the console
  • If the user selects image, an image of a white square on a black background will be printed on a file

Implementation

We aren't going to write tests for this example as it will be quite complicated to check that an image has appeared on the screen (although not impossible by using OpenCV, an impressive library for computer vision). We will start directly by defining our strategy interface that each printing strategy must implement (in our case, the file and console types):

type PrintStrategy interface { 
  Print() error 
} 

That's all. Our strategy defines a simple Print() method that returns an error (the error-returning type is mandatory when dealing with files, for example). The types that needs to implement PrintStrategy will be called ConsoleSquare and a ImageSquare type:

type ConsoleSquare struct {} 
 
type ImageSquare struct { 
  DestinationFilePath string 
} 

The ConsoleSquare struct doesn't need any inner field because it will always print the word Square to the console. The ImageSquare struct will store a field for the destination of the image file where we will print the square. We will start with the implementation of the ConsoleSquare type as it is the simplest:

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

Very easy, but the image is more complex. We won't spend too much time in explaining in detail how the image package works because the code is easily understandable:

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) 

However, here is a short explanation:

  • We define a size for the image (width and height variables) of 800 pixels of width and 600 pixels of height. Those are going to be the size limits of our image and anything that we write outside of that size won't be visible.
  • The origin variable stores an image.Point, a type to represent a position in any two-dimensional space. We set the position of this point at (0, 0), the upper-left corner of the image.
  • We need a bitmap that will represent our background, here we called it bgImage. We have a very handy function in the image package to create the image.RGBA types called image.NewRGBA. We need to pass a rectangle to this function so that it knows the bounds of the image. A rectangle is represented by two image.Point types--its upper left corner point (the Min field) and its lower right corner point (the Max field). We use origin as the upper-left and a new point with the values of width and height as the lower-right point.
  • The image will have a gray background color (bgColor). This is done by instancing a type of image.Uniform, which represents a uniform color (hence the name). The image.Uniform type needs an instance of a color.Color interface. A color.Color type is any type that implements the RGBA() (r, g, b, a uint32)  method to return a uint32 value for red, green, blue, and alpha colors (RGBA). Alpha is a value for the transparency of a pixel. The color package conveniently provides a type called color.RGBA for this purpose (in case we don't need to implement our own, which is our case).
  • When storing an image in certain formats, we have to specify the quality of the image. It will affect not only the quality but the size of the file, of course. Here, it is defined as 75; 100 is the maximum quality possible that we can set. As you can see, we are using the jpeg package here to set the value of a type called Options that simply stores the value of the quality, it doesn't have more values to apply.
  • Finally, the draw.Print function writes the pixels on the supplied image (bgImage) with the characteristics that we have defined on the bounds defined by the same image. The first argument of the draw.Print method takes the destination image, where we used bgImage. The second argument is the bounds of the object to draw in the destination image, we used the same bounds of the image but we could use any other if we wanted a smaller rectangle. The third argument is the color to use to colorize the bounds. The Origin variable is used to tell where the upper-left corner of the bound must be placed. In this case, the bounds are the same size as the image so we need to set it to the origin. The last argument specified is the operation type; just leave it in the draw.Src argument.

Now we have to draw the square. The operation is essentially the same as to draw the background but, in this case, we are drawing a square over the previously drawn 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) 

The square will be of 200*200 pixels of red color. When using the method Add, the Rect type origin is translated to the supplied point; this is to center the square on the image. We create an image with the square Rect and call the Print function on the bgImage image again to draw the red square over it:

  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 
} 

Finally, we will create a file to store the contents of the image. The file will be stored in the path supplied in the DestinationFilePath field of the ImageSquare struct. To create a file, we use os.Create that returns the *os.File. As with every file, it must be closed after using it so don't forget to use the defer keyword to ensure that you close it when the method finishes.

Tip

To defer, or not to defer?

Some people ask why the use of defer at all? Wouldn't it be the same to simply write it without defer at the end of the function? Well, actually not. If any error occurs during the method execution and you return this error, the Close method won't be executed if it's at the end of the function. You can close the file before returning but you'll have to do it in every error check. With defer, you don't have to worry about this because the deferred function is executed always (with or without error). This way, we ensure that the file is closed.

To parse the arguments, we'll use the flag package. We have used it before but let's recall its usage. A flag is a command that the user can pass when executing our app. We can define a flag by using the flag.[type] methods defined in the flag package. We want to read the output that the user wants to use from the console. This flag will be called output. A flag can have a default value; in this case, it will have the value console that will be used when printing to console. So, if the user executes the program without arguments, it prints to console:

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

Our final step is to write the main function:

func main(){ 
    flag.Parse() 

Remember that the first thing to do in the main when using flags is to parse them using the flag.Parse() method! It's very common to forget this step:

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

We define a variable for the strategy that the user has chosen, called activeStrategy. But check that the activeStrategy variable has the PrintStrategy type so it can be populated with any implementation of the PrintStrategy variable. We will set activeStrategy to a new instance of TextSquare when the user writes the  --output=console  command and an ImageSquare when we write the  --output=image command.

Finally, here is the design pattern execution:

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

Our activeStrategy variable is a type implementing PrintStrategy and either the TextSquare or ImageSquare classes. The user will choose at runtime which strategy he wants to use for each particular case. Also, we could have written a factory method pattern to create strategies, so that the strategy creation will also be uncoupled from the main function and abstracted in a different independent package. Think about it: if we have the strategy creation in a different package, it will also allow us to use this project as a library and not only as a standalone app.

Now we will execute both strategies; the TextSquare instance will give us a square by printing the word Square on the console:

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

It has worked as expected. Recalling how flags work, we have to use the -- (double dash) and the defined flag, output in our case. Then you have two options--using = (equals) and immediately writing the value for the flag or writing <space> and the value for the flag. In this case, we have defined the default value of output to the console so the following three executions are equivalent:

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

Now we have to try the file strategy. As defined before, the file strategy will print a red square to a file as an image with dark gray background:

$ go run main.go --output image

Nothing happened? But everything worked correctly. This is actually bad practice. Users must always have some sort of feedback when using your app or your library. Also, if they are using your code as a library, maybe they have a specific format for output so it won't be nice to directly print to the console. We will solve this issue later. Right now, open the folder /tmp with your favourite file explorer and you will see a file called image.jpg with our red square in a dark grey background.

Solving small issues in our library

We have a few issues in our code:

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

    Solution: Abstract to two different packages the strategy creation from the command-line application.

  • 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.

    Solution: Inject an io.Writer interface as dependency to act as a logging sink.

  • 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.

    Solution: Inject an io.Writer interface so that the TextSquare and ImageSquare can write to any of the io.Writer implementations that are available (file and console, but also bytes buffer, binary encoders, JSON handlers... dozens of packages).

So, to use it as a library and solve the first issue, we will follow a common approach in Go file structures for apps and libraries. First, we will place our main package and function outside of the root package; in this case, in a folder called cli. It is also common to call this folder cmd or even app. Then, we will place our PrintStrategy interface in the root package, which now will be called the strategy package. Finally, we will create a shapes package in a folder with the same name where we will put both text and image strategies. So, our file structure will be like this:

  • Root package: strategy

    File: print_strategy.go

  • SubPackage: shapes

    Files: image.go, text.go, factory.go

  • SubPackage: cli

    File: main.go

We are going to modify our interface a bit to fit the needs we have written previously:

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

We have added the SetLog(io.Writer) method to add a logger strategy to our types; this is to provide feedback to users. Also, it has a SetWriter method to set the io.Writer strategy. This interface is going to be located on the root package in the print_strategy.go file. So the final schema looks like this:

Solving small issues in our library

Both the TextSquare and ImageSquare strategies have to satisfy the SetLog and SetWriter methods which simply store some object on their fields so, instead of implementing the same twice, we can create a struct that implements them and embed this struct in the strategies. By the way, this would be the composite pattern we have seen previously:

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 
} 

So now each strategy must have the PrintOutput struct embedded if we want to modify their Writer and logger fields.

We also need to modify our strategy implementation. The TextSquare struct now needs a field to store the output io.Writer (the place where it is going to write instead of writing always to the console) and the log writer. These two fields can be provided by embedding the PrintOutput struct. The TextSquare struct is also stored in the file text.go within the shapes package. So, the struct is now like this:

package shapes 
 
type TextSquare struct { 
  strategy.PrintOutput 
} 

So now the Print() method is slightly different because, instead of writing directly to the console by using the println function, we have to write whichever io.Writer is stored in the Writer field:

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

The bytes.NewReader is a very useful function that takes an array of bytes and converts them to an io.Reader interface. We need an io.Reader interface to use the io.Copy function. The io.Copy function is also incredibly useful as it takes an io.Reader (as the second parameter) and pipes it to an io.Writer (its first parameter). So, we won't return an error in any case. However, it's easier to do so using directly the Write method of t.Writer:

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

You can use whichever method you like more. Usually, you will use the Write method but it's nice to know the bytes.NewReader function too.

Did you realize that when we use t.Writer, we are actually accessing PrintOutput.Writer? The TextSquare type has a Writer field because the PrintOutput struct has it and it's embedded on the TextSquare struct.

Tip

Embedding is not inheritance. We have embedded the PrintOutput struct on the TextSquare struct. Now we can access PrintOutput fields as if they were in TextSquare fields. This feels a bit like inheritance but there is a very important difference here: TextSquare is not a PrintOutput value but it has a PrintOutput in its composition. What does it mean? That if you have a function that expects a PrintOutput, you cannot a pass TextSquare just because it has a PrintOutput embedded.

But, if you have a function that accepts an interface that PrintOutput implements, you can pass TextSquare if it has a PrintOutput embedded. This is what we are doing in our example.

The ImageSquare struct is now like the TextSquare, with a PrintOutput embedded:

type ImageSquare struct { 
  strategy.PrintOutput 
} 

The Print method also needs to be modified. Now, we aren't creating a file from the Print method, as it was breaking the single responsibility principle. A file implements an io.Writer so we will open the file outside of the ImageSquare struct and inject it on the Writer field. So, we just need to modify the end of the Print() method where we wrote to the file:

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
") 
} 
 
return nil 

If you check our previous implementation, after using draw, you can see that we used the Print method, we created a file with os.Create and passed it to the jpeg.Encode function. We have deleted this part about creating the file and we have replaced it with a check looking for a Writer in the fields (if i.Writer != nil). Then, on jpeg.Encode we can replace the file value we were using previously with the content of the i.Writer field. Finally, we are using io.Copy again to log some message to the LogWriter if a logging strategy is provided.

We also have to abstract the knowledge needed from the user to create instances of implementors of the PrintStrategy for which we are going to use a Factory method:

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
", s) 
  } 
} 

We have two constants, one of each of our strategies: TEXT_STRATEGY and IMAGE_STRATEGY. Those are the constants that must be provided to the factory to retrieve each square drawer strategy. Our factory method receives an argument s, which is a string with one of the previous constants.

Each strategy has a PrintOutput type embedded with a default logger to stdout but you can override it later by using the SetLog(io.Writer) methods. This approach could be considered a Factory of prototypes. If it is not a recognized strategy, a proper message error will be returned.

What we have now is a library. We have all the functionality we need between the strategy and shapes packages. Now we will write the main package and function in a new folder called cli:

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

Again, like before, the main function starts by parsing the input arguments on the console to gather the chosen strategy. We can use the variable output now to create a strategy without Factory:

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

With this snippet, we have our strategy or we stop program execution in the log.Fatal method if any error is found (such as an unrecognized strategy).

Now we will implement the business needs by using our library. For the purpose of the TextStrategy, we want to write, for example, to stdout. For the purpose of the image, we will write to /tmp/image.jpg. Just like before. So, following the previous statements, we can write:

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) 
} 

In the case of TEXT_STRATEGY, we use SetWriter to set the io.Writer to os.Stdout. In the case of IMAGE_STRATEGY, we create an image in any of our folders and pass the file variable to the SetWriter method. Remember that os.File implements the io.Reader and io.Writer interfaces, so it's perfectly legal to pass it as an io.Writer to the SetWriter method:

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

Finally, we call the Print method of whichever strategy was chosen by the user and check for possible errors. Let's try the program now:

$ go run main.go --output text
Circle

It has worked as expected. What about the image strategy?

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

If we check in /tmp/image.jpg, we can find our red square on the dark background.

Final words on the Strategy pattern

We have learned a powerful way to encapsulate algorithms in different structs. We have also used embedding instead of inheritance to provide cross-functionality between types, which will come in handy very often in our apps. You'll find yourself combining strategies here and there as we have seen in the second example, where we have strategies for logging and writing by using the io.Writer interface, a strategy for byte-streaming operations.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.116.19.147