Simplifying Strategy Pattern with 3 Golang examples

Chidozie C. Okafor
5 min readApr 16, 2023

--

The strategy pattern is a behavioral design pattern that allows an algorithm to be selected at runtime. It is especially useful when you have multiple solutions to a problem and want to switch between them quickly. In this article, we will discuss the strategy pattern, illustrate it with a simple illustration, look at examples, and solve a real-world problem using Golang.

Consider yourself a chef preparing a salad. You can use a variety of cutting techniques, including slicing, dicing, and chopping. You can select the appropriate cutting technique based on the ingredients and the desired outcome. In this analogy, the cutting techniques represent the various strategies that can be used.

Strategy Pattern Components

  1. Context: Represents the entity that uses different strategies.
  2. Strategy Interface: An interface that defines the method signature that all concrete strategies must implement.
  3. Concrete Strategies: A set of structs/classes that implement the Strategy Interface.

Let’s implement the strategy pattern to solve a real-world problem.

1st Problem:

Design a payment system that supports multiple payment methods like credit card, PayPal, and cryptocurrency.

  1. Strategy Interface: PaymentMethod
package main

type PaymentMethod interface {
Pay(amount float64) string
}

2. Concrete Strategies: CreditCard, PayPal, and Cryptocurrency

type CreditCard struct {
name, cardNumber string
}

func (c *CreditCard) Pay(amount float64) string {
return fmt.Sprintf("Paid %.2f using Credit Card (%s)", amount, c.cardNumber)
}

type PayPal struct {
email string
}

func (p *PayPal) Pay(amount float64) string {
return fmt.Sprintf("Paid %.2f using PayPal (%s)", amount, p.email)
}

type Cryptocurrency struct {
walletAddress string
}

func (c *Cryptocurrency) Pay(amount float64) string {
return fmt.Sprintf("Paid %.2f using Cryptocurrency (%s)", amount, c.walletAddress)
}

3. Context: ShoppingCart

type ShoppingCart struct {
items []Item
paymentMethod PaymentMethod
}

func (s *ShoppingCart) SetPaymentMethod(paymentMethod PaymentMethod) {
s.paymentMethod = paymentMethod
}

func (s *ShoppingCart) Checkout() string {
var total float64
for _, item := range s.items {
total += item.price
}
return s.paymentMethod.Pay(total)
}

using these implementations

func main() {
shoppingCart := &ShoppingCart{
items: []Item{
{"Laptop", 1500},
{"Smartphone", 1000},
},
}

creditCard := &CreditCard{"Chidozie C. Okafor", "4111-1111-1111-1111"}
paypal := &PayPal{"chidosiky2015@gmail.com"}
cryptocurrency := &Cryptocurrency{"0xAbcDe1234FghIjKlMnOp"}

shoppingCart.SetPaymentMethod(creditCard)
fmt.Println(shoppingCart.Checkout())

shoppingCart.SetPaymentMethod(paypal)
fmt.Println(shoppingCart.Checkout())

shoppingCart.SetPaymentMethod(cryptocurrency)
fmt.Println(shoppingCart.Checkout())
}

output

Paid 2500.00 using Credit Card (4111-1111-1111-1111)
Paid 2500.00 using PayPal (chidosiky2015@gmail.com)
Paid 2500.00 using Cryptocurrency (0xAbcDe1234FghIjKlMnOp)

Let’s explain our payment system

We created a simple payment system that accepts credit card, PayPal, and cryptocurrency payments. The goal is for users to be able to select their preferred payment method at runtime.

  1. Strategy Interface — PaymentMethod: We defined a PaymentMethod interface with a single method, Pay(). Any concrete payment method we create must implement this method.
  2. Concrete Strategies — CreditCard, PayPal, and Cryptocurrency: We created three structs, each representing a different payment method: CreditCard, PayPal, and Cryptocurrency. The PaymentMethod interface requires that each struct implement the Pay() method. Pay() returns a formatted string indicating the payment process.
  3. Context — ShoppingCart: We built a ShoppingCart struct with a list of items and a paymentMethod field. The paymentMethod field contains the selected payment method. SetPaymentMethod() in ShoppingCart accepts a PaymentMethod as input and sets the paymentMethod field accordingly. ShoppingCart’s Checkout() method computes the total price of items and calls the Pay() method of the selected payment method.

2nd Problem:

Design a system to compress images using different algorithms like JPEG, PNG, or GIF.

  1. Strategy Interface — CompressionAlgorithm:
type CompressionAlgorithm interface {
Compress(data []byte) ([]byte, error)
}

2. Concrete Strategies — JPEGCompression, PNGCompression, and GIFCompression:

