Chapter 7. Behavioral Patterns - Visitor, State, Mediator, and Observer Design Patterns

This is the last chapter about Behavioral patterns and it also closes this book's section about common, well known design patterns in Go language.

In this chapter, we are going to look at three more design patterns. Visitor pattern is very useful when you want to abstract away some functionality from a set of objects.

State is used commonly to build Finite State Machines (FSM) and, in this section, we will develop a small guess the number game.

Finally, the Observer pattern is commonly used in event-driven architectures and is gaining a lot of traction again, especially in the microservices world.

After this chapter, we'll need to feel very comfortable with common design patterns before digging in concurrency and the advantages (and complexity), it brings to design patterns.

Visitor design pattern

In the next design pattern, we are going to delegate some logic of an object's type to an external type called the visitor that will visit our object to perform operations on it.

Description

In the Visitor design pattern, we are trying to separate the logic needed to work with a specific object outside of the object itself. So we could have many different visitors that do some things to specific types.

For example, imagine that we have a log writer that writes to console. We could make the logger "visitable" so that you can prepend any text to each log. We could write a Visitor pattern that prepends the date, the time, and the hostname to a field stored in the object.

Objectives

With Behavioral design patterns we are mainly dealing with algorithms. Visitor patterns are not an exception. The objectives that we are trying to achieve are as follows:

  • To separate the algorithm of some type from its implementation within some other type
  • To improve the flexibility of some types by using them with little or no logic at all so all new functionality can be added without altering the object structure
  • To fix a structure or behavior that would break the open/closed principle in a type

You might be thinking what the open/closed principle is. In computer science, the open/closed principle states that: entities should be open for  extension but closed for modification. This simple state has lots of implications that allows building more maintainable software and less prone to errors. And the Visitor pattern helps us to delegate some commonly changing algorithm from a type that we need it to be "stable" to an external type that can change often without affecting our original one.

A log appender

We are going to develop a simple log appender as an example of the Visitor pattern. Following the approach we have had in previous chapters, we will start with an extremely simple example to clearly understand how the Visitor design pattern works before jumping to a more complex one. We have already developed similar examples modifying texts too, but in slightly different ways.

For this particular example, we will create a Visitor that appends different information to the types it "visits".

Acceptance criteria

To effectively use the Visitor design pattern, we must have two roles--a visitor and a visitable. The Visitor is the type that will act within a Visitable type. So a Visitable interface implementation has an algorithm detached to the Visitor type:

  1. We need two message loggers: MessageA and MessageB that will print a message with an A: or a B: respectively before the message.
  2. We need a Visitor able to modify the message to be printed. It will append the text "Visited A" or "Visited B" to them, respectively.

Unit tests

As we mentioned before, we will need a role for the Visitor and the Visitable interfaces. They will be interfaces. We also need the MessageA and MessageB structs:

package visitor 
 
import ( 
  "io" 
  "os" 
  "fmt" 
) 
 
type MessageA struct { 
  Msg string 
  Output io.Writer 
} 
 
type MessageB struct { 
  Msg string 
  Output io.Writer 
} 
 
type Visitor interface { 
  VisitA(*MessageA) 
  VisitB(*MessageB) 
} 
 
type Visitable interface { 
  Accept(Visitor) 
} 
 
type MessageVisitor struct {} 

The types MessageA and MessageB structs both have an Msg field to store the text that they will print. The output io.Writer will implement the os.Stdout interface by default or a new io.Writer interface, like the one we will use to check that the contents are correct.

The Visitor interface has a Visit method, one for each of Visitable interface's MessageA and MessageB type. The Visitable interface has a method called Accept(Visitor) that will execute the decoupled algorithm.

Like in previous examples, we will create a type that implements the io.Writer package so that we can use it in tests:

package visitor 
 
import "testing" 
 
type TestHelper struct { 
  Received string 
} 
 
func (t *TestHelper) Write(p []byte) (int, error) { 
  t.Received = string(p) 
  return len(p), nil 
} 

The TestHelper struct implements the io.Writer interface. Its functionality is quite simple; it stores the written bytes on the Received field. Later we can check the contents of Received to test against our expected value.

We will write just one test that will check the overall correctness of the code. Within this test, we will write two sub tests: one for MessageA and one for MessageB types:

func Test_Overall(t *testing.T) { 
  testHelper := &TestHelper{} 
  visitor := &MessageVisitor{} 
  ... 
} 

We will use a TestHelper struct and a MessageVisitor struct on each test for each message type. First, we will test the MessageA type:

