DEV Community

Preston Pham
Preston Pham

Posted on • Originally published at Medium on

Design Pattern: Chain Of Responsibility

A colorful painting of a group of mango farmers working in the field, in the style of 1800s, pixel art

A feature can have different logics depending on the usage context. We can hide these logics from the user (caller) by placing them behind an interface. In this way, the caller will not know, and does not need to know, which logic is being used. We call the interface Handler , and each specific logic is a Concrete Handler.

When to use

  • There is more than one object that can handle the task we need, and we don’t need to know in advance which object will do that.
  • When we want to call multiple objects without having to specify them explicitly. The objects can be combined to do a task that can be chosen at run-time.

Implementation

Code Structure

I will use an example of the logging feature. In this case, we can log a string to the console if the string is just normal information, or we will send an email if it is an error. In any case, from the client’s side, there is only the Logger interface.

Usually, we would leave the most general logic, which can be used in the most cases, at the end of the chain. We want the more specific logics to run first because they might end the chain early.

In programming and problem-solving, it’s common to prioritize more specific and potentially terminating conditions before general ones for efficiency. This approach optimizes the process by handling specific cases early, potentially avoids unnecessary processing.

Sample Code

First, we write the Logger interface:

type LogLevel string

const (
 Info LogLevel = "info"
 Error LogLevel = "error"
)

type Logger interface {
 Log(level LogLevel, text string)
}

func NewLogger() Logger {
 return &EmailLogger{
  next: &ConsoleLogger{},
 }
}
Enter fullscreen mode Exit fullscreen mode

Next is a Concrete Handler ConsoleLogger. This logger will write to the console and pass on to the next logger.

type ConsoleLogger struct {
  next Logger
}

func (l *ConsoleLogger) Log(level LogLevel, text string) {
  fmt.Printf("%s | %s\n", level, text)
  if l.next != nil {
    l.next.Log(level, text)
  }
}
Enter fullscreen mode Exit fullscreen mode

Second Concrete Handler EmailLogger will only be activated when the log level is Error.

type EmailLogger struct {
    next Logger
}

func (l *EmailLogger) Log(level LogLevel, text string) {
 if level == Error {
  fmt.Printf("%s | sent alert email to me", level)
 }
 if l.next != nil {
  l.next.Log(level, text)
 }
}
Enter fullscreen mode Exit fullscreen mode

Usage:

logger := NewLogger()
logger.Log(Info, "hello world")
logger.Log(Error, "the world has already ended due to zombies!")

// Output:
// info | hello world
// error | sent alert email to contact@codenghiemtuc.vn
// error | the world has already ended due to zombies!
Enter fullscreen mode Exit fullscreen mode

Testing

We can test each specific logger like this:

func TestConsoleLogger(t *testing.T) {
  logger := &ConsoleLogger{}
  expect := "haha"

  logger.Log(Info, expect)
  // check console output
}
Enter fullscreen mode Exit fullscreen mode

Or we can just test a chain of logger:

func TestLoggerChain(t *testing.T) {
    logger := NewLogger()
    expect := "haha"
        logger.Log(Error, expect)
         // check console output
         // check email is sent
}  
Enter fullscreen mode Exit fullscreen mode

Related Patterns

Composite

The Composite pattern can be applied to create a hierarchical structure with a child Component pointing to a parent Component, forming a chain. In this way, the child can handle requests and forward them to the parent.


Top comments (0)