Decorator Pattern
The Decorator Pattern is a structural design pattern that allows behavior to be dynamically added to objects without modifying their code.
Imagine ordering a cup of coffee at a café: you start with a basic coffee, but you can customize it by adding milk, sugar, or caramel. Each of these additions enhances the coffee without altering its fundamental nature.
Similarly, in software development, the Decorator Pattern allows us to extend the functionality of an object dynamically while keeping its core structure unchanged.
The Decorator Pattern in Action
Let’s illustrate the Decorator Pattern with a simple example before diving into a real-world use case.
Basic Example: Decorating a Notifier
Suppose we have a simple notifier that sends messages:
// Notifier interface
type Notifier interface {
Send(message string)
}
// Concrete Notifier implementation
type EmailNotifier struct{}
func (e *EmailNotifier) Send(message string) {
fmt.Println("Sending Email:", message)
}
Now, let's say we want to add SMS notifications without modifying EmailNotifier
. We can use the Decorator Pattern:
// SMS Decorator
type SMSNotifier struct {
WrappedNotifier Notifier
}
func (s *SMSNotifier) Send(message string) {
s.WrappedNotifier.Send(message) // Call original notifier
fmt.Println("Sending SMS:", message)
}
With this setup, we can now layer multiple notification methods dynamically:
func main() {
email := &EmailNotifier{}
sms := &SMSNotifier{WrappedNotifier: email}
sms.Send("Hello, World!")
}
Real-World Use Case: Go's http.RoundTripper
A great real-world example of the Decorator Pattern in Go is the http.RoundTripper
interface used in the http.Client
. The RoundTripper
interface allows developers to wrap the default HTTP transport with additional behavior, such as logging, authentication, retries, or request modifications. This follows the Decorator Pattern since it enables dynamically extending the HTTP client's functionality without altering the core implementation.
How does RoundTripper
work?
By default, Go’s http.Client
uses http.DefaultTransport
(which implements http.RoundTripper
) to send HTTP requests. When we set a custom Transport
in http.Client
, the Do
method of http.Client
internally calls Transport.RoundTrip
, allowing us to modify the request or response within our custom decorator.
How RoundTrip
Works: The RoundTrip
method is responsible for executing a single HTTP request and returning the response. It is called internally by http.Client.Do(req)
. If we replace the Transport
field in http.Client
, every request will pass through our custom RoundTripper
, enabling us to modify requests before they are sent and responses before they are returned.
For example, middleware in HTTP clients, such as logging or retry mechanisms, can be implemented by wrapping http.RoundTripper
:
// Logging HTTP Client Decorator
type LoggingTransport struct {
Wrapped http.RoundTripper
}
func (l *LoggingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
fmt.Println("Request URL:", req.URL)
return l.Wrapped.RoundTrip(req)
}
Now, we can use this decorator with an http.Client
:
client := &http.Client{
Transport: &LoggingTransport{
Wrapped: http.DefaultTransport,
},
}
This way, we dynamically extend the client's behavior without modifying its core logic, demonstrating the Decorator Pattern in practice.
Combining Multiple RoundTripper
Decorators
One of the key advantages of the Decorator Pattern is that we can chain multiple Transport
decorators together. If we need additional behavior, such as retry logic, we can wrap LoggingTransport
inside another transport:
type RetryTransport struct {
Wrapped http.RoundTripper
MaxRetries int
}
func (r *RetryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
var resp *http.Response
var err error
for i := 0; i < r.MaxRetries; i++ {
resp, err = r.Wrapped.RoundTrip(req)
if err == nil {
return resp, nil
}
fmt.Println("Retrying request...", i+1)
}
return nil, err
}
Now, we can use both LoggingTransport
and RetryTransport
together:
client := &http.Client{
Transport: &RetryTransport{
Wrapped: &LoggingTransport{
Wrapped: http.DefaultTransport,
},
MaxRetries: 3,
},
}
In this case:
- The request first goes through
RetryTransport
, which retries the request if needed. - Then, it passes through
LoggingTransport
, which logs the request details. - Finally, it reaches
http.DefaultTransport
, which actually sends the request.
Advantages of the Decorator Pattern
- Open-Closed Principle: The Decorator Pattern allows us to extend behavior without modifying existing code, adhering to the Open-Closed Principle.
- Flexible and Reusable: Decorators can be combined in different ways to achieve various functionalities, making the system highly modular.
- Dynamically Add Features: New behaviors can be added at runtime without altering the core object.
Conclusion 🍻
The Decorator Pattern is a powerful tool for extending an object’s functionality without modifying its code. It enables flexible and modular designs, making it an excellent choice for applications that require dynamic behavior enhancements. By understanding and applying the Decorator Pattern, developers can create more maintainable and scalable Go applications, ensuring they can evolve smoothly as new requirements arise.
☕ Support My Work ☕
If you enjoy my work, consider buying me a coffee! Your support helps me keep creating valuable content and sharing knowledge. ☕
Top comments (0)