type JPEGCompression struct{}

func (j *JPEGCompression) Compress(data []byte) ([]byte, error) {
// Please Implement your own 🥰JPEG compression algorithm
}

type PNGCompression struct{}

func (p *PNGCompression) Compress(data []byte) ([]byte, error) {
// Please Implement your own 🥰 PNG compression algorithm
}

type GIFCompression struct{}

func (g *GIFCompression) Compress(data []byte) ([]byte, error) {
// Please Implement your own 🥰 GIF compression algorithm
}

3. Context — ImageProcessor:

type ImageProcessor struct {
compressionAlgorithm CompressionAlgorithm
}

func (i *ImageProcessor) SetCompressionAlgorithm(algorithm CompressionAlgorithm) {
i.compressionAlgorithm = algorithm
}

func (i *ImageProcessor) Process(data []byte) ([]byte, error) {
return i.compressionAlgorithm.Compress(data)
}

You can now pass different strategy and it would work perfectly.

3rd Problem:

Design a route planning system that supports different algorithms like shortest distance, least traffic, or fastest time.

  1. Strategy Interface — RoutePlanningAlgorithm:
type RoutePlanningAlgorithm interface {
FindRoute(source, destination string) []string
}

2. Concrete Strategies — ShortestDistance, LeastTraffic, and FastestTime:

type ShortestDistance struct{}

func (s *ShortestDistance) FindRoute(source, destination string) []string {
// Please Implement your own 🥰 shortest distance algorithm
}

type LeastTraffic struct{}

func (l *LeastTraffic) FindRoute(source, destination string) []string {
// Please Implement your own 🥰 least traffic algorithm
}

type FastestTime struct{}

func (f *FastestTime) FindRoute(source, destination string) []string {
// Please Implement your own 🥰 fastest time algorithm
}

3. Context — RoutePlanner:

type RoutePlanner struct {
routePlanningAlgorithm RoutePlanningAlgorithm
}

func (r *RoutePlanner) SetRoutePlanningAlgorithm(algorithm RoutePlanningAlgorithm) {
r.routePlanningAlgorithm = algorithm
}

func (r *RoutePlanner) PlanRoute(source, destination string) []string {
return r.routePlanningAlgorithm.FindRoute(source, destination)
}

usage:

func main() {
routePlanner := &RoutePlanner{}

shortestDistance := &ShortestDistance{}
leastTraffic := &LeastTraffic{}
fastestTime := &FastestTime{}

source := "A"
destination := "B"

routePlanner.SetRoutePlanningAlgorithm(shortestDistance)
fmt.Println("Shortest Distance Route:", routePlanner.PlanRoute(source, destination))

routePlanner.SetRoutePlanningAlgorithm(leastTraffic)
fmt.Println("Least Traffic Route:", routePlanner.PlanRoute(source, destination))

routePlanner.SetRoutePlanningAlgorithm(fastestTime)
fmt.Println("Fastest Time Route:", routePlanner.PlanRoute(source, destination))
}

We begin the strategy pattern by defining a strategy interface. The strategy interface is a critical component of the pattern because it establishes the contract for various algorithms to follow. It includes method signatures that all concrete strategies must follow. Because the algorithms share a common interface, they can be interchanged.

Let’s review the essential components of the strategy pattern and their purposes:

  1. Strategy Interface: The strategy interface is an abstraction that defines the contract for different algorithms. By providing a common interface, it allows for seamless switching between different algorithms at runtime.
  2. Concrete Strategies: Concrete strategies are strategy interface implementations. They represent the various algorithms that can be used to solve a specific problem. Each concrete strategy must follow the contract defined by the strategy interface in order to be interchangeable.
  3. Context: The context is the element that employs the strategies. It typically includes a reference to the strategy interface, allowing it to interact with any concrete strategy that implements the interface. The context may expose methods for changing the strategy at runtime or for carrying out the selected strategy.

The strategy pattern promotes separation of concerns and makes it simple to extend or modify the behavior of a system without changing the context class. When adding new algorithms or modifying existing ones, you only need to work with the concrete strategy classes, leaving the context class alone.

To summarize, the strategy pattern is a strong design pattern that allows for the selection and swapping of algorithms at runtime. You can create flexible, maintainable, and extensible code by defining a common strategy interface, implementing concrete strategies, and managing these strategies within a context.

--

--

Chidozie C. Okafor
Chidozie C. Okafor

Written by Chidozie C. Okafor

Software Engineer & Backend Magician 🎩 | Python, Rust | TypeScript, Node.js | Golang | Kafka & GRPC

No responses yet