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.
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.
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:
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.
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:
IngredientAdd
, and it will have the AddIngredient() string
method.PizzaDecorator
type (the decorator) that we will add ingredients to.IngredientAdd
interface that will add the string onion
to the returned pizza.IngredientAdd
interface that will add the string meat
to the returned pizza.AddIngredient
method on the top object, it must return a fully decorated pizza
with the text Pizza with the following ingredients: meat, onion
.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.
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.
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.
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:
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.
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.
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.
3.142.199.184