Introduction

The Decorator pattern adds new functionality to an existing object without altering its structure. It is a structural pattern as this pattern acts as a wrapper to existing class.

The instanciate a decorator struct which decorates (wraps) the original object and provides additional functionality keeping its methods signature intact.

Purpose

  • Attach additional responsibilities to an object dynamically.
  • Decorators provide a flexible alternative to inheritance for extending functionality.
  • Wrapping a present, putting it in a box, and wrapping the box.

Design Pattern Diagram

The Decorator Pattern has the following entities:

Decorator Class Diagram
  • Component defines the interface for objects that can have responsibilities added to them dynamically.
  • ConcreteComponent defines an object to which additional responsibilities can be attached.
  • Decorator maintains a reference to a Component object and defines an interface that conforms to Component’s interface.
  • ConcreteDecorator adds responsibilities to the component.

Implementation

We are explore the use of decorator pattern via following example in which we will extend an existing object that fetches a data from web service. We will decorate it by adding circuit breaker capabilities without changing the struct interface.

Lets have a Fetcher interface that defines a contract for fetching some data from different sources.

// Args of fetching function
type Args map[string]string

// Data returned by fetch
type Data map[string]string

// Fetcher fetches a data from remote endpoint
type Fetcher interface {
	// Fetch fetches the data
	Fetch(args Args) (Data, error)
}

A concrete implementation of the Fetcher interface is the Repository struct which provides some dummy data if the provided arguments are not empty, otherwise returns an error. The Repository struct is a concrete component in the context of The Decorator Pattern.

// Repository of data
type Repository struct{}

// Fetch fetches data
func (r *Repository) Fetch(args Args) (Data, error) {
	if len(args) == 0 {
		return Data{}, fmt.Errorf("No arguments are provided")
	}

	data := Data{
		"user":     "root",
		"password": "swordfish",
	}
	fmt.Printf("Repository fetched data successfully: %v\n", data)
	return data, nil
}

A Retrier struct is the decorator that adds circuit breaker capabilities to any component that implements the Fetcher interface. The Retrier has a few properties that allow that. The RetryCount property defines the number of times that the retrier should try to fetch if there is an error. The WaitInterval property defines the interval between every retry. The Fetcher property is points to the object that is decorated. The Retrier calls the Fetch function of the decorated object until it succeeds or exceed the retry policy.

// Retrier retries multiple times
type Retrier struct {
	RetryCount   int
	WaitInterval time.Duration
	Fetcher      Fetcher
}

// Fetch fetches data
func (r *Retrier) Fetch(args Args) (Data, error) {
	for retry := 1; retry <= r.RetryCount; retry++ {
		fmt.Printf("Retrier retries to fetch for %d\n", retry)
		if data, err := r.Fetcher.Fetch(args); err == nil {
			fmt.Printf("Retrier fetched for %d\n", retry)
			return data, nil
		} else if retry == r.RetryCount {
			fmt.Printf("Retrier failed to fetch for %d times\n", retry)
			return Data{}, err
		}
		fmt.Printf("Retrier is waiting after error fetch for %v\n", r.WaitInterval)
		time.Sleep(r.WaitInterval)
	}

	return Data{}, nil
}

Then we can add the new retry capabilities by wrapping the Repository instance with the Retrier:

repository := &cbreaker.Repository{}
retrier := &cbreaker.Retrier{
	RetryCount:   5,
	WaitInterval: time.Second,
	Fetcher:      repository,
}

data, err := repository.Fetch(cbreaker.Args{"id": "1"})
fmt.Printf("#1 repository.Fetch: %v\n", data)

data, err = retrier.Fetch(cbreaker.Args{})
fmt.Printf("#2 retrier.Fetch error: %v\n", err)

data, err = retrier.Fetch(cbreaker.Args{"id": "1"})
fmt.Printf("#3 retrier.Fetch: %v\n", data)

Verdict

The Decorator Pattern is more convenient for adding functionalities to objects instead of entire structs at runtime. With decoration it is also possible to remove the added functionalities dynamically.