Lesson 3
Introduction to the Strategy Pattern in Scala
Introduction

Welcome to another exciting chapter in our exploration of Behavioral Patterns in Scala! In our earlier lessons, we delved into the Command and Observer patterns, focusing on enhancing the way objects communicate and respond to changing states. In this lesson, we shift gears to the Strategy Pattern, a powerful design technique that allows for dynamism and flexibility in algorithm selection. This lesson will dissect the Strategy Pattern step-by-step and demonstrate its practical implementation using a clear, relatable example. Ready? 🔥

Understanding the Strategy Pattern

Imagine having a toolbox full of different gadgets, each perfect for a specific task. The Strategy Pattern allows a class to tap into this toolbox and choose the most suitable algorithm without altering its codebase. It's all about selecting the right strategy at runtime, providing flexibility and clean separation of concerns.

Consider a scenario with a Shopping Cart class that handles payments via multiple channels like credit cards or PayPal. By employing the Strategy Pattern, each payment method can be encapsulated into distinct classes. The Shopping Cart can then swap these strategies effortlessly.

The key components of the Strategy Pattern are:

  1. Strategy Trait: A trait defining a common interface for all strategies.
  2. Concrete Strategies: Specific case classes implementing the strategies.
  3. Context Class: A class that utilizes these strategies dynamically.

Let's roll up our sleeves and integrate the Strategy Pattern into our shopping cart scenario!

Strategy Trait

Firstly, we define a trait that all payment strategy case classes will implement. This guarantees consistency across all payment methods, allowing them to be interchangeable.

Scala
1// Define the trait for the payment strategy 2trait PaymentStrategy: 3 def pay(amount: Int): Unit

Here, the PaymentStrategy trait contains the pay method, and any class that extends this trait must provide a specific implementation for the pay method.

Concrete Strategies

Next, we implement concrete strategies that house different payment methods. We'll kick things off with the CreditCardStrategy:

Scala
1// Implementation of the CreditCard strategy 2case class CreditCardStrategy(cardNumber: String) extends PaymentStrategy: 3 def pay(amount: Int): Unit = 4 println(s"Paid $$amount using Credit Card: $cardNumber")

The CreditCardStrategy class extends PaymentStrategy and provides the pay method's logic to handle credit card transactions, requiring a card number during instantiation.

Similarly, the PayPalStrategy class encapsulates the logic for PayPal transactions. It initializes with an email address.

Scala
1// Implementation of the PayPal strategy 2case class PayPalStrategy(email: String) extends PaymentStrategy: 3 def pay(amount: Int): Unit = 4 println(s"Paid $$amount using PayPal: $email")
Context Class

In our scenario, the ShoppingCart takes the role of the context class, as it employs any designated payment strategy efficiently. This class references a PaymentStrategy and can switch strategies as needed.

Scala
1// ShoppingCart class that uses a payment strategy 2class ShoppingCart(strategy: Option[PaymentStrategy] = None): 3 def setPaymentStrategy(strategy: PaymentStrategy): ShoppingCart = 4 ShoppingCart(Some(strategy)) 5 6 def checkout(amount: Int): Unit = 7 strategy match 8 case Some(s) => s.pay(amount) 9 case None => println("No payment strategy set.")

The ShoppingCart class leverages a Scala Option to handle the possible absence of a strategy and uses pattern matching for its checkout method to determine which strategy to apply. This implementation also enforces immutability by returning a new ShoppingCart instance when setPaymentStrategy is called, aligning with best practices in Scala. By avoiding mutable state, we ensure that each ShoppingCart instance remains unmodified, promoting safer and more predictable code.

Integration in the Main Program

Finally, let's piece everything together in a main program that illustrates the combined operation of the ShoppingCart class and various payment strategies.

Scala
1@main def main(): Unit = 2 val cart = ShoppingCart() 3 4 // Create payment strategies 5 val creditCard = CreditCardStrategy("1234-5678-9876-5432") 6 val payPal = PayPalStrategy("user@example.com") 7 8 // Use CreditCard strategy and checkout 9 val cartWithCreditCard = cart.setPaymentStrategy(creditCard) 10 cartWithCreditCard.checkout(100) 11 // Output: Paid 100 using Credit Card: 1234-5678-9876-5432 12 13 // Switch to PayPal strategy and checkout 14 val cartWithPayPal = cart.setPaymentStrategy(payPal) 15 cartWithPayPal.checkout(200) 16 // Output: Paid 200 using PayPal: user@example.com

In this code snippet, we:

  1. Create a new ShoppingCart.
  2. Develop payment strategies for both credit cards and PayPal.
  3. Direct the cart to utilize the credit card strategy and proceed to checkout.
  4. Change the cart's strategy to PayPal and check out again.

The ShoppingCart adeptly alternates between strategies without modifying its underlying logic, showcasing the essence of the Strategy Pattern.

Conclusion

By understanding the Strategy Pattern, developers can boost flexibility and reusability within their codebases. Instead of embedding multiple algorithms directly into a class, strategies can be distinct classes that enrich the structure and scalability of your applications. 🌟

Imagine a real-world application: an online retail site. Different users may prefer diverse payment options such as credit cards or PayPal. With the Strategy Pattern, these payment options can coexist, empowering the shopping cart to adapt seamlessly without overhauling its foundational logic. Mastering the Strategy Pattern equips you with the skills to build adaptable systems that address changing requirements with minimal code alterations.

Time to practice now, happy coding!

Enjoy this lesson? Now it's time to practice with Cosmo!
Practice is how you turn knowledge into actual skills.