func Test_Overall(t *testing.T) { 
  testHelper := &TestHelper{} 
  visitor := &MessageVisitor{} 
 
  t.Run("MessageA test", func(t *testing.T){ 
    msg := MessageA{ 
      Msg: "Hello World", 
      Output: testHelper, 
    } 
 
    msg.Accept(visitor) 
    msg.Print() 
 
    expected := "A: Hello World (Visited A)" 
    if testHelper.Received !=  expected { 
      t.Errorf("Expected result was incorrect. %s != %s", 
      testHelper.Received, expected) 
    } 
  }) 
  ... 
} 

This is the full first test. We created MessageA struct, giving it a value Hello World for the Msg field and the pointer to TestHelper, which we created at the beginning of the test. Then, we execute its Accept method. Inside the Accept(Visitor) method on the MessageA struct, the VisitA(*MessageA) method is executed to alter the contents of the Msg field (that's why we passed the pointer to VisitA method, without a pointer the contents won't be persisted).

To test if the Visitor type has done its job within the Accept method, we must call the Print() method on the MessageA type later. This way, the MessageA struct must write the contents of Msg to the provided io.Writer interface (our TestHelper).

The last part of the test is the check. According to the description of acceptance criteria 2, the output text of MessageA type must be prefixed with the text A:, the stored message and the text "(Visited)" just at the end. So, for the MessageA type, the expected text must be "A: Hello World (Visited)", this is the check that we did in the if section.

The MessageB type has a very similar implementation:

  t.Run("MessageB test", func(t *testing.T){ 
    msg := MessageB { 
      Msg: "Hello World", 
      Output: testHelper, 
    } 
 
    msg.Accept(visitor) 
    msg.Print() 
 
    expected := "B: Hello World (Visited B)" 
    if testHelper.Received !=  expected { 
      t.Errorf("Expected result was incorrect. %s != %s", 
        testHelper.Received, expected) 
    } 
  }) 
} 

In fact, we have just changed the type from MessageA to MessageB and the expected text now is "B: Hello World (Visited B)". The Msg field is also "Hello World" and we also used the TestHelper type.

We still lack the correct implementations of the interfaces to compile the code and run the tests. The MessageA and MessageB structs have to implement the Accept(Visitor) method:

func (m *MessageA) Accept(v Visitor) { 
  //Do nothing 
} 
 
func (m *MessageB) Accept(v Visitor) { 
  //Do nothing 
} 

We need the implementations of the VisitA(*MessageA) and VisitB(*MessageB) methods that are declared on the Visitor interface. The MessageVisitor interface is the type that must implement them:

func (mf *MessageVisitor) VisitA(m *MessageA){ 
  //Do nothing 
} 
func (mf *MessageVisitor) VisitB(m *MessageB){ 
  //Do nothing 
} 

Finally, we will create a Print() method for each message type. This is the method that we will use to test the contents of the Msg field on each type:

func (m *MessageA) Print(){ 
  //Do nothing 
} 
 
func (m *MessageB) Print(){ 
  //Do nothing 
} 

Now we can run the tests to really check if they are failing yet:

go test -v .
=== RUN   Test_Overall
=== RUN   Test_Overall/MessageA_test
=== RUN   Test_Overall/MessageB_test
--- FAIL: Test_Overall (0.00s)
  --- FAIL: Test_Overall/MessageA_test (0.00s)
      visitor_test.go:30: Expected result was incorrect.  != A: Hello World (Visited A)
  --- FAIL: Test_Overall/MessageB_test (0.00s)
      visitor_test.go:46: Expected result was incorrect.  != B: Hello World (Visited B)
FAIL
exit status 1
FAIL

The outputs of the tests are clear. The expected messages were incorrect because the contents were empty. It's time to create the implementations.

Implementation of Visitor pattern

We will start completing the implementation of the VisitA(*MessageA) and VisitB(*MessageB) methods:

func (mf *MessageVisitor) VisitA(m *MessageA){ 
  m.Msg = fmt.Sprintf("%s %s", m.Msg, "(Visited A)") 
} 
func (mf *MessageVisitor) VisitB(m *MessageB){ 
  m.Msg = fmt.Sprintf("%s %s", m.Msg, "(Visited B)") 
} 

Its functionality is quite straightforward--the fmt.Sprintf method returns a formatted string with the actual contents of m.Msg, a white space, and the message, Visited. This string will be stored on the Msg field, overriding the previous contents.

