Chain of responsibility design pattern

Our next pattern is called chain of responsibility. As its name implies, it consists of a chain and, in our case, each link of the chain follows the single responsibility principle.

Description

The single responsibility principle implies that a type, function, method, or any similar abstraction must have one single responsibility only and it must do it quite well. This way, we can apply many functions that achieve one specific thing each to some struct, slice, map, and so on.

When we apply many of these abstractions in a logical way very often, we can chain them to execute in order such as, for example, a logging chain.

A logging chain is a set of types that logs the output of some program to more than one io.Writer interface. We could have a type that logs to the console, a type that logs to a file, and a type that logs to a remote server. You can make three calls every time you want to do some logging, but it's more elegant to make only one and provoke a chain reaction.

But also, we could have a chain of checks and, in case one of them fails, break the chain and return something. This is the authentication and authorization middleware works.

Objectives

The objective of the chain of responsibility is to provide to the developer a way to chain actions at runtime. The actions are chained to each other and each link will execute some action and pass the request to the next link (or not). The following are the objectives followed by this pattern:

  • Dynamically chain the actions at runtime based on some input
  • Pass a request through a chain of processors until one of them can process it, in which case the chain could be stopped

A multi-logger chain

We are going to develop a multi-logger solution that we can chain in the way we want. We will use two different console loggers and one general-purpose logger:

  1. We need a simple logger that logs the text of a request with a prefix First logger and passes it to the next link in the chain.
  2. A second logger will write on the console if the incoming text has the word hello and pass the request to a third logger. But, if not, the chain will be broken and it will return immediately.
  3. A third logger type is a general purpose logger called WriterLogger that uses an io.Writer interface to log.
  4. A concrete implementation of the WriterLogger writes to a file and represents the third link in the chain.

The implementation of these steps is described in the following figure:

A multi-logger chain

Unit test

The very first thing to do for the chain is, as usual, to define the interface. A chain of responsibility interface will usually have, at least, a  Next() method. The Next() method is the one that executes the next link in the chain, of course:

type ChainLogger interface { 
  Next(string) 
} 

The Next method on our example's interface takes the message we want to log and passes it to the following link in the chain. As written in the acceptance criteria, we need three loggers:

type FirstLogger struct { 
  NextChain ChainLogger 
} 
 
func (f *FirstLogger) Next(s string) {} 
 
type SecondLogger struct { 
  NextChain ChainLogger 
} 
 
func (f *SecondLogger) Next(s string) {} 
 
type WriterLogger struct { 
  NextChain ChainLogger 
  Writer    io.Writer 
} 
func (w *WriterLogger) Next(s string) {} 
The FirstLogger and SecondLogger types have exactly the same structure--both implement ChainLogger and have a NextChain field that points to the next ChainLogger. The WriterLogger type is equal to the FirstLogger and SecondLogger types but also has a field to write its data to, so you can pass any io.Writer interface to it.

As we have done before, we'll implement an io.Writer struct to use in our testing. In our test file, we define the following struct:

type myTestWriter struct { 
  receivedMessage string 
} 
 
func (m *myTestWriter) Write(p []byte) (int, error) { 
  m.receivedMessage += string(p) 
  return len(p), nil 
} 
 
func(m *myTestWriter) Next(s string){ 
  m.Write([]byte(s)) 
} 

We will pass an instance of the myTestWriter struct to WriterLogger so we can track what's being logged on testing. The myTestWriter class implements the common Write([]byte) (int, error) method from the io.Writer interface. Remember, if it has the Write method, it can be used as io.Writer. The Write method simply stored the string argument to the receivedMessage field so we can check later its value on tests.

This is the beginning of the first test function:

