Decorator design pattern

We'll continue this chapter with the big brother of the Proxy pattern, and maybe, one of the most powerful design patterns of all. The Decorator pattern is pretty simple, but, for instance, it provides a lot of benefits when working with legacy code.

Description

The Decorator design pattern allows you to decorate an already existing type with more functional features without actually touching it. How is it possible? Well, it uses an approach similar to matryoshka dolls, where you have a small doll that you can put inside a doll of the same shape but bigger, and so on and so forth.

The Decorator type implements the same interface of the type it decorates, and stores an instance of that type in its members. This way, you can stack as many decorators (dolls) as you want by simply storing the old decorator in a field of the new one.

Objectives

When you think about extending legacy code without the risk of breaking something, you should think of the Decorator pattern first. It's a really powerful approach to deal with this particular problem.

A different field where the Decorator is very powerful may not be so obvious though it reveals itself when creating types with lots of features based on user inputs, preferences, or similar inputs. Like in a Swiss knife, you have a base type (the frame of the knife), and from there you unfold its functionalities.

So, precisely when are we going to use the Decorator pattern? Answer to this question:

  • When you need to add functionality to some code that you don't have access to, or you don't want to modify to avoid a negative effect on the code, and follow the open/close principle (like legacy code)
  • When you want the functionality of an object to be created or altered dynamically, and the number of features is unknown and could grow fast

Example

In our example, we will prepare a Pizza type, where the core is the pizza and the ingredients are the decorating types. We will have a couple of ingredients for our pizza-onion and meat.

Acceptance criteria

The acceptance criteria for a Decorator pattern is to have a common interface and a core type, the one that all layers will be built over:

  • We must have the main interface that all decorators will implement. This interface will be called IngredientAdd, and it will have the AddIngredient() string method.
  • We must have a core PizzaDecorator type (the decorator) that we will add ingredients to.
  • We must have an ingredient "onion"  implementing the same IngredientAdd interface that will add the string onion to the returned pizza.
  • We must have a ingredient "meat" implementing the IngredientAdd interface that will add the string meat to the returned pizza.
  • When calling AddIngredient method on the top object, it must return a fully decorated pizza with the text Pizza with the following ingredients: meat, onion.

Unit test

To launch our unit tests, we must first create the basic structures described in accordance with the acceptance criteria. To begin with, the interface that all decorating types must implement is as follows:

type IngredientAdd interface { 
  AddIngredient() (string, error) 
} 

The following code defines the PizzaDecorator type, which must have IngredientAdd inside, and which implements IngredientAdd too:

type PizzaDecorator struct{ 
  Ingredient IngredientAdd 
} 

func (p *PizzaDecorator) AddIngredient() (string, error) { 
  return "", errors.New("Not implemented yet") 
} 

The definition of the Meat type will be very similar to that of the  PizzaDecorator structure:

type Meat struct { 
  Ingredient IngredientAdd 
} 
 
func (m *Meat) AddIngredient() (string, error) { 
  return "", errors.New("Not implemented yet") 
} 

Now we define the Onion struct in a similar fashion:

type Onion struct { 
  Ingredient IngredientAdd 
} 
 
func (o *Onion) AddIngredient() (string, error) { 
  return "", errors.New("Not implemented yet") 
}  

This is enough to implement the first unit test, and to allow the compiler to run them without any compiling errors:

func TestPizzaDecorator_AddIngredient(t *testing.T) { 
  pizza := &PizzaDecorator{} 
  pizzaResult, _ := pizza.AddIngredient() 
  expectedText := "Pizza with the following ingredients:" 
  if !strings.Contains(pizzaResult, expectedText) { 
    t.Errorf("When calling the add ingredient of the pizza decorator it must return the text %sthe expected text, not '%s'", pizzaResult, expectedText) 
  } 
} 

Now it must compile without problems, so we can check that the test fails:

$ go test -v -run=TestPizzaDecorator .
=== RUN   TestPizzaDecorator_AddIngredient
--- FAIL: TestPizzaDecorator_AddIngredient (0.00s)
decorator_test.go:29: Not implemented yet
decorator_test.go:34: When the the AddIngredient method of the pizza decorator object is called, it must return the text
Pizza with the following ingredients:
FAIL
exit status 1
FAIL 

Our first test is done, and we can see that the PizzaDecorator struct isn't returning anything yet, that's why it fails. We can now move on to the Onion type. The test of the Onion type is quite similar to that of the Pizza decorator, but we must also make sure that we actually add the ingredient to the IngredientAdd method and not to a nil pointer:

