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.
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.
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.
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:
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.
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".
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:
MessageA
and MessageB
that will print a message with an A:
or a B:
respectively before the message.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.
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.
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.
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.
52.15.245.1