func TestCreateDefaultChain(t *testing.T) { 
  //Our test ChainLogger 
  myWriter := myTestWriter{} 
 
  writerLogger := WriterLogger{Writer: &myWriter} 
  second := SecondLogger{NextChain: &writerLogger} 
  chain := FirstLogger{NextChain: &second} 

Let's describe these few lines a bit as they are quite important. We create a variable with a default myTestWriter type that we'll use as an io.Writer interface in the last link of our chain. Then we create the last piece of the link chain, the writerLogger interface. When implementing the chain, you usually start with the last piece on the link and, in our case, it is a WriterLogger. The WriterLogger writes to an io.Writer so we pass myWriter as io.Writer interface.

Then we have created a SecondLogger, the middle link in our chain, with a pointer to the writerLogger. As we mentioned before, SecondLogger just logs and passes the message in case it contains the word hello. In a production app, it could be an error-only logger.

Finally, the first link in the chain has the variable name chain. It points to the second logger. So, to resume, our chain looks like this: FirstLogger | SecondLogger | WriterLogger.

This is going to be our default setup for our tests:

t.Run("3 loggers, 2 of them writes to console, second only if it founds " + 
  "the word 'hello', third writes to some variable if second found 'hello'", 
  func(t *testing.T){ 
    chain.Next("message that breaks the chain
") 
 
    if myWriter.receivedMessage != "" { 
      t.Fatal("Last link should not receive any message") 
    } 
 
    chain.Next("Hello
") 
 
    if !strings.Contains(myWriter.receivedMessage, "Hello") { 
      t.Fatal("Last link didn't received expected message") 
    } 
}) 

Continuing with Go 1.7 or later testing signatures, we define an inner test with the following description: three loggers, two of them write to console, the second only if it finds the word 'hello', the third writes to some variable if the second found 'hello'. It's quite descriptive and very easy to understand if someone else has to maintain this code.

First, we use a message on the Next method that will not reach the third link in the chain as it doesn't contain the word hello. We check the contents of the receivedMessage variable, that by default is empty, to see if it has changed because it shouldn't.

Next, we use the chain variable again, our first link in the chain, and pass the message "Hello ". According to the description of the test, it should log using FirstLogger, then in SecondLogger and finally in WriterLogger because it contains the word hello and the SecondLogger will let it pass.

The test checks that myWriter, the last link in the chain that stored the past message in a variable called receivedMessage, has the word that we passed first in the chain: hello. Let's run it so it fails:

go test -v .
=== RUN   TestCreateDefaultChain
=== RUN   TestCreateDefaultChain/3_loggers,_2_of_them_writes_to_console,_second_only_if_it_founds_the_word_'hello',_third_writes_to_some_variable_if_second_found_'hello'
--- FAIL: TestCreateDefaultChain (0.00s)
--- FAIL: TestCreateDefaultChain/3_loggers,_2_of_them_writes_to_console,_second_only_if_it_founds_the_word_'hello',_third_writes_to_some_variable_if_second_found_'hello' (0.00s)
        chain_test.go:33: Last message didn't received expected message
FAIL
exit status 1
FAIL

The test passed for the first check of the test and didn't for the second check. Well... ideally no check should pass before any implementation is done. Remember that in test-driven development, tests must fail on the first launch because the code they are testing isn't implemented yet. Go zero-initialization misleads us with this passed check on the test. We can solve this in two ways:

  • Making the signature of the ChainLogger to return an error: Next(string) error. This way, we would break the chain returning an error. This is a much more convenient way in general, but it will introduce quite a lot of boilerplate right now.
  • Changing the receivedMessage field to a pointer. A default value of a pointer is nil, instead of an empty string.

We will use the second option now, as it's much simpler and quite effective too. So let's change the signature of the myTestWriter struct to the following:

type myTestWriter struct { 
  receivedMessage *string 
} 
 
func (m *myTestWriter) Write(p []byte) (int, error) { 
  if m.receivedMessage == nil { 
         m.receivedMessage = new(string) 
} 
  tempMessage := fmt.Sprintf("%s%s", m.receivedMessage, p) 
  m.receivedMessage = &tempMessage 
  return len(p), nil 
} 
 
func (m *myTestWriter) Next(s string) { 
  m.Write([]byte(s)) 
} 

Check that the type of receivedMessage has the asterisk (*) now to indicate that it's a pointer to a string. The Write function needed to change too. Now we have to check the contents of the receivedMessage field because, as every pointer, it's initialized to nil. Then we have to store the message in a variable first, so we can take the address in the next line on the assignment (m.receivedMessage = &tempMessage).

So now our test code should change a bit too:

t.Run("3 loggers, 2 of them writes to console, second only if it founds "+ 
"the word 'hello', third writes to some variable if second found 'hello'", 
func(t *testing.T) { 
  chain.Next("message that breaks the chain
") 
 
  if myWriter.receivedMessage != nil { 
    t.Error("Last link should not receive any message") 
  } 
 
  chain.Next("Hello
") 
 
  if myWriter.receivedMessage == "" || !strings.Contains(*myWriter.receivedMessage, "Hello") { 
    t.Fatal("Last link didn't received expected message") 
  } 
}) 

Now we are checking that myWriter.receivedMessage is actually nil, so no content has been written for sure on the variable. Also, we have to change the second if to check first that the member isn't nil before checking its contents or it can throw a panic on test. Let's test it again:

go test -v . 
=== RUN   TestCreateDefaultChain 
=== RUN   TestCreateDefaultChain/3_loggers,_2_of_them_writes_to_console,_second_only_if_it_founds_the_word_'hello',_third_writes_to_some_variable_if_second_found_'hello' 
--- FAIL: TestCreateDefaultChain (0.00s) 
--- FAIL: TestCreateDefaultChain/3_loggers,_2_of_them_writes_to_console,_second_only_if_it_founds_the_word_'hello',_third_writes_to_some_variable_if_second_found_'hello' (0.00s) 
        chain_test.go:40: Last link didn't received expected message 
FAIL 
exit status 1 
FAIL

It fails again and, again, the first half of the test passes correctly without implemented code. So what should we do now? We have change the signature of the myWriter type to make the test fail in both checks and, again, just fail in the second. Well, in this case we can pass this small issue. When writing tests, we must be very careful to not get too crazy about them; unit tests are tools to help us write and maintain code, but our target is to write functionality, not tests. This is important to keep in mind as you can get really crazy engineering unit tests.

Implementation

Now we have to implement the first, second, and third loggers called FirstLogger, SecondLogger, and WriterLogger respectively. The FirstLogger logger is the easiest one as described in the first acceptance criterion: We need a simple logger that logs the text of a request with a prefix First logger: and passes it to the next link in the chain. So let's do it:

type FirstLogger struct { 
  NextChain ChainLogger 
} 
 
func (f *FirstLogger) Next(s string) { 
  fmt.Printf("First logger: %s
", s) 
 
  if f.NextChain != nil { 
    f.NextChain.Next(s) 
  } 
} 

The implementation is quite easy. Using the fmt.Printf method to format and print the incoming string, we appended the text First Logger: text. Then, we check that the NextChain type has actually some content and pass the control to it by calling its Next(string) method. The test shouldn't pass yet so we'll continue with the SecondLogger logger:

type SecondLogger struct { 
  NextChain ChainLogger 
} 
 
func (se *SecondLogger) Next(s string) { 
  if strings.Contains(strings.ToLower(s), "hello") { 
    fmt.Printf("Second logger: %s
", s) 
 
    if se.NextChain != nil { 
      se.NextChain.Next(s) 
    } 
 
    return 
  } 
 
  fmt.Printf("Finishing in second logging

") 
} 

As mentioned in the second acceptance criterion, the SecondLogger description is: A second logger will write on the console if the incoming text has the word "hello" and pass the request to a third logger. First of all, it checks whether the incoming text contains the text hello. If it's true, it prints the message to the console, appending the text Second logger: and passes the message to the next link in the chain (check previous instance that a third link exists).

But if it doesn't contain the text hello, the chain is broken and it prints the message Finishing in second logging.

We'll finalize with the WriterLogger type:

type WriterLogger struct { 
  NextChain ChainLogger 
  Writer    io.Writer 
} 
 
func (w *WriterLogger) Next(s string) { 
  if w.Writer != nil { 
    w.Writer.Write([]byte("WriterLogger: " + s)) 
  } 
 
  if w.NextChain != nil { 
    w.NextChain.Next(s) 
  } 
} 

The WriterLogger struct's Next method checks that there is an existing io.Writer interface stored in the Writer member and writes there the incoming message appending the text WriterLogger: to it. Then, like the previous links, check that there are more links to pass the message.

Now the tests will pass successfully:

go test -v .
=== RUN   TestCreateDefaultChain
=== RUN   TestCreateDefaultChain/3_loggers,_2_of_them_writes_to_console,_second_only_if_it_founds_the_word_'hello',_third_writes_to_some_variable_if_second_found_'hello'
First logger: message that breaks the chain
Finishing in second logging
First logger: Hello
Second logger: Hello
--- PASS: TestCreateDefaultChain (0.00s)
    --- PASS: TestCreateDefaultChain/3_loggers,_2_of_them_writes_to_console,_second_only_if_it_founds_the_word_'hello',_third_writes_to_some_variable_if_second_found_'hello' (0.00s)
PASS
ok

The first half of the test prints two messages--the First logger: message that breaks the chain, which is the expected message for the FirstLogger. But it halts in the SecondLogger because no hello word has been found on the incoming message; that's why it prints the Finishing in second logging string.   

The second half of the test receives the message Hello. So the FirstLogger prints and the SecondLogger prints too. The third logger doesn't print to console at all but to our myWriter.receivedMessage line defined in the test.

What about a closure?

Sometimes it can be useful to define an even more flexible link in the chain for quick debugging. We can use closures for this so that the link functionality is defined by the caller. What does a closure link look like? Similar to the WriterLogger logger:

type ClosureChain struct { 
  NextChain ChainLogger 
  Closure   func(string) 
} 
 
func (c *ClosureChain) Next(s string) { 
  if c.Closure != nil { 
    c.Closure(s) 
  } 
 
  if c.NextChain != nil { 
    c.Next(s) 
  } 
} 

The ClosureChain type has a NextChain, as usual, and a Closure member. Look at the signature of the Closure: func(string). This means it is a function that takes a string and returns nothing.

The Next(string) method for ClosureChain checks that the Closure member is stored and executes it with the incoming string. As usual, the link checks for more links to pass the message as every link in the chain.

So, how do we use it now? We'll define a new test to show its functionality:

t.Run("2 loggers, second uses the closure implementation", func(t *testing.T) { 
  myWriter = myTestWriter{} 
  closureLogger := ClosureChain{ 
    Closure: func(s string) { 
      fmt.Printf("My closure logger! Message: %s
", s) 
      myWriter.receivedMessage = &s 
    }, 
  } 
 
  writerLogger.NextChain = &closureLogger 
 
  chain.Next("Hello closure logger") 
 
  if *myWriter.receivedMessage != "Hello closure logger" { 
    t.Fatal("Expected message wasn't received in myWriter") 
  } 
}) 

The description of this test makes it clear: "2 loggers, second uses the closure implementation". We simply use two ChainLogger implementations and we use the closureLogger in the second link. We have created a new myTestWriter to store the contents of the message. When defining the ClosureChain, we defined an anonymous function directly on the Closure member when creating closureLogger. It prints "My closure logger! Message: %s " with the incoming message replacing "%s". Then, we store the incoming message on myWriter, to check later.

After defining this new link, we use the third link from the previous test, add the closure as the fourth link, and passed the message Hello closure logger. We use the word Hello at the beginning so that we ensure that the message will pass the SecondLogger.

Finally, the contents of myWriter.receivedMessage must contain the pased text: Hello closure logger. This is quite a flexible approach with one drawback: when defining a closure like this, we cannot test its contents in a very elegant way. Let's run the tests again:

go test -v . 
=== RUN   TestCreateDefaultChain 
=== RUN   TestCreateDefaultChain/3_loggers,_2_of_them_writes_to_console,_second_only_if_it_founds_the_word_'hello',_third_writes_to_some_variable_if_second_found_'hello' 
First logger: message that breaks the chain 
Finishing in second logging 
 
First logger: Hello 
Second logger: Hello 
=== RUN   TestCreateDefaultChain/2_loggers,_second_uses_the_closure_implementation 
First logger: Hello closure logger 
Second logger: Hello closure logger 
My closure logger! Message: Hello closure logger 
--- PASS: TestCreateDefaultChain (0.00s) 
    --- PASS: TestCreateDefaultChain/3_loggers,_2_of_them_writes_to_console,_second_only_if_it_founds_the_word_'hello',_third_writes_to_some_variable_if_second_found_'hello' (0.00s) 
    --- PASS: TestCreateDefaultChain/2_loggers,_second_uses_the_closure_implementation (0.00s) 
PASS 
ok

Look at the third RUN: the message passes correctly through the first, second, and third links to arrive at the closure that prints the expected  My closure logger! Message: Hello closure logger message.

It's very useful to add a closure method implementation to some interfaces as it provides quite a lot of flexibility when using the library. You can find this approach very often in Go code, being the most known the one of package net/http. The HandleFunc function which we used previously in the structural patterns to define a handler for an HTTP request.

Putting it together

We learned a powerful tool to achieve dynamic processing of actions and state handling. The Chain of responsibility pattern is widely used, also to create Finite State Machines (FSM). It is also used interchangeably with the Decorator pattern with the difference that when you decorate, you change the structure of an object while with the chain you define a behavior for each link in the chain that can break it too.

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

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