func TestOnion_AddIngredient(t *testing.T) { 
  onion := &Onion{} 
  onionResult, err := onion.AddIngredient() 
  if err == nil { 
    t.Errorf("When calling AddIngredient on the onion decorator without" + "an IngredientAdd on its Ingredient field must return an error, not a string with '%s'", onionResult) 
  } 

The first half of the preceding test examines the returning error when no IngredientAdd method is passed to the Onion struct initializer. As no pizza is available to add the ingredient, an error must be returned:

  onion = &Onion{&PizzaDecorator{}} 
  onionResult, err = onion.AddIngredient() 

  if err != nil { 
    t.Error(err) 
  } 
  if !strings.Contains(onionResult, "onion") { 
    t.Errorf("When calling the add ingredient of the onion decorator it" + "must return a text with the word 'onion', not '%s'", onionResult) 
  } 
} 

The second part of the Onion type test actually passes PizzaDecorator structure to the initializer. Then, we check whether no error is being returned, and also whether the returning string contains the word onion in it. This way, we can ensure that onion has been added to the pizza.

Finally for the Onion type, the console output of this test with our current implementation will be the following:

$ go test -v -run=TestOnion_AddIngredient .
=== RUN   TestOnion_AddIngredient
--- FAIL: TestOnion_AddIngredient (0.00s)
decorator_test.go:48: Not implemented yet
decorator_test.go:52: When calling the add ingredient of the onion decorator it must return a text with the word 'onion', not ''
FAIL
exit status 1
FAIL

The meat ingredient is exactly the same, but we change the type to meat instead of onion:

func TestMeat_AddIngredient(t *testing.T) { 
  meat := &Meat{} 
  meatResult, err := meat.AddIngredient() 
  if err == nil { 
    t.Errorf("When calling AddIngredient on the meat decorator without" + "an IngredientAdd in its Ingredient field must return an error," + "not a string with '%s'", meatResult) 
  } 
 
  meat = &Meat{&PizzaDecorator{}} 
  meatResult, err = meat.AddIngredient() 
  if err != nil { 
    t.Error(err) 
  } 

  if !strings.Contains(meatResult, "meat") { 
    t.Errorf("When calling the add ingredient of the meat decorator it" + "must return a text with the word 'meat', not '%s'", meatResult) 
  } 
} 

So, the result of the tests will be similar:

go test -v -run=TestMeat_AddIngredient .
=== RUN   TestMeat_AddIngredient
--- FAIL: TestMeat_AddIngredient (0.00s)
decorator_test.go:68: Not implemented yet
decorator_test.go:72: When calling the add ingredient of the meat decorator it must return a text with the word 'meat', not ''
FAIL
exit status 1
FAIL

Finally, we must check the full stack test. Creating a pizza with onion and meat must return the text Pizza with the following ingredients: meat, onion:

func TestPizzaDecorator_FullStack(t *testing.T) { 
  pizza := &Onion{&Meat{&PizzaDecorator{}}} 
  pizzaResult, err := pizza.AddIngredient() 
  if err != nil { 
    t.Error(err) 
  } 
 
  expectedText := "Pizza with the following ingredients: meat, onion" 
  if !strings.Contains(pizzaResult, expectedText){ 
    t.Errorf("When asking for a pizza with onion and meat the returned " + "string must contain the text '%s' but '%s' didn't have it", expectedText,pizzaResult) 
  } 
 
  t.Log(pizzaResult) 
} 

Our test creates a variable called pizza which, like the matryoshka dolls, embeds types of the IngredientAdd method in several levels. Calling the AddIngredient method executes the method at the "onion" level, which executes the "meat" one, which, finally, executes that of the PizzaDecorator struct. After checking that no error had been returned, we check whether the returned text follows the needs of the acceptance criteria 5. The tests are run with the following command:

