How the Strategy Pattern works?
See how to implement and switch between different strategies to reach a goal by using the Strategy pattern with go.
Overview
Have you ever entered to an e-commerce and buy a product?, If you have, then you may have been show different ways to pay (PayPal, Credit Card, etc.)
This purchase process is a way to see the Strategy Pattern. Strategy is a Behavioral design pattern that lets you define different strategies and switch between them to reach a goal. In the above image, you have a goal (buy a product) and different strategies to accomplish it (pay with PayPal, Credit Card, etc.).
Let’s see how we would implement this purchase process.
Structure of Strategy Pattern
First, let's understand the structure of the pattern. It is defined by 3 parts:
Context Class: This is our goal
Contract Interface: These are the rules to be followed by the Strategies
Strategy Classes: Refers to the different ways to reach my goal, are the implementation of the
Contract Interface
Here is a Class Diagram for the Strategy structure
Will also need a Client who trigger the purchase process. Will see this in the code examples.
Code time, yay!
Now, let’s take the Class Diagram into code to see how it is done in code. I’ll be using Golang because it is the language I’m comfortable with:
Define the Contract
// PaymentMethodStrategy will let us interchange // the payment method in the context type PaymentMethodStrategy interface { Pay() error }
Define the Context
// Purchase is our goal/context which maintains a reference to all the // possible strategies that the client can use to pay type Purchase struct { // we create a map to save all the strategies implementations // so in runtime, when a client execute the Process method // the client can choose the strategy to use paymentMethodStrategies map[string]PaymentMethodStrategy } func NewPurchase() Purchase { return Purchase{paymentMethodStrategies: make(map[string]PaymentMethodStrategy)} } // RegisterStrategy will let us register all the possible strategies that // the client can use func (p *Purchase) RegisterStrategy(name string, strategy PaymentMethodStrategy) { p.paymentMethodStrategies[name] = strategy } // Process processes the purchase by doing the necessary validations // and calling the Pay() method of the selected strategy by the client func (p Purchase) Process(paymentMethod string) error { // you can add logic to query and validate the order // in runtime, we chose the payment method strategy paymentMethodStrategy := p.paymentMethodStrategies[paymentMethod] // after we got the strategy implementation we just use it by calling the Pay() method if err := paymentMethodStrategy.Pay(); err != nil { return fmt.Errorf("purchase.Process.paymentMethodStrategy.Pay(): %w", err) } // here you may want to create an invoice, send a notification, etc fmt.Printf("Purchase with %s completed\n", paymentMethod) return nil }
Implement the Strategies
For this, will have 3 different classes (PayPal, CreditCard and Bank)
// PayPal implementation of the PaymentMethodStrategy interface // with the necessary logic to pay with PayPal type PayPal struct {} func NewPaypal() PayPal { return PayPal{} } func (p PayPal) Pay() error { // Here you'll add the logic to pay with PayPal fmt.Println("Processing purchase with PayPal...") return nil }
type CreditCard struct {} func NewCreditCard() CreditCard { return CreditCard{} } func (c CreditCard) Pay() error { // Here you'll add the logic to pay with credit card fmt.Println("Processing purchase with CreditCard...") return nil }
type Bank struct {} func NewBank() Bank { return Bank{} } func (b Bank) Pay() error { // Here you'll add the logic to pay with a bank fmt.Println("Processing purchase with Bank...") return nil }
Client
In this example, I’ll be using the console to execute the purchase process.
func main() { // we initialize our strategies purchase := NewPurchase() purchase.RegisterStrategy("PayPal", NewPaypal()) purchase.RegisterStrategy("CreditCard", NewCreditCard()) purchase.RegisterStrategy("Bank", NewBank()) fmt.Println("Enter the payment method you want to use (PayPal, CreditCard or Bank: ") var paymentMethod string fmt.Scanln(&paymentMethod) if err := purchase.Process(paymentMethod); err != nil { log.Fatalln(err) } }
When we run our program, this is what happen:
ℹ️ Note: This pattern is a way to implement the Open/Closed Principle of SOLID.What if we want to add another payment method?
This would be easy, you just have to create another class that implements the PaymentMethodStrategy
interface:
type Bitcoin struct {}
func NewBitcoin() Bitcoin {
return Bitcoin{}
}
func (b Bitcoin) Pay() error {
// Here you'll add the logic to pay with a Bitcoin
fmt.Println("Processing purchase with Bitcoin...")
return nil
}
And after that, you just have to register it in your context class.
purchase.RegisterStrategy("Bitcoin", NewBitcoin())
The conventional way
How would it be to implement this process without the Strategy pattern?
Well, instead of having 3 classes (Context, Contract and Strategies), we’ll only have 1 class that implements all the logic to pay with PayPal, CreditCard, Bank, etc.
Our Purchase class that implements all the logic
type Purchase struct{} func NewPurchase() Purchase { return Purchase{} } func (p Purchase) Process(paymentMethod string) error { // you can add logic to query and validate the order switch paymentMethod { case "PayPal": if err := p.payWithPayPal(); err != nil { return err } case "CreditCard": if err := p.payWithCreditCard(); err != nil { return err } case "Bank": if err := p.payWithBank(); err != nil { return err } } // here you may want to create an invoice, send a notification, etc fmt.Printf("Purchase with %s completed\n", paymentMethod) return nil } func (p Purchase) payWithPayPal() error { // Here you'll add the logic to pay with PayPal fmt.Println("Processing purchase with PayPal...") return nil } func (p Purchase) payWithCreditCard() error { // Here you'll add the logic to pay with CreditCard fmt.Println("Processing purchase with CreditCard...") return nil } func (p Purchase) payWithBank() error { // Here you'll add the logic to pay with Bank fmt.Println("Processing purchase with Bank...") return nil }
Our client
func main() { // unlike the Strategy Pattern implementation // here we don't have to initialize our strategies // because all the logic is inside the Purchase "Class" purchase := NewPurchase() fmt.Println("Enter the payment method you want to use (PayPal, CreditCard or Bank: ") var paymentMethod string fmt.Scanln(&paymentMethod) if err := purchase.Process(paymentMethod); err != nil { log.Fatalln(err) } }
When we run our program, this is what happen:
This could be fine when you first build our e-commerce, or when you know that you’ll rarely will add or modify existent logic. But if you know that the business logic will grow, things like this may happen in the future:
- It will be difficult to navigate throughout the code
- Highly chance you modify something you’re not supposed to
- Git conflicts when different people working on the same class
- A lot of conditionals to switch between the different payment methods
From the business perspective may be ok because it works, but from the technical perspective is not because it will not be maintainable in the future.
ℹ️ Note: Putting all the logic in one class, for this example, will go against the Single Responsibility Principle of SOLID.Practice
Here I showed what the Strategy Pattern is, how it is structured, and why you should use this pattern instead of putting all the logic in one class.
I invite you to implement this pattern to apply a discount to an order and share the code in the comments. Hope this blog helped you understand the Strategy pattern. See you in the next Design Pattern blog of this series.
References
Dive into Design Patterns: This article is made by the notes I took from this book
Peek: Tool to record my terminal
Article repository: You can find the examples in my GitHub