Now we will develop the Accept method for each message type that must execute the corresponding Visitor:

func (m *MessageA) Accept(v Visitor) { 
  v.VisitA(m) 
} 
 
func (m *MessageB) Accept(v Visitor) { 
  v.VisitB(m) 
} 

This small code has some implications on it. In both cases, we are using a Visitor, which in our example is exactly the same as the MessageVisitor interface, but they could be completely different. The key is to understand that the Visitor pattern executes an algorithm in its Visit method that deals with the Visitable object. What could the Visitor be doing? In this example, it alters the Visitable object, but it could be simply fetching information from it. For example, we could have a Person type with lots of fields: name, surname, age, address, city, postal code, and so on. We could write a Visitor to fetch just the name and surname from a person as a unique string, a visitor to fetch the address info for a different section of an app, and so on.

Finally, there is the Print() method, which will help us to test the types. We mentioned before that it must print to the Stdout call by default:

func (m *MessageA) Print() { 
  if m.Output == nil { 
    m.Output = os.Stdout 
  } 
 
  fmt.Fprintf(m.Output, "A: %s", m.Msg) 
} 
 
func (m *MessageB) Print() { 
  if m.Output == nil { 
    m.Output = os.Stdout 
  } 
  fmt.Fprintf(m.Output, "B: %s", m.Msg) 
} 

It first checks the content of the Output field to assign the output of the os.Stdout call in case it is null. In our tests, we are storing a pointer there to our TestHelper type so this line is never executed in our test. Finally, each message type prints to the Output field, the full message stored in the Msg field. This is done by using the Fprintf method, which takes an io.Writer package as the first argument and the text to format as the next arguments.

Our implementation is now complete and we can run the tests again to see if they all pass now:

go test -v .
=== RUN   Test_Overall
=== RUN   Test_Overall/MessageA_test
=== RUN   Test_Overall/MessageB_test
--- PASS: Test_Overall (0.00s)
  --- PASS: Test_Overall/MessageA_test (0.00s)
  --- PASS: Test_Overall/MessageB_test (0.00s)
PASS
ok

Everything is OK! The Visitor pattern has done its job flawlessly and the message contents were altered after calling their Visit methods. The very important thing here is that we can add more functionality to both the structs, MessageA and MessageB, without altering their types. We can just create a new Visitor type that does everything on the Visitable, for example, we can create a Visitor to add a method that prints the contents of the Msg field:

type MsgFieldVisitorPrinter struct {} 
 
func (mf *MsgFieldVisitorPrinter) VisitA(m *MessageA){ 
  fmt.Printf(m.Msg) 
} 
func (mf *MsgFieldVisitorPrinter) VisitB(m *MessageB){ 
  fmt.Printf(m.Msg) 
} 

We have just added some functionality to both types without altering their contents! That's the power of the Visitor design pattern.

Another example

We will develop a second example, this one a bit more complex. In this case, we will emulate an online shop with a few products. The products will have plain types, with just fields and we will make a couple of visitors to deal with them in the group.

First of all, we will develop the interfaces. The ProductInfoRetriever type has a method to get the price and the name of the product. The Visitor interface, like before, has a Visit method that accepts the ProductInfoRetriever type. Finally, Visitable interface is exactly the same; it has an Accept method that takes a Visitor type as an argument:

type ProductInfoRetriever interface { 
  GetPrice() float32 
  GetName() string 
} 
 
type Visitor interface { 
  Visit(ProductInfoRetriever) 
} 
 
type Visitable interface { 
  Accept(Visitor) 
} 

All the products of the online shop must implement the ProductInfoRetriever type. Also, most products will have some commons fields, such as name or price (the ones defined in the ProductInfoRetriever interface). We created the Product type, implemented the ProductInfoRetriever and the Visitable interfaces, and embedded it on each product:

type Product struct { 
  Price float32 
  Name  string 
} 
 
func (p *Product) GetPrice() float32 { 
  return p.Price 
} 
 
func (p *Product) Accept(v Visitor) { 
  v.Visit(p) 
} 
 
func (p *Product) GetName() string { 
  return p.Name 
} 

Now we have a very generic Product type that can store the information about almost any product of the shop. For example, we could have a Rice and a Pasta product:

type Rice struct { 
  Product 
} 
 
type Pasta struct { 
  Product 
} 

Each has the Product type embedded. Now we need to create a couple of Visitors interfaces, one that sums the price of all products and one that prints the name of each product:

type PriceVisitor struct { 
  Sum float32 
} 
 