go test -v -run=TestPizzaDecorator_FullStack .
=== RUN   TestPizzaDecorator_FullStack
--- FAIL: TestPizzaDecorator_FullStack (0.
decorator_test.go:80: Not implemented yet
decorator_test.go:87: When asking for a pizza with onion and meat the returned string must contain the text 'Pizza with the following ingredients: meat, onion' but '' didn't have it
FAIL
exit status 1
FAIL

From the preceding output, we can see that the tests now return an empty string for our decorated type. This is, of course, because no implementation has been done yet. This was the last test to check the fully decorated implementation. Let's look closely at the implementation then.

Implementation

We are going to start implementing the PizzaDecorator type. Its role is to provide the initial text of the full pizza:

type PizzaDecorator struct { 
  Ingredient IngredientAdd 
} 
 
func (p *PizzaDecorator) AddIngredient() (string, error) { 
  return "Pizza with the following ingredients:", nil 
} 

A single line change on the return of the AddIngredient method was enough to pass the test:

go test -v -run=TestPizzaDecorator_Add .
=== RUN   TestPizzaDecorator_AddIngredient
--- PASS: TestPizzaDecorator_AddIngredient (0.00s)
PASS
ok

Moving on to the Onion struct implementation, we must take the beginning of our IngredientAdd returned string, and add the word onion at the end of it in order to get a composed pizza in return:

type Onion struct { 
  Ingredient IngredientAdd 
} 
 
func (o *Onion) AddIngredient() (string, error) { 
  if o.Ingredient == nil { 
    return "", errors.New("An IngredientAdd is needed in the Ingredient field of the Onion") 
  } 
  s, err := o.Ingredient.AddIngredient() 
  if err != nil { 
    return "", err 
  } 
  return fmt.Sprintf("%s %s,", s, "onion"), nil 
} 

Checking that we actually have a pointer to IngredientAdd first, we use the contents of the inner IngredientAdd, and check it for errors. If no errors occur, we receive a new string composed of this content, a space, and the word onion (and no errors). Looks good enough to run the tests:

go test -v -run=TestOnion_AddIngredient .
=== RUN   TestOnion_AddIngredient
--- PASS: TestOnion_AddIngredient (0.00s)
PASS
ok

Implementation of the Meat struct is very similar:

type Meat struct { 
  Ingredient IngredientAdd 
} 
 
func (m *Meat) AddIngredient() (string, error) { 
  if m.Ingredient == nil { 
    return "", errors.New("An IngredientAdd is needed in the Ingredient field of the Meat") 
  } 
  s, err := m.Ingredient.AddIngredient() 
  if err != nil { 
    return "", err 
  } 
  return fmt.Sprintf("%s %s,", s, "meat"), nil 
} 

And here goes their test execution:

go test -v -run=TestMeat_AddIngredient .
=== RUN   TestMeat_AddIngredient
--- PASS: TestMeat_AddIngredient (0.00s)
PASS
ok

Okay. So, now all the pieces are to be tested separately. If everything is okay, the test of the full stacked solution must be passing smoothly:

go test -v -run=TestPizzaDecorator_FullStack .
=== RUN   TestPizzaDecorator_FullStack
--- PASS: TestPizzaDecorator_FullStack (0.00s)
decorator_test.go:92: Pizza with the following ingredients: meat, onion,
PASS
ok

Awesome! With the Decorator pattern, we could keep stacking IngredientAdds which call their inner pointer to add functionality to PizzaDecorator. We aren't touching the core type either, nor modifying or implementing new things. All the new features are implemented by an external type.

A real-life example - server middleware

By now, you should have understood how the Decorator pattern works. Now we can try a more advanced example using the small HTTP server that we designed in the Adapter pattern section. You learned that an HTTP server can be created by using the http package, and implementing the http.Handler interface. This interface has only one method called ServeHTTP(http.ResponseWriter, http.Request). Can we use the Decorator pattern to add more functionality to a server? Of course!

We will add a couple of pieces to this server. First, we are going to log every connection made to it to the io.Writer interface (for the sake of simplicity, we'll use the io.Writer implementation of the os.Stdout interface so that it outputs to the console). The second piece will add basic HTTP authentication to every request made to the server. If the authentication passes, a Hello Decorator! message will appear. Finally, the user will be able to select the number of decoration items that he/she wants in the server, and the server will be structured and created at runtime.

Starting with the common interface, http.Handler

We already have the common interface that we will decorate using nested types. We first need to create our core type, which is going to be the Handler that returns the sentence Hello Decorator!:

type MyServer struct{} 
 
func (m *MyServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 
  fmt.Fprintln(w, "Hello Decorator!") 
} 

This handler can be attributed to the http.Handle method to define our first endpoint. Let's check this now by creating the package's main function, and sending a GET request to it:

func main() { 
  http.Handle("/", &MyServer{}) 
 
  log.Fatal(http.ListenAndServe(":8080", nil)) 
} 

Execute the server using the Terminal to execute the  go run main.go command. Then, open a new Terminal to make the GET request. We'll use the curl command to make our requests:

$ curl http://localhost:8080
Hello Decorator!

We have crossed the first milestone of our decorated server. The next step is to decorate it with logging capabilities. To do so, we must implement the http.Handler interface, in a new type, as follows:

type LoggerServer struct { 
  Handler   http.Handler 
  LogWriter io.Writer 
} 
 
func (s *LoggerServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 
  fmt.Fprintf(s.LogWriter, "Request URI: %s
", r.RequestURI) 
  fmt.Fprintf(s.LogWriter, "Host: %s
", r.Host) 
  fmt.Fprintf(s.LogWriter, "Content Length: %d
",  
r.ContentLength) 
  fmt.Fprintf(s.LogWriter, "Method: %s
", r.Method)fmt.Fprintf(s.LogWriter, "--------------------------------
") 
 
  s.Handler.ServeHTTP(w, r) 
} 

We call this type LoggerServer. As you can see, it stores not only a Handler, but also io.Writer to write the output of the log. Our implementation of the ServeHTTP method prints the request URI, the host, the content length, and the used method io.Writer. Once printing is finished, it calls the ServeHTTP function of its inner Handler field.

We can decorate MyServer with this LoggerMiddleware:

func main() { 
  http.Handle("/", &LoggerServer{ 
    LogWriter:os.Stdout, 
    Handler:&MyServer{}, 
  }) 
 
  log.Fatal(http.ListenAndServe(":8080", nil)) 
} 

Now run the curl   command:

$ curl http://localhost:8080
Hello Decorator!

Our curl command returns the same message, but if you look at the Terminal where you have run the Go application, you can see the logging:

$ go run server_decorator.go
Request URI: /
Host: localhost:8080
Content Length: 0
Method: GET

We have decorated MyServer with logging capabilities without actually modifying it. Can we do the same with authentication? Of course! After logging the request, we will authenticate it by using HTTP Basic Authentication as follows:

type BasicAuthMiddleware struct { 
  Handler  http.Handler 
  User     string 
  Password string 
} 

The BasicAuthMiddleware middleware stores three fields--a handler to decorate like in the previous middlewares, a user, and a password, which will be the only authorization to access the contents on the server. The implementation of the decorating method will proceed as follows:

func (s *BasicAuthMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { 
  user, pass, ok := r.BasicAuth() 
 
  if ok { 
    if user == s.User && pass == s.Password { 
      s.Handler.ServeHTTP(w, r) 
    } 
    else { 
      fmt.Fprintf(w, "User or password incorrect
") 
    } 
  } 
  else { 
    fmt.Fprintln(w, "Error trying to retrieve data from Basic auth") 
  } 
} 

In the preceding implementation, we use the BasicAuth method from http.Request to automatically retrieve the user and password from the request, plus an ok/ko from the parsing action. Then we check whether the parsing is correct (returning a message to the requester if incorrect, and finishing the request). If no problems have been detected during parsing, we check whether the username and the password match with the ones stored in BasicAuthMiddleware. If the credentials are valid, we shall call the decorated type (our server), but if the credentials aren't valid, we receive the User or password incorrect message in return, and the request is finished.

Now, we need to provide the user with a way to choose among different types of servers. We will retrieve user input data in the main function. We'll have three options to choose from:

  • Simple server
  • Server with logging
  • Server with logging and authentication

We have to use the Fscanf function to retrieve input from the user:

func main() { 
  fmt.Println("Enter the type number of server you want to launch from the  following:") 
  fmt.Println("1.- Plain server") 
  fmt.Println("2.- Server with logging") 
  fmt.Println("3.- Server with logging and authentication") 
 
  var selection int 
  fmt.Fscanf(os.Stdin, "%d", &selection) 
} 

The Fscanf function needs an io.Reader  implementor as the first argument (which is going to be the input in the console), and it takes the server selected by the user from it. We'll pass os.Stdin as the io.Reader interface to retrieve user input. Then, we'll write the type of data it is going to parse. The %d specifier refers to an integer number. Finally, we'll write memory direction to store the parsed input, in this case, the memory position of the selection variable.

Once the user selects an option, we can take the basic server and decorate it at runtime, switching over to the selected option:

   switch selection { 
   case 1: 
     mySuperServer = new(MyServer) 
   case 2: 
     mySuperServer = &LoggerMiddleware{ 
       Handler:   new(MyServer), 
       LogWriter: os.Stdout, 
     } 
   case 3: 
     var user, password string 
 
     fmt.Println("Enter user and password separated by a space") 
     fmt.Fscanf(os.Stdin, "%s %s", &user, &password) 
 
     mySuperServer = &LoggerMiddleware{ 
     Handler: &SimpleAuthMiddleware{ 
       Handler:  new(MyServer), 
       User:     user, 
       Password: password, 
     }, 
     LogWriter: os.Stdout, 
   } 
   default: 
   mySuperServer = new(MyServer) 
 } 

The first option will be handled by the default switch option--a plain MyServer. In the case of the second option, we decorate a plain server with logging. The third Option is a bit more developed--we ask the user for a username and a password using Fscanf again. Note that you can scan more than one input, as we are doing to retrieve the user and the password. Then, we take the basic server, decorate it with authentication, and finally, with logging.

If you follow the indentation of the nested types of option three, the request passes through the logger, then the authentication middleware, and finally, the MyServer argument if everything is okay. The requests will follow the same route.

The end of the main function takes the decorated handler, and launches the server on the 8080 port:

http.Handle("/", mySuperServer) 
log.Fatal(http.ListenAndServe(":8080", nil)) 

So, let's launch the server with the third option:

$go run server_decorator.go 
Enter the server type number you want to launch from the following: 
1.- Plain server 
2.- Server with logging 
3.- Server with logging and authentication 
 
Enter user and password separated by a space 
mario castro

We will first test the plain server by choosing the first option. Run the server with the command go run server_decorator.go, and select the first option. Then, in a different Terminal, run the basic request with curl, as follows:

$ curl http://localhost:8080
Error trying to retrieve data from Basic auth

Uh, oh! It doesn't give us access. We haven't passed any user and password, so it tells us that we cannot continue. Let's try with some random user and password:

$ curl -u no:correct http://localhost:8080
User or password incorrect

No access! We can also check in the Terminal where we launched the server and where every request is being logged:

Request URI: /
Host: localhost:8080
Content Length: 0
Method: GET

Finally, enter the correct username and password:

$ curl -u packt:publishing http://localhost:8080
Hello Decorator!

Here we are! Our request has also been logged, and the server has granted access to us. Now we can improve our server as much as we want by writing more middlewares to decorate the server's functionality.

A few words about Go's structural typing

Go has a feature that most people dislike at the beginning--structural typing. This is when your structure defines your type without explicitly writing it. For example, when you implement an interface, you don't have to write explicitly that you are actually implementing it, contrary to languages such as Java where you have to write the keyword implements. If your method follows the signature of the interface, you are actually implementing the interface. This can also lead to accidental implementations of interface, something that could provoke an impossible-to-track mistake, but that is very unlikely.

However, structural typing also allows you to define an interface after defining their implementers. Imagine a MyPrinter struct as follows:

type MyPrinter struct{} 
func(m *MyPrinter)Print(){ 
  println("Hello") 
} 

Imagine we have been working with the MyPrinter type for few months now, but it didn't implement any interface, so it can't be a possible candidate for a Decorator pattern, or maybe it can? What if we wrote an interface that matches its Print method after a few months? Consider the following code snippet:

type Printer interface { 
  Print() 
} 

It actually implements the Printer interface, and we can use it to create a Decorator solution.

Structural typing allows a lot of flexibility when writing programs. If you don't know whether a type should be a part of an interface or not, you can leave it and add the interface later, when you are completely sure about it. This way, you can decorate types very easily and with little modification in your source code.

Summarizing the Decorator design pattern - Proxy versus Decorator

You might be wondering, what's the difference between the Decorator pattern and the Proxy pattern? In the Decorator pattern, we decorate a type dynamically. This means that the decoration may or may not be there, or it may be composed of one or many types. If you remember, the Proxy pattern wraps a type in a similar fashion, but it does so at compile time and it's more like a way to access some type.

At the same time, a decorator might implement the entire interface that the type it decorates also implements or not. So you can have an interface with 10 methods and a decorator that just implements one of them and it will still be valid. A call on a method not implemented by the decorator will be passed to the decorated type. This is a very powerful feature but also very prone to undesired behaviors at runtime if you forget to implement any interface method.

In this aspect, you may think that the Proxy pattern is less flexible, and it is. But the Decorator pattern is weaker, as you could have errors at runtime, which you can avoid at compile time by using the Proxy pattern. Just keep in mind that the Decorator is commonly used when you want to add functionality to an object at runtime, like in our web server. It's a compromise between what you need and what you want to sacrifice to achieve it.

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

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