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{},
}
}
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)
}
}
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)
}
}
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!
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
}
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
}
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)