func (pv *PriceVisitor) Visit(p ProductInfoRetriever) { 
  pv.Sum += p.GetPrice() 
} 
 
type NamePrinter struct { 
  ProductList string 
} 
 
func (n *NamePrinter) Visit(p ProductInfoRetriever) { 
  n.Names = fmt.Sprintf("%s
%s", p.GetName(), n.ProductList) 
} 

The PriceVisitor struct takes the value of the Price variable of the ProductInfoRetriever type, passed as an argument, and adds it to the Sum field. The NamePrinter struct stores the name of the ProductInfoRetriever type, passed as an argument, and appends it to a new line on the ProductList field.

Time for the main function:

func main() { 
  products := make([]Visitable, 2) 
  products[0] = &Rice{ 
    Product: Product{ 
      Price: 32.0, 
      Name:  "Some rice", 
    }, 
  } 
  products[1] = &Pasta{ 
    Product: Product{ 
      Price: 40.0, 
      Name:  "Some pasta", 
    }, 
  } 
 
  //Print the sum of prices 
  priceVisitor := &PriceVisitor{} 
 
  for _, p := range products { 
    p.Accept(priceVisitor) 
  } 
 
  fmt.Printf("Total: %f
", priceVisitor.Sum) 
 
  //Print the products list 
  nameVisitor := &NamePrinter{} 
 
  for _, p := range products { 
    p.Accept(nameVisitor) 
  } 
 
  fmt.Printf("
Product list:
-------------
%s",  nameVisitor.ProductList) 
} 

We create a slice of two Visitable objects: a Rice and a Pasta type with some arbitrary names. Then we iterate for each of them using a PriceVisitor instance as an argument. We print the total price after the range for. Finally, we repeat this operation with the NamePrinter and print the resulting ProductList. The output of this main function is as follows:

go run visitor.go
Total: 72.000000
Product list:
-------------
Some pasta
Some rice

Ok, this is a nice example of the Visitor pattern but... what if there are special considerations about a product? For example, what if we need to sum 20 to the total price of a fridge type? OK, let's write the Fridge structure:

type Fridge struct { 
  Product 
} 

The idea here is to just override the GetPrice() method to return the product's price plus 20:

type Fridge struct { 
  Product 
} 
 
func (f *Fridge) GetPrice() float32 { 
  return f.Product.Price + 20 
} 

Unfortunately, this isn't enough for our example. The Fridge structure is not of a  Visitable type. The Product struct is of a Visitable type and the Fridge struct has a Product struct embedded but, as we mentioned in earlier chapters, a type that embeds a second type cannot be considered of that latter type, even when it has all its fields and methods. The solution is to also implement the Accept(Visitor) method so that it can be considered as a Visitable:

type Fridge struct { 
  Product 
} 
 
func (f *Fridge) GetPrice() float32 { 
  return f.Product.Price + 20 
} 
 
func (f *Fridge) Accept(v Visitor) { 
  v.Visit(f) 
} 

Let's rewrite the main function  to add this new Fridge product to the slice:

func main() { 
  products := make([]Visitable, 3) 
  products[0] = &Rice{ 
    Product: Product{ 
      Price: 32.0, 
      Name:  "Some rice", 
    }, 
  } 
  products[1] = &Pasta{ 
    Product: Product{ 
      Price: 40.0, 
      Name:  "Some pasta", 
    }, 
  } 
  products[2] = &Fridge{ 
    Product: Product{ 
      Price: 50, 
      Name:  "A fridge", 
    }, 
  } 
  ... 
} 

Everything else continues the same. Running this new main function produces the following output:

$ go run visitor.go
Total: 142.000000
Product list:
-------------
A fridge
Some pasta
Some rice

As expected, the total price is higher now, outputting the sum of the rice (32), the pasta (40), and the fridge (50 of the product plus 20 of the transport, so 70). We could be adding visitors forever to this products, but the idea is clear--we decoupled some algorithms outside of the types to the visitors.

Visitors to the rescue!

We have seen a powerful abstraction to add new algorithms to some types. However, because of the lack of overloading in Go, this pattern could be limiting in some aspects (we have seen it in the first example, where we had to create the VisitA and VisitB implementations). In the second example, we haven't dealt with this limitation because we have used an interface to the Visit method of the Visitor struct, but we just used one type of visitor (ProductInfoRetriever) and we would have the same problem if we implemented a Visit method for a second type, which is one of the objectives of the original Gang of Four design patterns.

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

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